diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml new file mode 100644 index 0000000..7570e26 --- /dev/null +++ b/.github/workflows/python-package.yml @@ -0,0 +1,109 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: CI + +on: + push: + pull_request: + branches: [ dev ] + # This guards against unknown PR until a community member vet it and label it. + types: [ labeled ] + +jobs: + ci: + + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: [3.7, 3.8, 3.9, 2.7] + os: [ubuntu-latest, windows-latest, macos-latest] + include: + # https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#using-environment-variables-in-a-matrix + - python-version: 3.7 + toxenv: "py37" + - python-version: 3.8 + toxenv: "py38" + - python-version: 3.9 + toxenv: "py39" + - python-version: 2.7 + toxenv: "py27" + - python-version: 3.9 + os: ubuntu-latest + lint: "true" + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install Linux dependencies for Python 2 + if: ${{ matrix.os == 'ubuntu-latest' && matrix.python-version == '2.7' }} + run: | + sudo apt update + sudo apt install python-dev libgirepository1.0-dev libcairo2-dev gir1.2-secret-1 gnome-keyring + - name: Install Linux dependencies for Python 3 + if: ${{ matrix.os == 'ubuntu-latest' && matrix.python-version != '2.7' }} + run: | + sudo apt update + sudo apt install python3-dev libgirepository1.0-dev libcairo2-dev gir1.2-secret-1 gnome-keyring + - name: Install PyGObject on Linux + if: ${{ matrix.os == 'ubuntu-latest' }} + run: | + python -m pip install --upgrade pip + python -m pip install pygobject + - name: Install Python dependencies + run: | + python -m pip install --upgrade pip + python -m pip install pylint tox pytest + pip install . + - name: Lint + if: ${{ matrix.lint == 'true' }} + run: | + pylint msal_extensions + # stop the build if there are Python syntax errors or undefined names + #flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + #flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test on Linux with encryption + if: ${{ matrix.os == 'ubuntu-latest' }} + run: | + # Don't know why, but the pytest and "." have to be re-installed again for them to be used + echo "echo secret_placeholder | gnome-keyring-daemon --unlock; pip install pytest .; pytest" > linux_test.sh + chmod +x linux_test.sh + sudo dbus-run-session -- ./linux_test.sh + - name: Test on other platforms without encryption + if: ${{ matrix.os != 'ubuntu-latest' }} + env: + TOXENV: ${{ matrix.toxenv }} + run: | + tox + + cd: + needs: ci + if: github.event_name == 'push' && (startsWith(github.ref, 'refs/tags') || github.ref == 'refs/heads/master') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.9 + uses: actions/setup-python@v2 + with: + python-version: 3.9 + - name: Build a package for release + run: | + python -m pip install build --user + python -m build --sdist --wheel --outdir dist/ . + - name: Publish to TestPyPI + uses: pypa/gh-action-pypi-publish@v1.4.2 + if: github.ref == 'refs/heads/master' + with: + user: __token__ + password: ${{ secrets.TEST_PYPI_API_TOKEN }} + repository_url: https://test.pypi.org/legacy/ + - name: Publish to PyPI + if: startsWith(github.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@v1.4.2 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} + diff --git a/.gitignore b/.gitignore index b9e6255..1b806a6 100644 --- a/.gitignore +++ b/.gitignore @@ -332,3 +332,5 @@ ASALocalRun/ # MFractors (Xamarin productivity tool) working folder .mfractor/ + +.eggs/ diff --git a/.pylintrc b/.pylintrc index bd618db..101e3ed 100644 --- a/.pylintrc +++ b/.pylintrc @@ -2,5 +2,7 @@ good-names= logger disable= + super-with-arguments, # For Python 2.x + raise-missing-from, # For Python 2.x trailing-newlines, useless-object-inheritance diff --git a/.travis.yml b/.travis.yml index 31a5e21..7fb4663 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,18 +9,26 @@ matrix: before_install: - sudo apt update - sudo apt install python-dev libgirepository1.0-dev libcairo2-dev gir1.2-secret-1 + - pip install --upgrade pip - python: "3.5" env: TOXENV=py35 os: linux before_install: - sudo apt update - sudo apt install python3-dev libgirepository1.0-dev libcairo2-dev gir1.2-secret-1 - - python: "3.6" - env: TOXENV=py36 - os: linux - before_install: - - sudo apt update - - sudo apt install python3-dev libgirepository1.0-dev libcairo2-dev gir1.2-secret-1 + - pip install --upgrade pip + + ## Somehow cryptography is not able to be compiled and installed on Python 3.6 + #- python: "3.6" + # env: + # - TOXENV=py36 + # - CRYPTOGRAPHY_DONT_BUILD_RUST=1 + # os: linux + # before_install: + # - sudo apt update + # - sudo apt install python3-dev libgirepository1.0-dev libcairo2-dev gir1.2-secret-1 + # - pip install --upgrade pip + - python: "3.7" env: TOXENV=py37 os: linux @@ -28,6 +36,7 @@ matrix: before_install: - sudo apt update - sudo apt install python3-dev libgirepository1.0-dev libcairo2-dev gir1.2-secret-1 + - pip install --upgrade pip - python: "3.8" env: TOXENV=py38 os: linux @@ -35,6 +44,7 @@ matrix: before_install: - sudo apt update - sudo apt install python3-dev libgirepository1.0-dev libcairo2-dev gir1.2-secret-1 + - pip install --upgrade pip - name: "Python 3.7 on macOS" env: TOXENV=py37 os: osx @@ -43,17 +53,23 @@ matrix: - name: "Python 2.7 on Windows" env: TOXENV=py27 PATH=/c/Python27:/c/Python27/Scripts:$PATH os: windows - before_install: choco install python2 + before_install: + - choco install python2 + - pip install --upgrade --user pip language: shell - name: "Python 3.5 on Windows" env: TOXENV=py35 PATH=/c/Python35:/c/Python35/Scripts:$PATH os: windows - before_install: choco install python3 --version 3.5.4 + before_install: + - choco install python3 --version 3.5.4 + - pip install --upgrade --user pip language: shell - name: "Python 3.7 on Windows" env: TOXENV=py37 PATH=/c/Python37:/c/Python37/Scripts:$PATH os: windows - before_install: choco install python3 --version 3.7.3 + before_install: + - choco install python3 --version 3.7.3 + - pip install --upgrade --user pip language: shell install: diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a56a2ea --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +# TODO: Can this Dockerfile use multi-stage build? +# Final size 690MB. (It would be 1.16 GB if started with python:3 as base) +FROM python:3-slim + +# Install Generic PyGObject (sans GTK) +#The following somehow won't work: +#RUN apt-get update && apt-get install -y python3-gi python3-gi-cairo +RUN apt-get update && apt-get install -y \ + libcairo2-dev \ + libgirepository1.0-dev \ + python3-dev +RUN pip install "pygobject>=3,<4" + +# Install MSAL Extensions dependencies +# Don't know how to get container talk to dbus on host, +# so we choose to create a self-contained image by installing gnome-keyring +RUN apt-get install -y \ + gir1.2-secret-1 \ + gnome-keyring + +# Not strictly necessary, but we include a pytest (which is only 3MB) to facilitate testing. +RUN pip install "pytest>=6,<7" + +# Install MSAL Extensions. Upgrade the pinned version number to trigger a new image build. +RUN pip install "msal-extensions==0.3" + +# This setup is inspired from https://github.com/jaraco/keyring#using-keyring-on-headless-linux-systems-in-a-docker-container +ENTRYPOINT ["dbus-run-session", "--"] +# Note: gnome-keyring-daemon needs previleged mode, therefore can not be run by a RUN command. +CMD ["sh", "-c", "echo default_secret | gnome-keyring-daemon --unlock; bash"] diff --git a/docker_run.sh b/docker_run.sh new file mode 100755 index 0000000..9836192 --- /dev/null +++ b/docker_run.sh @@ -0,0 +1,15 @@ +#!/usr/bin/bash +IMAGE_NAME=msal-extensions:latest + +docker build -t $IMAGE_NAME - < Dockerfile + +echo "==== Integration Test for Persistence on Linux (libsecret) ====" +echo "After seeing the bash prompt, run the following to test encryption on Linux:" +echo " pip install -e ." +echo " pytest" +docker run --rm -it \ + --privileged \ + -w /home -v $PWD:/home \ + $IMAGE_NAME \ + $1 + diff --git a/msal_extensions/__init__.py b/msal_extensions/__init__.py index bfa02bd..14a8c6d 100644 --- a/msal_extensions/__init__.py +++ b/msal_extensions/__init__.py @@ -1,5 +1,5 @@ """Provides auxiliary functionality to the `msal` package.""" -__version__ = "0.3.0" +__version__ = "0.3.1" import sys diff --git a/msal_extensions/cache_lock.py b/msal_extensions/cache_lock.py index c4f8f3d..ebb2601 100644 --- a/msal_extensions/cache_lock.py +++ b/msal_extensions/cache_lock.py @@ -2,9 +2,15 @@ import os import sys import errno -import portalocker +import time +import logging from distutils.version import LooseVersion +import portalocker + + +logger = logging.getLogger(__name__) + class CrossPlatLock(object): """Offers a mechanism for waiting until another process is finished interacting with a shared @@ -14,7 +20,8 @@ class CrossPlatLock(object): def __init__(self, lockfile_path): self._lockpath = lockfile_path # Support for passing through arguments to the open syscall was added in v1.4.0 - open_kwargs = {'buffering': 0} if LooseVersion(portalocker.__version__) >= LooseVersion("1.4.0") else {} + open_kwargs = ({'buffering': 0} + if LooseVersion(portalocker.__version__) >= LooseVersion("1.4.0") else {}) self._lock = portalocker.Lock( lockfile_path, mode='wb+', @@ -25,9 +32,32 @@ def __init__(self, lockfile_path): flags=portalocker.LOCK_EX | portalocker.LOCK_NB, **open_kwargs) + def _try_to_create_lock_file(self): + timeout = 5 + check_interval = 0.25 + current_time = getattr(time, "monotonic", time.time) + timeout_end = current_time() + timeout + pid = os.getpid() + while timeout_end > current_time(): + try: + with open(self._lockpath, 'x'): # pylint: disable=unspecified-encoding + return True + except ValueError: # This needs to be the first clause, for Python 2 to hit it + logger.warning("Python 2 does not support atomic creation of file") + return False + except FileExistsError: # Only Python 3 will reach this clause + logger.debug( + "Process %d found existing lock file, will retry after %f second", + pid, check_interval) + time.sleep(check_interval) + return False + def __enter__(self): + pid = os.getpid() + if not self._try_to_create_lock_file(): + logger.warning("Process %d failed to create lock file", pid) file_handle = self._lock.__enter__() - file_handle.write('{} {}'.format(os.getpid(), sys.argv[0]).encode('utf-8')) + file_handle.write('{} {}'.format(pid, sys.argv[0]).encode('utf-8')) # pylint: disable=consider-using-f-string return file_handle def __exit__(self, *args): @@ -38,5 +68,5 @@ def __exit__(self, *args): # file for itself. os.remove(self._lockpath) except OSError as ex: # pylint: disable=invalid-name - if ex.errno != errno.ENOENT and ex.errno != errno.EACCES: + if ex.errno not in (errno.ENOENT, errno.EACCES): raise diff --git a/msal_extensions/libsecret.py b/msal_extensions/libsecret.py index 9f624e7..a2b8da1 100644 --- a/msal_extensions/libsecret.py +++ b/msal_extensions/libsecret.py @@ -13,33 +13,29 @@ pip install wheel PYGOBJECT_WITHOUT_PYCAIRO=1 pip install --no-build-isolation pygobject """ -import logging - -logger = logging.getLogger(__name__) try: - import gi # https://github.com/AzureAD/microsoft-authentication-extensions-for-python/wiki/Encryption-on-Linux + import gi # https://github.com/AzureAD/microsoft-authentication-extensions-for-python/wiki/Encryption-on-Linux # pylint: disable=line-too-long except ImportError: - logger.exception( - """Runtime dependency of PyGObject is missing. + raise ImportError("""Unable to import module 'gi' +Runtime dependency of PyGObject is missing. Depends on your Linux distro, you could install it system-wide by something like: sudo apt install python3-gi python3-gi-cairo gir1.2-secret-1 If necessary, please refer to PyGObject's doc: https://pygobject.readthedocs.io/en/latest/getting_started.html -""") - raise +""") # Message via exception rather than log try: # pylint: disable=no-name-in-module gi.require_version("Secret", "1") # Would require a package gir1.2-secret-1 # pylint: disable=wrong-import-position from gi.repository import Secret # Would require a package gir1.2-secret-1 -except (ValueError, ImportError): - logger.exception( +except (ValueError, ImportError) as ex: + raise type(ex)( """Require a package "gir1.2-secret-1" which could be installed by: sudo apt install gir1.2-secret-1 - """) - raise + """) # Message via exception rather than log + class LibSecretAgent(object): """A loader/saver built on top of low-level libsecret""" @@ -51,7 +47,7 @@ def __init__( # pylint: disable=too-many-arguments label="", # Helpful when visualizing secrets by other viewers attribute_types=None, # {name: SchemaAttributeType, ...} collection=None, # None means default collection - ): # pylint: disable=bad-continuation + ): """This agent is built on top of lower level libsecret API. Content stored via libsecret is associated with a bunch of attributes. @@ -126,14 +122,13 @@ def trial_run(): agent.save(payload) # It would fail when running inside an SSH session assert agent.load() == payload # This line is probably not reachable agent.clear() - except (gi.repository.GLib.Error, AssertionError): + except (gi.repository.GLib.Error, AssertionError): # pylint: disable=no-member + # https://pygobject.readthedocs.io/en/latest/guide/api/error_handling.html#examples message = """libsecret did not perform properly. * If you encountered error "Remote error from secret service: org.freedesktop.DBus.Error.ServiceUnknown", you may need to install gnome-keyring package. * Headless mode (such as in an ssh session) is not supported. """ - logger.exception(message) # This log contains trace stack for debugging - logger.warning(message) # This is visible by default - raise + raise RuntimeError(message) # Message via exception rather than log diff --git a/msal_extensions/osx.py b/msal_extensions/osx.py index 33f85e9..c2e8448 100644 --- a/msal_extensions/osx.py +++ b/msal_extensions/osx.py @@ -5,7 +5,7 @@ import os import ctypes as _ctypes -OS_RESULT = _ctypes.c_int32 +OS_RESULT = _ctypes.c_int32 # pylint: disable=invalid-name class KeychainError(OSError): @@ -21,10 +21,9 @@ def __init__(self, exit_status): self.exit_status = exit_status # TODO: pylint: disable=fixme # use SecCopyErrorMessageString to fetch the appropriate message here. - self.message = \ - '{} ' \ - 'see https://opensource.apple.com/source/CarbonHeaders/CarbonHeaders-18.1/MacErrors.h'\ - .format(self.exit_status) + self.message = ( + '{} see https://opensource.apple.com/source/CarbonHeaders/CarbonHeaders-18.1/MacErrors.h' # pylint: disable=consider-using-f-string,line-too-long + .format(self.exit_status)) def _get_native_location(name): # type: (str) -> str @@ -33,7 +32,7 @@ def _get_native_location(name): :param name: The name of the library to be loaded. :return: The location of the library on a MacOS filesystem. """ - return '/System/Library/Frameworks/{0}.framework/{0}'.format(name) + return '/System/Library/Frameworks/{0}.framework/{0}'.format(name) # pylint: disable=consider-using-f-string # Load native MacOS libraries diff --git a/msal_extensions/persistence.py b/msal_extensions/persistence.py index 1ad5c8e..e4b6c66 100644 --- a/msal_extensions/persistence.py +++ b/msal_extensions/persistence.py @@ -10,9 +10,10 @@ import os import errno import logging +import sys try: from pathlib import Path # Built-in in Python 3 -except: +except ImportError: from pathlib2 import Path # An extra lib for Python 2 @@ -28,14 +29,19 @@ def _mkdir_p(path): """Creates a directory, and any necessary parents. - This implementation based on a Stack Overflow question that can be found here: - https://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python - If the path provided is an existing file, this function raises an exception. :param path: The directory name that should be created. """ if not path: return # NO-OP + + if sys.version_info >= (3, 2): + os.makedirs(path, exist_ok=True) + return + + # This fallback implementation is based on a Stack Overflow question: + # https://stackoverflow.com/questions/600268/mkdir-p-functionality-in-python + # Known issue: it won't work when the path is a root folder like "C:\\" try: os.makedirs(path) except OSError as exp: @@ -53,10 +59,12 @@ class PersistenceNotFound(IOError): # Use IOError rather than OSError as base, # https://github.com/AzureAD/microsoft-authentication-extensions-for-python/blob/0.2.2/msal_extensions/token_cache.py#L38 # Now we want to maintain backward compatibility even when using Python 2.x # It makes no difference in Python 3.3+ where IOError is an alias of OSError. - def __init__( - self, - err_no=errno.ENOENT, message="Persistence not found", location=None): - super(PersistenceNotFound, self).__init__(err_no, message, location) + """This happens when attempting BasePersistence.load() on a non-existent persistence instance""" + def __init__(self, err_no=None, message=None, location=None): + super(PersistenceNotFound, self).__init__( + err_no or errno.ENOENT, + message or "Persistence not found", + location) class BasePersistence(ABC): @@ -105,14 +113,14 @@ def __init__(self, location): def save(self, content): # type: (str) -> None """Save the content into this persistence""" - with open(self._location, 'w+') as handle: + with open(self._location, 'w+') as handle: # pylint: disable=unspecified-encoding handle.write(content) def load(self): # type: () -> str """Load content from this persistence""" try: - with open(self._location, 'r') as handle: + with open(self._location, 'r') as handle: # pylint: disable=unspecified-encoding return handle.read() except EnvironmentError as exp: # EnvironmentError in Py 2.7 works across platform if exp.errno == errno.ENOENT: @@ -214,12 +222,12 @@ def load(self): try: return locker.get_generic_password( self._service_name, self._account_name) - except self._KeychainError as ex: + except self._KeychainError as ex: # pylint: disable=invalid-name if ex.exit_status == self._KeychainError.ITEM_NOT_FOUND: # This happens when a load() is called before a save(). # We map it into cross-platform error for unified catching. raise PersistenceNotFound( - location="Service:{} Account:{}".format( + location="Service:{} Account:{}".format( # pylint: disable=consider-using-f-string self._service_name, self._account_name), message=( "Keychain persistence not initialized. " diff --git a/msal_extensions/token_cache.py b/msal_extensions/token_cache.py index e3bbea3..f5edcdd 100644 --- a/msal_extensions/token_cache.py +++ b/msal_extensions/token_cache.py @@ -15,7 +15,30 @@ logger = logging.getLogger(__name__) class PersistedTokenCache(msal.SerializableTokenCache): - """A token cache using given persistence layer, coordinated by a file lock.""" + """A token cache backed by a persistence layer, coordinated by a file lock, + to sustain a certain level of multi-process concurrency for a desktop app. + + The scenario is that multiple instances of same desktop app + (or even multiple different apps) + create their own ``PersistedTokenCache`` instances, + which are all backed by the same token cache file on disk + (known as a persistence). The goal is to have Single Sign On (SSO). + + Each instance of ``PersistedTokenCache`` holds a snapshot of the token cache + in memory. + Each :func:`~find` call will + automatically reload token cache from the persistence when necessary, + so that it will have fresh data. + Each :func:`~modify` call will + automatically reload token cache from the persistence when necessary, + so that new writes will be appended on top of latest token cache data, + and then the new data will be immediately flushed back to the persistence. + + Note: :func:`~deserialize` and :func:`~serialize` remain the same + as their counterparts in the parent class ``msal.SerializableTokenCache``. + In other words, they do not have the "reload from persistence if necessary" + nor the "flush back to persistence" behavior. + """ def __init__(self, persistence, lock_location=None): super(PersistedTokenCache, self).__init__() @@ -50,9 +73,21 @@ def modify(self, credential_type, old_entry, new_key_value_pairs=None): self._last_sync = time.time() def find(self, credential_type, **kwargs): # pylint: disable=arguments-differ - with CrossPlatLock(self._lock_location): - self._reload_if_necessary() - return super(PersistedTokenCache, self).find(credential_type, **kwargs) + # Use optimistic locking rather than CrossPlatLock(self._lock_location) + retry = 3 + for attempt in range(1, retry + 1): + try: + self._reload_if_necessary() + except Exception: # pylint: disable=broad-except + # Presumably other processes are writing the file, causing dirty read + if attempt < retry: + logger.debug("Unable to load token cache file in No. %d attempt", attempt) + time.sleep(0.5) + else: + raise # End of retry. Re-raise the exception as-is. + else: # If reload encountered no error, the data is considered intact + return super(PersistedTokenCache, self).find(credential_type, **kwargs) + return [] # Not really reachable here. Just to keep pylint happy. class FileTokenCache(PersistedTokenCache): diff --git a/msal_extensions/windows.py b/msal_extensions/windows.py index 479c496..f1e8b59 100644 --- a/msal_extensions/windows.py +++ b/msal_extensions/windows.py @@ -5,6 +5,9 @@ _LOCAL_FREE = ctypes.windll.kernel32.LocalFree _GET_LAST_ERROR = ctypes.windll.kernel32.GetLastError _MEMCPY = ctypes.cdll.msvcrt.memcpy +_MEMCPY.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t] # Note: + # Suggested by https://github.com/AzureAD/microsoft-authentication-extensions-for-python/issues/85 # pylint: disable=line-too-long + # Matching https://docs.microsoft.com/en-us/cpp/c-runtime-library/reference/memcpy-wmemcpy?view=msvc-160 # pylint: disable=line-too-long _CRYPT_PROTECT_DATA = ctypes.windll.crypt32.CryptProtectData _CRYPT_UNPROTECT_DATA = ctypes.windll.crypt32.CryptUnprotectData _CRYPTPROTECT_UI_FORBIDDEN = 0x01 @@ -67,7 +70,7 @@ def protect(self, message): if _CRYPT_PROTECT_DATA( ctypes.byref(message_blob), - u"python_data", + u"python_data", # pylint: disable=redundant-u-string-prefix entropy, None, None, diff --git a/setup.cfg b/setup.cfg index 3c6e79c..80050d9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,11 @@ +# https://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files + [bdist_wheel] universal=1 + +[metadata] +license = MIT +project_urls = Changelog = https://github.com/AzureAD/microsoft-authentication-extensions-for-python/releases +classifiers = + License :: OSI Approved :: MIT License + Development Status :: 4 - Beta diff --git a/setup.py b/setup.py index 1af3713..1500a37 100644 --- a/setup.py +++ b/setup.py @@ -19,14 +19,20 @@ packages=find_packages(), long_description=long_description, long_description_content_type="text/markdown", - classifiers=[ - 'Development Status :: 2 - Pre-Alpha', - ], package_data={'': ['LICENSE']}, install_requires=[ 'msal>=0.4.1,<2.0.0', - "portalocker~=1.6;platform_system=='Windows'", - "portalocker~=1.0;platform_system!='Windows'", + + # In order to implement these requirements: + # Lowerbound = (1.6 if playform_system == 'Windows' else 1.0) + # Upperbound < (3 if python_version >= '3.5' else 2) + # The following 4 lines use the `and` syntax defined here: + # https://www.python.org/dev/peps/pep-0508/#grammar + "portalocker<3,>=1.0;python_version>='3.5' and platform_system!='Windows'", + "portalocker<2,>=1.0;python_version=='2.7' and platform_system!='Windows'", + "portalocker<3,>=1.6;python_version>='3.5' and platform_system=='Windows'", + "portalocker<2,>=1.6;python_version=='2.7' and platform_system=='Windows'", + "pathlib2;python_version<'3.0'", ## We choose to NOT define a hard dependency on this. # "pygobject>=3,<4;platform_system=='Linux'", diff --git a/tests/test_cache_lock_file_perf.py b/tests/test_cache_lock_file_perf.py index 757fb80..8bc1a1c 100644 --- a/tests/test_cache_lock_file_perf.py +++ b/tests/test_cache_lock_file_perf.py @@ -67,7 +67,7 @@ def test_lock_for_high_workload(temp_location): def test_lock_for_timeout(temp_location): - num_of_processes = 10 + num_of_processes = 30 sleep_interval = 1 _run_multiple_processes(num_of_processes, temp_location, sleep_interval) count = _validate_result_in_cache(temp_location)