Skip to content

Commit 7ac2952

Browse files
committed
Make portalocker optional
1 parent ee452ea commit 7ac2952

File tree

9 files changed

+93
-17
lines changed

9 files changed

+93
-17
lines changed

.github/workflows/python-package.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
runs-on: ${{ matrix.os }}
1717
strategy:
1818
matrix:
19-
python-version: [3.7, 3.8, 3.9, 2.7]
19+
python-version: [3.7, 3.8, 3.9, "3.10"]
2020
os: [ubuntu-latest, windows-latest, macos-latest]
2121
include:
2222
# https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#using-environment-variables-in-a-matrix
@@ -26,8 +26,8 @@ jobs:
2626
toxenv: "py38"
2727
- python-version: 3.9
2828
toxenv: "py39"
29-
- python-version: 2.7
30-
toxenv: "py27"
29+
- python-version: "3.10"
30+
toxenv: "py310"
3131
- python-version: 3.9
3232
os: ubuntu-latest
3333
lint: "true"

msal_extensions/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
KeychainPersistence,
99
LibsecretPersistence,
1010
)
11-
from .cache_lock import CrossPlatLock
11+
try:
12+
from .cache_lock import CrossPlatLock, LockError # It needs portalocker
13+
except ImportError:
14+
from .filelock import CrossPlatLock, LockError
1215
from .token_cache import PersistedTokenCache
1316

msal_extensions/cache_lock.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@
66
import logging
77
from distutils.version import LooseVersion
88

9-
import portalocker
9+
import portalocker # pylint: disable=import-error
1010

1111

1212
logger = logging.getLogger(__name__)
1313

1414

15+
LockError = portalocker.exceptions.LockException
16+
17+
1518
class CrossPlatLock(object):
1619
"""Offers a mechanism for waiting until another process is finished interacting with a shared
1720
resource. This is specifically written to interact with a class of the same name in the .NET

msal_extensions/filelock.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
"""A cross-process lock based on exclusive creation of a given file name"""
2+
import os
3+
import sys
4+
import errno
5+
import time
6+
import logging
7+
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
class LockError(RuntimeError):
13+
"""It will be raised when unable to obtain a lock"""
14+
pass
15+
16+
17+
class CrossPlatLock(object):
18+
"""This implementation relies only on ``open(..., 'x')``"""
19+
def __init__(self, lockfile_path):
20+
self._lockpath = lockfile_path
21+
22+
def __enter__(self):
23+
self._create_lock_file('{} {}'.format(
24+
os.getpid(),
25+
sys.argv[0],
26+
).encode('utf-8')) # pylint: disable=consider-using-f-string
27+
return self
28+
29+
def _create_lock_file(self, content):
30+
timeout = 5
31+
check_interval = 0.25
32+
current_time = getattr(time, "monotonic", time.time)
33+
timeout_end = current_time() + timeout
34+
while timeout_end > current_time():
35+
try:
36+
with open(self._lockpath, 'xb') as lock_file: # pylint: disable=unspecified-encoding
37+
lock_file.write(content)
38+
return None # Happy path
39+
except ValueError: # This needs to be the first clause, for Python 2 to hit it
40+
raise LockError("Python 2 does not support atomic creation of file")
41+
except FileExistsError: # Only Python 3 will reach this clause
42+
logger.debug(
43+
"Process %d found existing lock file, will retry after %f second",
44+
os.getpid(), check_interval)
45+
time.sleep(check_interval)
46+
raise LockError(
47+
"Unable to obtain lock, despite trying for {} second(s). "
48+
"You may want to manually remove the stale lock file {}".format(
49+
timeout,
50+
self._lockpath,
51+
))
52+
53+
def __exit__(self, *args):
54+
try:
55+
os.remove(self._lockpath)
56+
except OSError as ex: # pylint: disable=invalid-name
57+
if ex.errno in (errno.ENOENT, errno.EACCES):
58+
# Probably another process has raced this one
59+
# and ended up clearing or locking the file for itself.
60+
logger.debug("Unable to remove lock file")
61+
else:
62+
raise
63+

msal_extensions/token_cache.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55

66
import msal
77

8-
from .cache_lock import CrossPlatLock
8+
try:
9+
from .cache_lock import CrossPlatLock # It needs portalocker
10+
except ImportError:
11+
from .filelock import CrossPlatLock
912
from .persistence import _mkdir_p, PersistenceNotFound
1013

1114

setup.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@
2222
package_data={'': ['LICENSE']},
2323
install_requires=[
2424
'msal>=0.4.1,<2.0.0',
25-
25+
"pathlib2;python_version<'3.0'",
26+
## We choose to NOT define a hard dependency on this.
27+
# "pygobject>=3,<4;platform_system=='Linux'",
28+
],
29+
extras_require={
30+
"portalocker": [
2631
# In order to implement these requirements:
2732
# Lowerbound = (1.6 if playform_system == 'Windows' else 1.0)
2833
# Upperbound < (3 if python_version >= '3.5' else 2)
@@ -32,10 +37,7 @@
3237
"portalocker<2,>=1.0;python_version=='2.7' and platform_system!='Windows'",
3338
"portalocker<3,>=1.6;python_version>='3.5' and platform_system=='Windows'",
3439
"portalocker<2,>=1.6;python_version=='2.7' and platform_system=='Windows'",
35-
36-
"pathlib2;python_version<'3.0'",
37-
## We choose to NOT define a hard dependency on this.
38-
# "pygobject>=3,<4;platform_system=='Linux'",
39-
],
40+
],
41+
},
4042
tests_require=['pytest'],
4143
)

tests/cache_file_generator.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414
import sys
1515
import time
1616

17-
from portalocker import exceptions
17+
from msal_extensions import FilePersistence, CrossPlatLock, LockError
1818

19-
from msal_extensions import FilePersistence, CrossPlatLock
19+
20+
print("Testing with {}".format(CrossPlatLock))
2021

2122

2223
def _acquire_lock_and_write_to_cache(cache_location, sleep_interval):
@@ -31,7 +32,7 @@ def _acquire_lock_and_write_to_cache(cache_location, sleep_interval):
3132
time.sleep(sleep_interval)
3233
data += "> " + str(os.getpid()) + "\n"
3334
cache_accessor.save(data)
34-
except exceptions.LockException as e:
35+
except LockError as e:
3536
logging.warning("Unable to acquire lock %s", e)
3637

3738

tests/test_crossplatlock.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import pytest
2-
from msal_extensions.cache_lock import CrossPlatLock
2+
from msal_extensions import CrossPlatLock
33

44

55
def test_ensure_file_deleted():
@@ -10,6 +10,7 @@ def test_ensure_file_deleted():
1010
except NameError:
1111
FileNotFoundError = IOError
1212

13+
print("Testing with {}".format(CrossPlatLock))
1314
with CrossPlatLock(lockfile):
1415
pass
1516

tox.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[tox]
2-
envlist = py27,py35,py36,py37,py38
2+
envlist = py35,py36,py37,py38,py39,py310
33

44
[testenv]
55
deps = pytest

0 commit comments

Comments
 (0)