From cc109bcc0b70a7915298368a10208da51f76b00a Mon Sep 17 00:00:00 2001 From: Clinton Hall Date: Wed, 28 Feb 2024 15:47:04 +1300 Subject: [PATCH] Add Python 3.12 and fix Radarr handling (#1989) * Added Python3.12 and future 3.13 * Fix Radarr result handling * remove py2.7 and py3.7 support --- .bumpversion.cfg | 2 +- azure-pipelines.yml | 8 +- core/__init__.py | 2 +- core/auto_process/movies.py | 5 +- eol.py | 4 +- libs/common/__init__.py | 1 + libs/common/importlib_resources/__init__.py | 17 + libs/common/importlib_resources/_adapters.py | 168 + libs/common/importlib_resources/_common.py | 209 + libs/common/importlib_resources/_itertools.py | 38 + libs/common/importlib_resources/abc.py | 171 + .../importlib_resources/compat/__init__.py | 0 .../common/importlib_resources/compat/py38.py | 11 + .../common/importlib_resources/compat/py39.py | 10 + .../importlib_resources/future/__init__.py | 0 .../importlib_resources/future/adapters.py | 46 + libs/common/importlib_resources/py.typed | 0 libs/common/importlib_resources/readers.py | 194 + libs/common/importlib_resources/simple.py | 106 + .../importlib_resources/tests/__init__.py | 0 .../importlib_resources/tests/_compat.py | 32 + .../common/importlib_resources/tests/_path.py | 56 + .../tests/data01/__init__.py | 0 .../tests/data01/binary.file | Bin 0 -> 4 bytes .../tests/data01/subdirectory/__init__.py | 0 .../tests/data01/subdirectory/binary.file | 1 + .../tests/data01/utf-16.file | Bin 0 -> 44 bytes .../tests/data01/utf-8.file | 1 + .../tests/data02/__init__.py | 0 .../tests/data02/one/__init__.py | 0 .../tests/data02/one/resource1.txt | 1 + .../subdirectory/subsubdir/resource.txt | 1 + .../tests/data02/two/__init__.py | 0 .../tests/data02/two/resource2.txt | 1 + .../tests/namespacedata01/binary.file | Bin 0 -> 4 bytes .../namespacedata01/subdirectory/binary.file | 1 + .../tests/namespacedata01/utf-16.file | Bin 0 -> 44 bytes .../tests/namespacedata01/utf-8.file | 1 + .../tests/test_compatibilty_files.py | 104 + .../tests/test_contents.py | 43 + .../importlib_resources/tests/test_custom.py | 47 + .../importlib_resources/tests/test_files.py | 111 + .../importlib_resources/tests/test_open.py | 89 + .../importlib_resources/tests/test_path.py | 65 + .../importlib_resources/tests/test_read.py | 97 + .../importlib_resources/tests/test_reader.py | 145 + .../tests/test_resource.py | 241 + libs/common/importlib_resources/tests/util.py | 165 + libs/common/importlib_resources/tests/zip.py | 32 + libs/common/jaraco/context.py | 288 + libs/common/jaraco/functools/__init__.py | 633 ++ libs/common/jaraco/functools/__init__.pyi | 128 + libs/common/jaraco/functools/py.typed | 0 libs/common/jaraco/text/Lorem ipsum.txt | 2 + libs/common/jaraco/text/__init__.py | 624 ++ libs/common/jaraco/text/layouts.py | 25 + libs/common/jaraco/text/show-newlines.py | 33 + libs/common/jaraco/text/strip-prefix.py | 21 + libs/common/jaraco/text/to-dvorak.py | 6 + libs/common/jaraco/text/to-qwerty.py | 6 + .../__about__.py => packaging/__init__.py} | 12 +- libs/common/packaging/_elffile.py | 108 + libs/common/packaging/_manylinux.py | 252 + libs/common/packaging/_musllinux.py | 83 + libs/common/packaging/_parser.py | 359 ++ libs/common/packaging/_structures.py | 61 + libs/common/packaging/_tokenizer.py | 192 + libs/common/packaging/markers.py | 252 + libs/common/packaging/metadata.py | 822 +++ libs/common/packaging/py.typed | 0 libs/common/packaging/requirements.py | 90 + libs/common/packaging/specifiers.py | 1008 +++ libs/common/packaging/tags.py | 553 ++ libs/common/packaging/utils.py | 172 + libs/common/packaging/version.py | 563 ++ libs/common/pkg_resources/__init__.py | 969 +-- libs/common/pkg_resources/_vendor/appdirs.py | 608 -- .../_vendor/packaging/__init__.py | 19 +- .../_vendor/packaging/_compat.py | 30 - .../_vendor/packaging/_structures.py | 51 +- .../_vendor/packaging/markers.py | 277 +- .../_vendor/packaging/requirements.py | 138 +- .../_vendor/packaging/specifiers.py | 1040 +-- .../pkg_resources/_vendor/packaging/utils.py | 133 +- .../_vendor/packaging/version.py | 537 +- .../common/pkg_resources/_vendor/pyparsing.py | 5742 ----------------- libs/common/pkg_resources/_vendor/six.py | 868 --- libs/common/pkg_resources/extern/__init__.py | 43 +- libs/common/pkg_resources/py31compat.py | 23 - libs/common/platformdirs/__init__.py | 625 ++ libs/common/platformdirs/__main__.py | 55 + libs/common/platformdirs/android.py | 221 + libs/common/platformdirs/api.py | 279 + libs/common/platformdirs/macos.py | 127 + libs/common/platformdirs/py.typed | 0 libs/common/platformdirs/unix.py | 270 + libs/common/platformdirs/version.py | 16 + libs/common/platformdirs/windows.py | 267 + libs/common/setuptools/__init__.py | 269 +- .../common/setuptools/_deprecation_warning.py | 7 - libs/common/setuptools/_imp.py | 35 +- .../setuptools/_vendor/packaging/__about__.py | 27 - .../setuptools/_vendor/packaging/__init__.py | 31 +- .../setuptools/_vendor/packaging/_compat.py | 31 - .../_vendor/packaging/_structures.py | 47 +- .../setuptools/_vendor/packaging/markers.py | 246 +- .../_vendor/packaging/requirements.py | 135 +- .../_vendor/packaging/specifiers.py | 999 +-- .../setuptools/_vendor/packaging/tags.py | 610 +- .../setuptools/_vendor/packaging/utils.py | 130 +- .../setuptools/_vendor/packaging/version.py | 492 +- libs/common/setuptools/_vendor/pyparsing.py | 5742 ----------------- libs/common/setuptools/_vendor/six.py | 868 --- libs/common/setuptools/archive_util.py | 157 +- libs/common/setuptools/build_meta.py | 430 +- libs/common/setuptools/cli-32.exe | Bin 65536 -> 11776 bytes libs/common/setuptools/cli-64.exe | Bin 74752 -> 14336 bytes libs/common/setuptools/cli.exe | Bin 65536 -> 11776 bytes libs/common/setuptools/command/__init__.py | 17 +- libs/common/setuptools/command/alias.py | 4 +- libs/common/setuptools/command/bdist_egg.py | 207 +- libs/common/setuptools/command/bdist_rpm.py | 34 +- .../setuptools/command/bdist_wininst.py | 21 - libs/common/setuptools/command/build_clib.py | 68 +- libs/common/setuptools/command/build_ext.py | 281 +- libs/common/setuptools/command/build_py.py | 267 +- libs/common/setuptools/command/develop.py | 81 +- libs/common/setuptools/command/dist_info.py | 96 +- .../common/setuptools/command/easy_install.py | 1073 +-- libs/common/setuptools/command/egg_info.py | 400 +- libs/common/setuptools/command/install.py | 67 +- .../setuptools/command/install_egg_info.py | 15 +- libs/common/setuptools/command/install_lib.py | 12 +- .../setuptools/command/install_scripts.py | 21 +- libs/common/setuptools/command/py36compat.py | 136 - libs/common/setuptools/command/rotate.py | 16 +- libs/common/setuptools/command/saveopts.py | 1 - libs/common/setuptools/command/sdist.py | 162 +- libs/common/setuptools/command/setopt.py | 49 +- libs/common/setuptools/command/test.py | 91 +- libs/common/setuptools/command/upload_docs.py | 79 +- libs/common/setuptools/config.py | 659 -- libs/common/setuptools/dep_util.py | 35 +- libs/common/setuptools/depends.py | 48 +- libs/common/setuptools/dist.py | 1090 ++-- libs/common/setuptools/errors.py | 54 +- libs/common/setuptools/extension.py | 99 +- libs/common/setuptools/extern/__init__.py | 47 +- libs/common/setuptools/glob.py | 27 +- libs/common/setuptools/gui-32.exe | Bin 65536 -> 11776 bytes libs/common/setuptools/gui-64.exe | Bin 75264 -> 14336 bytes libs/common/setuptools/gui.exe | Bin 65536 -> 11776 bytes libs/common/setuptools/installer.py | 156 +- libs/common/setuptools/launch.py | 3 +- libs/common/setuptools/lib2to3_ex.py | 62 - libs/common/setuptools/monkey.py | 86 +- libs/common/setuptools/msvc.py | 571 +- libs/common/setuptools/namespaces.py | 39 +- libs/common/setuptools/package_index.py | 416 +- libs/common/setuptools/py27compat.py | 60 - libs/common/setuptools/py31compat.py | 32 - libs/common/setuptools/py33compat.py | 59 - libs/common/setuptools/py34compat.py | 13 - libs/common/setuptools/sandbox.py | 127 +- libs/common/setuptools/site-patch.py | 74 - libs/common/setuptools/ssl_support.py | 260 - libs/common/setuptools/unicode_utils.py | 10 +- libs/common/setuptools/version.py | 6 +- libs/common/setuptools/wheel.py | 126 +- libs/common/setuptools/windows_support.py | 5 +- libs/common/six.py | 7 +- setup.py | 9 +- tox.ini | 8 +- 173 files changed, 17498 insertions(+), 21001 deletions(-) create mode 100644 libs/common/__init__.py create mode 100644 libs/common/importlib_resources/__init__.py create mode 100644 libs/common/importlib_resources/_adapters.py create mode 100644 libs/common/importlib_resources/_common.py create mode 100644 libs/common/importlib_resources/_itertools.py create mode 100644 libs/common/importlib_resources/abc.py create mode 100644 libs/common/importlib_resources/compat/__init__.py create mode 100644 libs/common/importlib_resources/compat/py38.py create mode 100644 libs/common/importlib_resources/compat/py39.py create mode 100644 libs/common/importlib_resources/future/__init__.py create mode 100644 libs/common/importlib_resources/future/adapters.py create mode 100644 libs/common/importlib_resources/py.typed create mode 100644 libs/common/importlib_resources/readers.py create mode 100644 libs/common/importlib_resources/simple.py create mode 100644 libs/common/importlib_resources/tests/__init__.py create mode 100644 libs/common/importlib_resources/tests/_compat.py create mode 100644 libs/common/importlib_resources/tests/_path.py create mode 100644 libs/common/importlib_resources/tests/data01/__init__.py create mode 100644 libs/common/importlib_resources/tests/data01/binary.file create mode 100644 libs/common/importlib_resources/tests/data01/subdirectory/__init__.py create mode 100644 libs/common/importlib_resources/tests/data01/subdirectory/binary.file create mode 100644 libs/common/importlib_resources/tests/data01/utf-16.file create mode 100644 libs/common/importlib_resources/tests/data01/utf-8.file create mode 100644 libs/common/importlib_resources/tests/data02/__init__.py create mode 100644 libs/common/importlib_resources/tests/data02/one/__init__.py create mode 100644 libs/common/importlib_resources/tests/data02/one/resource1.txt create mode 100644 libs/common/importlib_resources/tests/data02/subdirectory/subsubdir/resource.txt create mode 100644 libs/common/importlib_resources/tests/data02/two/__init__.py create mode 100644 libs/common/importlib_resources/tests/data02/two/resource2.txt create mode 100644 libs/common/importlib_resources/tests/namespacedata01/binary.file create mode 100644 libs/common/importlib_resources/tests/namespacedata01/subdirectory/binary.file create mode 100644 libs/common/importlib_resources/tests/namespacedata01/utf-16.file create mode 100644 libs/common/importlib_resources/tests/namespacedata01/utf-8.file create mode 100644 libs/common/importlib_resources/tests/test_compatibilty_files.py create mode 100644 libs/common/importlib_resources/tests/test_contents.py create mode 100644 libs/common/importlib_resources/tests/test_custom.py create mode 100644 libs/common/importlib_resources/tests/test_files.py create mode 100644 libs/common/importlib_resources/tests/test_open.py create mode 100644 libs/common/importlib_resources/tests/test_path.py create mode 100644 libs/common/importlib_resources/tests/test_read.py create mode 100644 libs/common/importlib_resources/tests/test_reader.py create mode 100644 libs/common/importlib_resources/tests/test_resource.py create mode 100644 libs/common/importlib_resources/tests/util.py create mode 100644 libs/common/importlib_resources/tests/zip.py create mode 100644 libs/common/jaraco/context.py create mode 100644 libs/common/jaraco/functools/__init__.py create mode 100644 libs/common/jaraco/functools/__init__.pyi create mode 100644 libs/common/jaraco/functools/py.typed create mode 100644 libs/common/jaraco/text/Lorem ipsum.txt create mode 100644 libs/common/jaraco/text/__init__.py create mode 100644 libs/common/jaraco/text/layouts.py create mode 100644 libs/common/jaraco/text/show-newlines.py create mode 100644 libs/common/jaraco/text/strip-prefix.py create mode 100644 libs/common/jaraco/text/to-dvorak.py create mode 100644 libs/common/jaraco/text/to-qwerty.py rename libs/common/{pkg_resources/_vendor/packaging/__about__.py => packaging/__init__.py} (54%) create mode 100644 libs/common/packaging/_elffile.py create mode 100644 libs/common/packaging/_manylinux.py create mode 100644 libs/common/packaging/_musllinux.py create mode 100644 libs/common/packaging/_parser.py create mode 100644 libs/common/packaging/_structures.py create mode 100644 libs/common/packaging/_tokenizer.py create mode 100644 libs/common/packaging/markers.py create mode 100644 libs/common/packaging/metadata.py create mode 100644 libs/common/packaging/py.typed create mode 100644 libs/common/packaging/requirements.py create mode 100644 libs/common/packaging/specifiers.py create mode 100644 libs/common/packaging/tags.py create mode 100644 libs/common/packaging/utils.py create mode 100644 libs/common/packaging/version.py delete mode 100644 libs/common/pkg_resources/_vendor/appdirs.py delete mode 100644 libs/common/pkg_resources/_vendor/packaging/_compat.py delete mode 100644 libs/common/pkg_resources/_vendor/pyparsing.py delete mode 100644 libs/common/pkg_resources/_vendor/six.py delete mode 100644 libs/common/pkg_resources/py31compat.py create mode 100644 libs/common/platformdirs/__init__.py create mode 100644 libs/common/platformdirs/__main__.py create mode 100644 libs/common/platformdirs/android.py create mode 100644 libs/common/platformdirs/api.py create mode 100644 libs/common/platformdirs/macos.py create mode 100644 libs/common/platformdirs/py.typed create mode 100644 libs/common/platformdirs/unix.py create mode 100644 libs/common/platformdirs/version.py create mode 100644 libs/common/platformdirs/windows.py delete mode 100644 libs/common/setuptools/_deprecation_warning.py delete mode 100644 libs/common/setuptools/_vendor/packaging/__about__.py delete mode 100644 libs/common/setuptools/_vendor/packaging/_compat.py delete mode 100644 libs/common/setuptools/_vendor/pyparsing.py delete mode 100644 libs/common/setuptools/_vendor/six.py delete mode 100644 libs/common/setuptools/command/bdist_wininst.py delete mode 100644 libs/common/setuptools/command/py36compat.py delete mode 100644 libs/common/setuptools/config.py delete mode 100644 libs/common/setuptools/lib2to3_ex.py delete mode 100644 libs/common/setuptools/py27compat.py delete mode 100644 libs/common/setuptools/py31compat.py delete mode 100644 libs/common/setuptools/py33compat.py delete mode 100644 libs/common/setuptools/py34compat.py delete mode 100644 libs/common/setuptools/site-patch.py delete mode 100644 libs/common/setuptools/ssl_support.py diff --git a/.bumpversion.cfg b/.bumpversion.cfg index e7da34cf..8b429ab1 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 12.1.12 +current_version = 12.1.13 commit = True tag = False diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 06ceef4f..c7a04b35 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -13,8 +13,6 @@ jobs: vmImage: 'Ubuntu-latest' strategy: matrix: - Python37: - python.version: '3.7' Python38: python.version: '3.8' Python39: @@ -23,6 +21,8 @@ jobs: python.version: '3.10' Python311: python.version: '3.11' + Python312: + python.version: '3.12' maxParallel: 3 steps: @@ -68,5 +68,7 @@ jobs: versionSpec: '3.x' architecture: 'x64' - - script: python setup.py sdist + - script: | + python -m pip install setuptools + python setup.py sdist displayName: 'Build sdist' diff --git a/core/__init__.py b/core/__init__.py index 04faf3ef..db6bc0c4 100644 --- a/core/__init__.py +++ b/core/__init__.py @@ -83,7 +83,7 @@ from core.utils import ( wake_up, ) -__version__ = '12.1.12' +__version__ = '12.1.13' # Client Agents NZB_CLIENTS = ['sabnzbd', 'nzbget', 'manual'] diff --git a/core/auto_process/movies.py b/core/auto_process/movies.py index 87c50e16..a539405d 100644 --- a/core/auto_process/movies.py +++ b/core/auto_process/movies.py @@ -260,7 +260,10 @@ def process(section, dir_name, input_name=None, status=0, client_agent='manual', ) elif section == 'Radarr': try: - scan_id = int(result['id']) + if isinstance(result, list): + scan_id = int(result[0]['id']) + else: + scan_id = int(result['id']) logger.debug('Scan started with id: {0}'.format(scan_id), section) except Exception as e: logger.warning('No scan id was returned due to: {0}'.format(e), section) diff --git a/eol.py b/eol.py index de9b9502..b31abfd4 100644 --- a/eol.py +++ b/eol.py @@ -28,6 +28,8 @@ def date(string, fmt='%Y-%m-%d'): # https://devguide.python.org/ # https://devguide.python.org/devcycle/#devcycle PYTHON_EOL = { + (3, 13): date('2029-10-1'), + (3, 12): date('2028-10-1'), (3, 11): date('2027-10-1'), (3, 10): date('2026-10-01'), (3, 9): date('2025-10-05'), @@ -99,7 +101,7 @@ def check(version=None, grace_period=0): :return: None """ try: - warn_for_status(version, grace_period) + raise_for_status(version, grace_period) except LifetimeError as error: print('Please use a newer version of Python.') print_statuses() diff --git a/libs/common/__init__.py b/libs/common/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/libs/common/__init__.py @@ -0,0 +1 @@ + diff --git a/libs/common/importlib_resources/__init__.py b/libs/common/importlib_resources/__init__.py new file mode 100644 index 00000000..e6b60c18 --- /dev/null +++ b/libs/common/importlib_resources/__init__.py @@ -0,0 +1,17 @@ +"""Read resources contained within a package.""" + +from ._common import ( + as_file, + files, + Package, +) + +from .abc import ResourceReader + + +__all__ = [ + 'Package', + 'ResourceReader', + 'as_file', + 'files', +] diff --git a/libs/common/importlib_resources/_adapters.py b/libs/common/importlib_resources/_adapters.py new file mode 100644 index 00000000..50688fbb --- /dev/null +++ b/libs/common/importlib_resources/_adapters.py @@ -0,0 +1,168 @@ +from contextlib import suppress +from io import TextIOWrapper + +from . import abc + + +class SpecLoaderAdapter: + """ + Adapt a package spec to adapt the underlying loader. + """ + + def __init__(self, spec, adapter=lambda spec: spec.loader): + self.spec = spec + self.loader = adapter(spec) + + def __getattr__(self, name): + return getattr(self.spec, name) + + +class TraversableResourcesLoader: + """ + Adapt a loader to provide TraversableResources. + """ + + def __init__(self, spec): + self.spec = spec + + def get_resource_reader(self, name): + return CompatibilityFiles(self.spec)._native() + + +def _io_wrapper(file, mode='r', *args, **kwargs): + if mode == 'r': + return TextIOWrapper(file, *args, **kwargs) + elif mode == 'rb': + return file + raise ValueError(f"Invalid mode value '{mode}', only 'r' and 'rb' are supported") + + +class CompatibilityFiles: + """ + Adapter for an existing or non-existent resource reader + to provide a compatibility .files(). + """ + + class SpecPath(abc.Traversable): + """ + Path tied to a module spec. + Can be read and exposes the resource reader children. + """ + + def __init__(self, spec, reader): + self._spec = spec + self._reader = reader + + def iterdir(self): + if not self._reader: + return iter(()) + return iter( + CompatibilityFiles.ChildPath(self._reader, path) + for path in self._reader.contents() + ) + + def is_file(self): + return False + + is_dir = is_file + + def joinpath(self, other): + if not self._reader: + return CompatibilityFiles.OrphanPath(other) + return CompatibilityFiles.ChildPath(self._reader, other) + + @property + def name(self): + return self._spec.name + + def open(self, mode='r', *args, **kwargs): + return _io_wrapper(self._reader.open_resource(None), mode, *args, **kwargs) + + class ChildPath(abc.Traversable): + """ + Path tied to a resource reader child. + Can be read but doesn't expose any meaningful children. + """ + + def __init__(self, reader, name): + self._reader = reader + self._name = name + + def iterdir(self): + return iter(()) + + def is_file(self): + return self._reader.is_resource(self.name) + + def is_dir(self): + return not self.is_file() + + def joinpath(self, other): + return CompatibilityFiles.OrphanPath(self.name, other) + + @property + def name(self): + return self._name + + def open(self, mode='r', *args, **kwargs): + return _io_wrapper( + self._reader.open_resource(self.name), mode, *args, **kwargs + ) + + class OrphanPath(abc.Traversable): + """ + Orphan path, not tied to a module spec or resource reader. + Can't be read and doesn't expose any meaningful children. + """ + + def __init__(self, *path_parts): + if len(path_parts) < 1: + raise ValueError('Need at least one path part to construct a path') + self._path = path_parts + + def iterdir(self): + return iter(()) + + def is_file(self): + return False + + is_dir = is_file + + def joinpath(self, other): + return CompatibilityFiles.OrphanPath(*self._path, other) + + @property + def name(self): + return self._path[-1] + + def open(self, mode='r', *args, **kwargs): + raise FileNotFoundError("Can't open orphan path") + + def __init__(self, spec): + self.spec = spec + + @property + def _reader(self): + with suppress(AttributeError): + return self.spec.loader.get_resource_reader(self.spec.name) + + def _native(self): + """ + Return the native reader if it supports files(). + """ + reader = self._reader + return reader if hasattr(reader, 'files') else self + + def __getattr__(self, attr): + return getattr(self._reader, attr) + + def files(self): + return CompatibilityFiles.SpecPath(self.spec, self._reader) + + +def wrap_spec(package): + """ + Construct a package spec with traversable compatibility + on the spec/loader/reader. + """ + return SpecLoaderAdapter(package.__spec__, TraversableResourcesLoader) diff --git a/libs/common/importlib_resources/_common.py b/libs/common/importlib_resources/_common.py new file mode 100644 index 00000000..c9891359 --- /dev/null +++ b/libs/common/importlib_resources/_common.py @@ -0,0 +1,209 @@ +import os +import pathlib +import tempfile +import functools +import contextlib +import types +import importlib +import inspect +import warnings +import itertools + +from typing import Union, Optional, cast +from .abc import ResourceReader, Traversable + +from .future.adapters import wrap_spec + +Package = Union[types.ModuleType, str] +Anchor = Package + + +def package_to_anchor(func): + """ + Replace 'package' parameter as 'anchor' and warn about the change. + + Other errors should fall through. + + >>> files('a', 'b') + Traceback (most recent call last): + TypeError: files() takes from 0 to 1 positional arguments but 2 were given + + Remove this compatibility in Python 3.14. + """ + undefined = object() + + @functools.wraps(func) + def wrapper(anchor=undefined, package=undefined): + if package is not undefined: + if anchor is not undefined: + return func(anchor, package) + warnings.warn( + "First parameter to files is renamed to 'anchor'", + DeprecationWarning, + stacklevel=2, + ) + return func(package) + elif anchor is undefined: + return func() + return func(anchor) + + return wrapper + + +@package_to_anchor +def files(anchor: Optional[Anchor] = None) -> Traversable: + """ + Get a Traversable resource for an anchor. + """ + return from_package(resolve(anchor)) + + +def get_resource_reader(package: types.ModuleType) -> Optional[ResourceReader]: + """ + Return the package's loader if it's a ResourceReader. + """ + # We can't use + # a issubclass() check here because apparently abc.'s __subclasscheck__() + # hook wants to create a weak reference to the object, but + # zipimport.zipimporter does not support weak references, resulting in a + # TypeError. That seems terrible. + spec = package.__spec__ + reader = getattr(spec.loader, 'get_resource_reader', None) # type: ignore + if reader is None: + return None + return reader(spec.name) # type: ignore + + +@functools.singledispatch +def resolve(cand: Optional[Anchor]) -> types.ModuleType: + return cast(types.ModuleType, cand) + + +@resolve.register +def _(cand: str) -> types.ModuleType: + return importlib.import_module(cand) + + +@resolve.register +def _(cand: None) -> types.ModuleType: + return resolve(_infer_caller().f_globals['__name__']) + + +def _infer_caller(): + """ + Walk the stack and find the frame of the first caller not in this module. + """ + + def is_this_file(frame_info): + return frame_info.filename == __file__ + + def is_wrapper(frame_info): + return frame_info.function == 'wrapper' + + not_this_file = itertools.filterfalse(is_this_file, inspect.stack()) + # also exclude 'wrapper' due to singledispatch in the call stack + callers = itertools.filterfalse(is_wrapper, not_this_file) + return next(callers).frame + + +def from_package(package: types.ModuleType): + """ + Return a Traversable object for the given package. + + """ + spec = wrap_spec(package) + reader = spec.loader.get_resource_reader(spec.name) + return reader.files() + + +@contextlib.contextmanager +def _tempfile( + reader, + suffix='', + # gh-93353: Keep a reference to call os.remove() in late Python + # finalization. + *, + _os_remove=os.remove, +): + # Not using tempfile.NamedTemporaryFile as it leads to deeper 'try' + # blocks due to the need to close the temporary file to work on Windows + # properly. + fd, raw_path = tempfile.mkstemp(suffix=suffix) + try: + try: + os.write(fd, reader()) + finally: + os.close(fd) + del reader + yield pathlib.Path(raw_path) + finally: + try: + _os_remove(raw_path) + except FileNotFoundError: + pass + + +def _temp_file(path): + return _tempfile(path.read_bytes, suffix=path.name) + + +def _is_present_dir(path: Traversable) -> bool: + """ + Some Traversables implement ``is_dir()`` to raise an + exception (i.e. ``FileNotFoundError``) when the + directory doesn't exist. This function wraps that call + to always return a boolean and only return True + if there's a dir and it exists. + """ + with contextlib.suppress(FileNotFoundError): + return path.is_dir() + return False + + +@functools.singledispatch +def as_file(path): + """ + Given a Traversable object, return that object as a + path on the local file system in a context manager. + """ + return _temp_dir(path) if _is_present_dir(path) else _temp_file(path) + + +@as_file.register(pathlib.Path) +@contextlib.contextmanager +def _(path): + """ + Degenerate behavior for pathlib.Path objects. + """ + yield path + + +@contextlib.contextmanager +def _temp_path(dir: tempfile.TemporaryDirectory): + """ + Wrap tempfile.TemporyDirectory to return a pathlib object. + """ + with dir as result: + yield pathlib.Path(result) + + +@contextlib.contextmanager +def _temp_dir(path): + """ + Given a traversable dir, recursively replicate the whole tree + to the file system in a context manager. + """ + assert path.is_dir() + with _temp_path(tempfile.TemporaryDirectory()) as temp_dir: + yield _write_contents(temp_dir, path) + + +def _write_contents(target, source): + child = target.joinpath(source.name) + if source.is_dir(): + child.mkdir() + for item in source.iterdir(): + _write_contents(child, item) + else: + child.write_bytes(source.read_bytes()) + return child diff --git a/libs/common/importlib_resources/_itertools.py b/libs/common/importlib_resources/_itertools.py new file mode 100644 index 00000000..7b775ef5 --- /dev/null +++ b/libs/common/importlib_resources/_itertools.py @@ -0,0 +1,38 @@ +# from more_itertools 9.0 +def only(iterable, default=None, too_long=None): + """If *iterable* has only one item, return it. + If it has zero items, return *default*. + If it has more than one item, raise the exception given by *too_long*, + which is ``ValueError`` by default. + >>> only([], default='missing') + 'missing' + >>> only([1]) + 1 + >>> only([1, 2]) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ValueError: Expected exactly one item in iterable, but got 1, 2, + and perhaps more.' + >>> only([1, 2], too_long=TypeError) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + TypeError + Note that :func:`only` attempts to advance *iterable* twice to ensure there + is only one item. See :func:`spy` or :func:`peekable` to check + iterable contents less destructively. + """ + it = iter(iterable) + first_value = next(it, default) + + try: + second_value = next(it) + except StopIteration: + pass + else: + msg = ( + 'Expected exactly one item in iterable, but got {!r}, {!r}, ' + 'and perhaps more.'.format(first_value, second_value) + ) + raise too_long or ValueError(msg) + + return first_value diff --git a/libs/common/importlib_resources/abc.py b/libs/common/importlib_resources/abc.py new file mode 100644 index 00000000..7a58dd2f --- /dev/null +++ b/libs/common/importlib_resources/abc.py @@ -0,0 +1,171 @@ +import abc +import io +import itertools +import pathlib +from typing import Any, BinaryIO, Iterable, Iterator, NoReturn, Text, Optional +from typing import runtime_checkable, Protocol + +from .compat.py38 import StrPath + + +__all__ = ["ResourceReader", "Traversable", "TraversableResources"] + + +class ResourceReader(metaclass=abc.ABCMeta): + """Abstract base class for loaders to provide resource reading support.""" + + @abc.abstractmethod + def open_resource(self, resource: Text) -> BinaryIO: + """Return an opened, file-like object for binary reading. + + The 'resource' argument is expected to represent only a file name. + If the resource cannot be found, FileNotFoundError is raised. + """ + # This deliberately raises FileNotFoundError instead of + # NotImplementedError so that if this method is accidentally called, + # it'll still do the right thing. + raise FileNotFoundError + + @abc.abstractmethod + def resource_path(self, resource: Text) -> Text: + """Return the file system path to the specified resource. + + The 'resource' argument is expected to represent only a file name. + If the resource does not exist on the file system, raise + FileNotFoundError. + """ + # This deliberately raises FileNotFoundError instead of + # NotImplementedError so that if this method is accidentally called, + # it'll still do the right thing. + raise FileNotFoundError + + @abc.abstractmethod + def is_resource(self, path: Text) -> bool: + """Return True if the named 'path' is a resource. + + Files are resources, directories are not. + """ + raise FileNotFoundError + + @abc.abstractmethod + def contents(self) -> Iterable[str]: + """Return an iterable of entries in `package`.""" + raise FileNotFoundError + + +class TraversalError(Exception): + pass + + +@runtime_checkable +class Traversable(Protocol): + """ + An object with a subset of pathlib.Path methods suitable for + traversing directories and opening files. + + Any exceptions that occur when accessing the backing resource + may propagate unaltered. + """ + + @abc.abstractmethod + def iterdir(self) -> Iterator["Traversable"]: + """ + Yield Traversable objects in self + """ + + def read_bytes(self) -> bytes: + """ + Read contents of self as bytes + """ + with self.open('rb') as strm: + return strm.read() + + def read_text(self, encoding: Optional[str] = None) -> str: + """ + Read contents of self as text + """ + with self.open(encoding=encoding) as strm: + return strm.read() + + @abc.abstractmethod + def is_dir(self) -> bool: + """ + Return True if self is a directory + """ + + @abc.abstractmethod + def is_file(self) -> bool: + """ + Return True if self is a file + """ + + def joinpath(self, *descendants: StrPath) -> "Traversable": + """ + Return Traversable resolved with any descendants applied. + + Each descendant should be a path segment relative to self + and each may contain multiple levels separated by + ``posixpath.sep`` (``/``). + """ + if not descendants: + return self + names = itertools.chain.from_iterable( + path.parts for path in map(pathlib.PurePosixPath, descendants) + ) + target = next(names) + matches = ( + traversable for traversable in self.iterdir() if traversable.name == target + ) + try: + match = next(matches) + except StopIteration: + raise TraversalError( + "Target not found during traversal.", target, list(names) + ) + return match.joinpath(*names) + + def __truediv__(self, child: StrPath) -> "Traversable": + """ + Return Traversable child in self + """ + return self.joinpath(child) + + @abc.abstractmethod + def open(self, mode='r', *args, **kwargs): + """ + mode may be 'r' or 'rb' to open as text or binary. Return a handle + suitable for reading (same as pathlib.Path.open). + + When opening as text, accepts encoding parameters such as those + accepted by io.TextIOWrapper. + """ + + @property + @abc.abstractmethod + def name(self) -> str: + """ + The base name of this object without any parent references. + """ + + +class TraversableResources(ResourceReader): + """ + The required interface for providing traversable + resources. + """ + + @abc.abstractmethod + def files(self) -> "Traversable": + """Return a Traversable object for the loaded package.""" + + def open_resource(self, resource: StrPath) -> io.BufferedReader: + return self.files().joinpath(resource).open('rb') + + def resource_path(self, resource: Any) -> NoReturn: + raise FileNotFoundError(resource) + + def is_resource(self, path: StrPath) -> bool: + return self.files().joinpath(path).is_file() + + def contents(self) -> Iterator[str]: + return (item.name for item in self.files().iterdir()) diff --git a/libs/common/importlib_resources/compat/__init__.py b/libs/common/importlib_resources/compat/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/common/importlib_resources/compat/py38.py b/libs/common/importlib_resources/compat/py38.py new file mode 100644 index 00000000..4d548257 --- /dev/null +++ b/libs/common/importlib_resources/compat/py38.py @@ -0,0 +1,11 @@ +import os +import sys + +from typing import Union + + +if sys.version_info >= (3, 9): + StrPath = Union[str, os.PathLike[str]] +else: + # PathLike is only subscriptable at runtime in 3.9+ + StrPath = Union[str, "os.PathLike[str]"] diff --git a/libs/common/importlib_resources/compat/py39.py b/libs/common/importlib_resources/compat/py39.py new file mode 100644 index 00000000..ab87b9dc --- /dev/null +++ b/libs/common/importlib_resources/compat/py39.py @@ -0,0 +1,10 @@ +import sys + + +__all__ = ['ZipPath'] + + +if sys.version_info >= (3, 10): + from zipfile import Path as ZipPath # type: ignore +else: + from zipp import Path as ZipPath # type: ignore diff --git a/libs/common/importlib_resources/future/__init__.py b/libs/common/importlib_resources/future/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/common/importlib_resources/future/adapters.py b/libs/common/importlib_resources/future/adapters.py new file mode 100644 index 00000000..56dea7cf --- /dev/null +++ b/libs/common/importlib_resources/future/adapters.py @@ -0,0 +1,46 @@ +import pathlib +from contextlib import suppress +from types import SimpleNamespace + +from .. import readers, _adapters + + +class TraversableResourcesLoader(_adapters.TraversableResourcesLoader): + """ + Adapt loaders to provide TraversableResources and other + compatibility. + + Ensures the readers from importlib_resources are preferred + over stdlib readers. + """ + + def get_resource_reader(self, name): + return self._standard_reader() or super().get_resource_reader(name) + + def _standard_reader(self): + return self._zip_reader() or self._namespace_reader() or self._file_reader() + + def _zip_reader(self): + with suppress(AttributeError): + return readers.ZipReader(self.spec.loader, self.spec.name) + + def _namespace_reader(self): + with suppress(AttributeError, ValueError): + return readers.NamespaceReader(self.spec.submodule_search_locations) + + def _file_reader(self): + try: + path = pathlib.Path(self.spec.origin) + except TypeError: + return None + if path.exists(): + return readers.FileReader(SimpleNamespace(path=path)) + + +def wrap_spec(package): + """ + Override _adapters.wrap_spec to use TraversableResourcesLoader + from above. Ensures that future behavior is always available on older + Pythons. + """ + return _adapters.SpecLoaderAdapter(package.__spec__, TraversableResourcesLoader) diff --git a/libs/common/importlib_resources/py.typed b/libs/common/importlib_resources/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/libs/common/importlib_resources/readers.py b/libs/common/importlib_resources/readers.py new file mode 100644 index 00000000..4a80a774 --- /dev/null +++ b/libs/common/importlib_resources/readers.py @@ -0,0 +1,194 @@ +import collections +import contextlib +import itertools +import pathlib +import operator +import re +import warnings + +from . import abc + +from ._itertools import only +from .compat.py39 import ZipPath + + +def remove_duplicates(items): + return iter(collections.OrderedDict.fromkeys(items)) + + +class FileReader(abc.TraversableResources): + def __init__(self, loader): + self.path = pathlib.Path(loader.path).parent + + def resource_path(self, resource): + """ + Return the file system path to prevent + `resources.path()` from creating a temporary + copy. + """ + return str(self.path.joinpath(resource)) + + def files(self): + return self.path + + +class ZipReader(abc.TraversableResources): + def __init__(self, loader, module): + _, _, name = module.rpartition('.') + self.prefix = loader.prefix.replace('\\', '/') + name + '/' + self.archive = loader.archive + + def open_resource(self, resource): + try: + return super().open_resource(resource) + except KeyError as exc: + raise FileNotFoundError(exc.args[0]) + + def is_resource(self, path): + """ + Workaround for `zipfile.Path.is_file` returning true + for non-existent paths. + """ + target = self.files().joinpath(path) + return target.is_file() and target.exists() + + def files(self): + return ZipPath(self.archive, self.prefix) + + +class MultiplexedPath(abc.Traversable): + """ + Given a series of Traversable objects, implement a merged + version of the interface across all objects. Useful for + namespace packages which may be multihomed at a single + name. + """ + + def __init__(self, *paths): + self._paths = list(map(_ensure_traversable, remove_duplicates(paths))) + if not self._paths: + message = 'MultiplexedPath must contain at least one path' + raise FileNotFoundError(message) + if not all(path.is_dir() for path in self._paths): + raise NotADirectoryError('MultiplexedPath only supports directories') + + def iterdir(self): + children = (child for path in self._paths for child in path.iterdir()) + by_name = operator.attrgetter('name') + groups = itertools.groupby(sorted(children, key=by_name), key=by_name) + return map(self._follow, (locs for name, locs in groups)) + + def read_bytes(self): + raise FileNotFoundError(f'{self} is not a file') + + def read_text(self, *args, **kwargs): + raise FileNotFoundError(f'{self} is not a file') + + def is_dir(self): + return True + + def is_file(self): + return False + + def joinpath(self, *descendants): + try: + return super().joinpath(*descendants) + except abc.TraversalError: + # One of the paths did not resolve (a directory does not exist). + # Just return something that will not exist. + return self._paths[0].joinpath(*descendants) + + @classmethod + def _follow(cls, children): + """ + Construct a MultiplexedPath if needed. + + If children contains a sole element, return it. + Otherwise, return a MultiplexedPath of the items. + Unless one of the items is not a Directory, then return the first. + """ + subdirs, one_dir, one_file = itertools.tee(children, 3) + + try: + return only(one_dir) + except ValueError: + try: + return cls(*subdirs) + except NotADirectoryError: + return next(one_file) + + def open(self, *args, **kwargs): + raise FileNotFoundError(f'{self} is not a file') + + @property + def name(self): + return self._paths[0].name + + def __repr__(self): + paths = ', '.join(f"'{path}'" for path in self._paths) + return f'MultiplexedPath({paths})' + + +class NamespaceReader(abc.TraversableResources): + def __init__(self, namespace_path): + if 'NamespacePath' not in str(namespace_path): + raise ValueError('Invalid path') + self.path = MultiplexedPath(*map(self._resolve, namespace_path)) + + @classmethod + def _resolve(cls, path_str) -> abc.Traversable: + r""" + Given an item from a namespace path, resolve it to a Traversable. + + path_str might be a directory on the filesystem or a path to a + zipfile plus the path within the zipfile, e.g. ``/foo/bar`` or + ``/foo/baz.zip/inner_dir`` or ``foo\baz.zip\inner_dir\sub``. + """ + (dir,) = (cand for cand in cls._candidate_paths(path_str) if cand.is_dir()) + return dir + + @classmethod + def _candidate_paths(cls, path_str): + yield pathlib.Path(path_str) + yield from cls._resolve_zip_path(path_str) + + @staticmethod + def _resolve_zip_path(path_str): + for match in reversed(list(re.finditer(r'[\\/]', path_str))): + with contextlib.suppress( + FileNotFoundError, + IsADirectoryError, + NotADirectoryError, + PermissionError, + ): + inner = path_str[match.end() :].replace('\\', '/') + '/' + yield ZipPath(path_str[: match.start()], inner.lstrip('/')) + + def resource_path(self, resource): + """ + Return the file system path to prevent + `resources.path()` from creating a temporary + copy. + """ + return str(self.path.joinpath(resource)) + + def files(self): + return self.path + + +def _ensure_traversable(path): + """ + Convert deprecated string arguments to traversables (pathlib.Path). + + Remove with Python 3.15. + """ + if not isinstance(path, str): + return path + + warnings.warn( + "String arguments are deprecated. Pass a Traversable instead.", + DeprecationWarning, + stacklevel=3, + ) + + return pathlib.Path(path) diff --git a/libs/common/importlib_resources/simple.py b/libs/common/importlib_resources/simple.py new file mode 100644 index 00000000..96f117fe --- /dev/null +++ b/libs/common/importlib_resources/simple.py @@ -0,0 +1,106 @@ +""" +Interface adapters for low-level readers. +""" + +import abc +import io +import itertools +from typing import BinaryIO, List + +from .abc import Traversable, TraversableResources + + +class SimpleReader(abc.ABC): + """ + The minimum, low-level interface required from a resource + provider. + """ + + @property + @abc.abstractmethod + def package(self) -> str: + """ + The name of the package for which this reader loads resources. + """ + + @abc.abstractmethod + def children(self) -> List['SimpleReader']: + """ + Obtain an iterable of SimpleReader for available + child containers (e.g. directories). + """ + + @abc.abstractmethod + def resources(self) -> List[str]: + """ + Obtain available named resources for this virtual package. + """ + + @abc.abstractmethod + def open_binary(self, resource: str) -> BinaryIO: + """ + Obtain a File-like for a named resource. + """ + + @property + def name(self): + return self.package.split('.')[-1] + + +class ResourceContainer(Traversable): + """ + Traversable container for a package's resources via its reader. + """ + + def __init__(self, reader: SimpleReader): + self.reader = reader + + def is_dir(self): + return True + + def is_file(self): + return False + + def iterdir(self): + files = (ResourceHandle(self, name) for name in self.reader.resources) + dirs = map(ResourceContainer, self.reader.children()) + return itertools.chain(files, dirs) + + def open(self, *args, **kwargs): + raise IsADirectoryError() + + +class ResourceHandle(Traversable): + """ + Handle to a named resource in a ResourceReader. + """ + + def __init__(self, parent: ResourceContainer, name: str): + self.parent = parent + self.name = name # type: ignore + + def is_file(self): + return True + + def is_dir(self): + return False + + def open(self, mode='r', *args, **kwargs): + stream = self.parent.reader.open_binary(self.name) + if 'b' not in mode: + stream = io.TextIOWrapper(stream, *args, **kwargs) + return stream + + def joinpath(self, name): + raise RuntimeError("Cannot traverse into a resource") + + +class TraversableReader(TraversableResources, SimpleReader): + """ + A TraversableResources based on SimpleReader. Resource providers + may derive from this class to provide the TraversableResources + interface by supplying the SimpleReader interface. + """ + + def files(self): + return ResourceContainer(self) diff --git a/libs/common/importlib_resources/tests/__init__.py b/libs/common/importlib_resources/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/common/importlib_resources/tests/_compat.py b/libs/common/importlib_resources/tests/_compat.py new file mode 100644 index 00000000..e7bf06dd --- /dev/null +++ b/libs/common/importlib_resources/tests/_compat.py @@ -0,0 +1,32 @@ +import os + + +try: + from test.support import import_helper # type: ignore +except ImportError: + # Python 3.9 and earlier + class import_helper: # type: ignore + from test.support import ( + modules_setup, + modules_cleanup, + DirsOnSysPath, + CleanImport, + ) + + +try: + from test.support import os_helper # type: ignore +except ImportError: + # Python 3.9 compat + class os_helper: # type:ignore + from test.support import temp_dir + + +try: + # Python 3.10 + from test.support.os_helper import unlink +except ImportError: + from test.support import unlink as _unlink + + def unlink(target): + return _unlink(os.fspath(target)) diff --git a/libs/common/importlib_resources/tests/_path.py b/libs/common/importlib_resources/tests/_path.py new file mode 100644 index 00000000..1f97c961 --- /dev/null +++ b/libs/common/importlib_resources/tests/_path.py @@ -0,0 +1,56 @@ +import pathlib +import functools + +from typing import Dict, Union + + +#### +# from jaraco.path 3.4.1 + +FilesSpec = Dict[str, Union[str, bytes, 'FilesSpec']] # type: ignore + + +def build(spec: FilesSpec, prefix=pathlib.Path()): + """ + Build a set of files/directories, as described by the spec. + + Each key represents a pathname, and the value represents + the content. Content may be a nested directory. + + >>> spec = { + ... 'README.txt': "A README file", + ... "foo": { + ... "__init__.py": "", + ... "bar": { + ... "__init__.py": "", + ... }, + ... "baz.py": "# Some code", + ... } + ... } + >>> target = getfixture('tmp_path') + >>> build(spec, target) + >>> target.joinpath('foo/baz.py').read_text(encoding='utf-8') + '# Some code' + """ + for name, contents in spec.items(): + create(contents, pathlib.Path(prefix) / name) + + +@functools.singledispatch +def create(content: Union[str, bytes, FilesSpec], path): + path.mkdir(exist_ok=True) + build(content, prefix=path) # type: ignore + + +@create.register +def _(content: bytes, path): + path.write_bytes(content) + + +@create.register +def _(content: str, path): + path.write_text(content, encoding='utf-8') + + +# end from jaraco.path +#### diff --git a/libs/common/importlib_resources/tests/data01/__init__.py b/libs/common/importlib_resources/tests/data01/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/common/importlib_resources/tests/data01/binary.file b/libs/common/importlib_resources/tests/data01/binary.file new file mode 100644 index 0000000000000000000000000000000000000000..eaf36c1daccfdf325514461cd1a2ffbc139b5464 GIT binary patch literal 4 LcmZQzWMT#Y01f~L literal 0 HcmV?d00001 diff --git a/libs/common/importlib_resources/tests/data01/subdirectory/__init__.py b/libs/common/importlib_resources/tests/data01/subdirectory/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/common/importlib_resources/tests/data01/subdirectory/binary.file b/libs/common/importlib_resources/tests/data01/subdirectory/binary.file new file mode 100644 index 00000000..5bd8bb89 --- /dev/null +++ b/libs/common/importlib_resources/tests/data01/subdirectory/binary.file @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/libs/common/importlib_resources/tests/data01/utf-16.file b/libs/common/importlib_resources/tests/data01/utf-16.file new file mode 100644 index 0000000000000000000000000000000000000000..2cb772295ef4b480a8d83725bd5006a0236d8f68 GIT binary patch literal 44 ucmezW&x0YAAqNQa8FUyF7(y9B7~B|i84MZBfV^^`Xc15@g+Y;liva-T)Ce>H literal 0 HcmV?d00001 diff --git a/libs/common/importlib_resources/tests/data01/utf-8.file b/libs/common/importlib_resources/tests/data01/utf-8.file new file mode 100644 index 00000000..1c0132ad --- /dev/null +++ b/libs/common/importlib_resources/tests/data01/utf-8.file @@ -0,0 +1 @@ +Hello, UTF-8 world! diff --git a/libs/common/importlib_resources/tests/data02/__init__.py b/libs/common/importlib_resources/tests/data02/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/common/importlib_resources/tests/data02/one/__init__.py b/libs/common/importlib_resources/tests/data02/one/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/common/importlib_resources/tests/data02/one/resource1.txt b/libs/common/importlib_resources/tests/data02/one/resource1.txt new file mode 100644 index 00000000..61a813e4 --- /dev/null +++ b/libs/common/importlib_resources/tests/data02/one/resource1.txt @@ -0,0 +1 @@ +one resource diff --git a/libs/common/importlib_resources/tests/data02/subdirectory/subsubdir/resource.txt b/libs/common/importlib_resources/tests/data02/subdirectory/subsubdir/resource.txt new file mode 100644 index 00000000..48f587a2 --- /dev/null +++ b/libs/common/importlib_resources/tests/data02/subdirectory/subsubdir/resource.txt @@ -0,0 +1 @@ +a resource \ No newline at end of file diff --git a/libs/common/importlib_resources/tests/data02/two/__init__.py b/libs/common/importlib_resources/tests/data02/two/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/libs/common/importlib_resources/tests/data02/two/resource2.txt b/libs/common/importlib_resources/tests/data02/two/resource2.txt new file mode 100644 index 00000000..a80ce46e --- /dev/null +++ b/libs/common/importlib_resources/tests/data02/two/resource2.txt @@ -0,0 +1 @@ +two resource diff --git a/libs/common/importlib_resources/tests/namespacedata01/binary.file b/libs/common/importlib_resources/tests/namespacedata01/binary.file new file mode 100644 index 0000000000000000000000000000000000000000..eaf36c1daccfdf325514461cd1a2ffbc139b5464 GIT binary patch literal 4 LcmZQzWMT#Y01f~L literal 0 HcmV?d00001 diff --git a/libs/common/importlib_resources/tests/namespacedata01/subdirectory/binary.file b/libs/common/importlib_resources/tests/namespacedata01/subdirectory/binary.file new file mode 100644 index 00000000..100f5064 --- /dev/null +++ b/libs/common/importlib_resources/tests/namespacedata01/subdirectory/binary.file @@ -0,0 +1 @@ +  \ No newline at end of file diff --git a/libs/common/importlib_resources/tests/namespacedata01/utf-16.file b/libs/common/importlib_resources/tests/namespacedata01/utf-16.file new file mode 100644 index 0000000000000000000000000000000000000000..2cb772295ef4b480a8d83725bd5006a0236d8f68 GIT binary patch literal 44 ucmezW&x0YAAqNQa8FUyF7(y9B7~B|i84MZBfV^^`Xc15@g+Y;liva-T)Ce>H literal 0 HcmV?d00001 diff --git a/libs/common/importlib_resources/tests/namespacedata01/utf-8.file b/libs/common/importlib_resources/tests/namespacedata01/utf-8.file new file mode 100644 index 00000000..1c0132ad --- /dev/null +++ b/libs/common/importlib_resources/tests/namespacedata01/utf-8.file @@ -0,0 +1 @@ +Hello, UTF-8 world! diff --git a/libs/common/importlib_resources/tests/test_compatibilty_files.py b/libs/common/importlib_resources/tests/test_compatibilty_files.py new file mode 100644 index 00000000..13ad0dfb --- /dev/null +++ b/libs/common/importlib_resources/tests/test_compatibilty_files.py @@ -0,0 +1,104 @@ +import io +import unittest + +import importlib_resources as resources + +from importlib_resources._adapters import ( + CompatibilityFiles, + wrap_spec, +) + +from . import util + + +class CompatibilityFilesTests(unittest.TestCase): + @property + def package(self): + bytes_data = io.BytesIO(b'Hello, world!') + return util.create_package( + file=bytes_data, + path='some_path', + contents=('a', 'b', 'c'), + ) + + @property + def files(self): + return resources.files(self.package) + + def test_spec_path_iter(self): + self.assertEqual( + sorted(path.name for path in self.files.iterdir()), + ['a', 'b', 'c'], + ) + + def test_child_path_iter(self): + self.assertEqual(list((self.files / 'a').iterdir()), []) + + def test_orphan_path_iter(self): + self.assertEqual(list((self.files / 'a' / 'a').iterdir()), []) + self.assertEqual(list((self.files / 'a' / 'a' / 'a').iterdir()), []) + + def test_spec_path_is(self): + self.assertFalse(self.files.is_file()) + self.assertFalse(self.files.is_dir()) + + def test_child_path_is(self): + self.assertTrue((self.files / 'a').is_file()) + self.assertFalse((self.files / 'a').is_dir()) + + def test_orphan_path_is(self): + self.assertFalse((self.files / 'a' / 'a').is_file()) + self.assertFalse((self.files / 'a' / 'a').is_dir()) + self.assertFalse((self.files / 'a' / 'a' / 'a').is_file()) + self.assertFalse((self.files / 'a' / 'a' / 'a').is_dir()) + + def test_spec_path_name(self): + self.assertEqual(self.files.name, 'testingpackage') + + def test_child_path_name(self): + self.assertEqual((self.files / 'a').name, 'a') + + def test_orphan_path_name(self): + self.assertEqual((self.files / 'a' / 'b').name, 'b') + self.assertEqual((self.files / 'a' / 'b' / 'c').name, 'c') + + def test_spec_path_open(self): + self.assertEqual(self.files.read_bytes(), b'Hello, world!') + self.assertEqual(self.files.read_text(encoding='utf-8'), 'Hello, world!') + + def test_child_path_open(self): + self.assertEqual((self.files / 'a').read_bytes(), b'Hello, world!') + self.assertEqual( + (self.files / 'a').read_text(encoding='utf-8'), 'Hello, world!' + ) + + def test_orphan_path_open(self): + with self.assertRaises(FileNotFoundError): + (self.files / 'a' / 'b').read_bytes() + with self.assertRaises(FileNotFoundError): + (self.files / 'a' / 'b' / 'c').read_bytes() + + def test_open_invalid_mode(self): + with self.assertRaises(ValueError): + self.files.open('0') + + def test_orphan_path_invalid(self): + with self.assertRaises(ValueError): + CompatibilityFiles.OrphanPath() + + def test_wrap_spec(self): + spec = wrap_spec(self.package) + self.assertIsInstance(spec.loader.get_resource_reader(None), CompatibilityFiles) + + +class CompatibilityFilesNoReaderTests(unittest.TestCase): + @property + def package(self): + return util.create_package_from_loader(None) + + @property + def files(self): + return resources.files(self.package) + + def test_spec_path_joinpath(self): + self.assertIsInstance(self.files / 'a', CompatibilityFiles.OrphanPath) diff --git a/libs/common/importlib_resources/tests/test_contents.py b/libs/common/importlib_resources/tests/test_contents.py new file mode 100644 index 00000000..7dc3b0a6 --- /dev/null +++ b/libs/common/importlib_resources/tests/test_contents.py @@ -0,0 +1,43 @@ +import unittest +import importlib_resources as resources + +from . import data01 +from . import util + + +class ContentsTests: + expected = { + '__init__.py', + 'binary.file', + 'subdirectory', + 'utf-16.file', + 'utf-8.file', + } + + def test_contents(self): + contents = {path.name for path in resources.files(self.data).iterdir()} + assert self.expected <= contents + + +class ContentsDiskTests(ContentsTests, unittest.TestCase): + def setUp(self): + self.data = data01 + + +class ContentsZipTests(ContentsTests, util.ZipSetup, unittest.TestCase): + pass + + +class ContentsNamespaceTests(ContentsTests, unittest.TestCase): + expected = { + # no __init__ because of namespace design + 'binary.file', + 'subdirectory', + 'utf-16.file', + 'utf-8.file', + } + + def setUp(self): + from . import namespacedata01 + + self.data = namespacedata01 diff --git a/libs/common/importlib_resources/tests/test_custom.py b/libs/common/importlib_resources/tests/test_custom.py new file mode 100644 index 00000000..35336f8f --- /dev/null +++ b/libs/common/importlib_resources/tests/test_custom.py @@ -0,0 +1,47 @@ +import unittest +import contextlib +import pathlib + +import importlib_resources as resources +from .. import abc +from ..abc import TraversableResources, ResourceReader +from . import util +from ._compat import os_helper + + +class SimpleLoader: + """ + A simple loader that only implements a resource reader. + """ + + def __init__(self, reader: ResourceReader): + self.reader = reader + + def get_resource_reader(self, package): + return self.reader + + +class MagicResources(TraversableResources): + """ + Magically returns the resources at path. + """ + + def __init__(self, path: pathlib.Path): + self.path = path + + def files(self): + return self.path + + +class CustomTraversableResourcesTests(unittest.TestCase): + def setUp(self): + self.fixtures = contextlib.ExitStack() + self.addCleanup(self.fixtures.close) + + def test_custom_loader(self): + temp_dir = pathlib.Path(self.fixtures.enter_context(os_helper.temp_dir())) + loader = SimpleLoader(MagicResources(temp_dir)) + pkg = util.create_package_from_loader(loader) + files = resources.files(pkg) + assert isinstance(files, abc.Traversable) + assert list(files.iterdir()) == [] diff --git a/libs/common/importlib_resources/tests/test_files.py b/libs/common/importlib_resources/tests/test_files.py new file mode 100644 index 00000000..51ecc896 --- /dev/null +++ b/libs/common/importlib_resources/tests/test_files.py @@ -0,0 +1,111 @@ +import textwrap +import unittest +import warnings +import importlib +import contextlib + +import importlib_resources as resources +from ..abc import Traversable +from . import data01 +from . import util +from . import _path +from ._compat import os_helper, import_helper + + +@contextlib.contextmanager +def suppress_known_deprecation(): + with warnings.catch_warnings(record=True) as ctx: + warnings.simplefilter('default', category=DeprecationWarning) + yield ctx + + +class FilesTests: + def test_read_bytes(self): + files = resources.files(self.data) + actual = files.joinpath('utf-8.file').read_bytes() + assert actual == b'Hello, UTF-8 world!\n' + + def test_read_text(self): + files = resources.files(self.data) + actual = files.joinpath('utf-8.file').read_text(encoding='utf-8') + assert actual == 'Hello, UTF-8 world!\n' + + def test_traversable(self): + assert isinstance(resources.files(self.data), Traversable) + + def test_old_parameter(self): + """ + Files used to take a 'package' parameter. Make sure anyone + passing by name is still supported. + """ + with suppress_known_deprecation(): + resources.files(package=self.data) + + +class OpenDiskTests(FilesTests, unittest.TestCase): + def setUp(self): + self.data = data01 + + +class OpenZipTests(FilesTests, util.ZipSetup, unittest.TestCase): + pass + + +class OpenNamespaceTests(FilesTests, unittest.TestCase): + def setUp(self): + from . import namespacedata01 + + self.data = namespacedata01 + + +class OpenNamespaceZipTests(FilesTests, util.ZipSetup, unittest.TestCase): + ZIP_MODULE = 'namespacedata01' + + +class SiteDir: + def setUp(self): + self.fixtures = contextlib.ExitStack() + self.addCleanup(self.fixtures.close) + self.site_dir = self.fixtures.enter_context(os_helper.temp_dir()) + self.fixtures.enter_context(import_helper.DirsOnSysPath(self.site_dir)) + self.fixtures.enter_context(import_helper.CleanImport()) + + +class ModulesFilesTests(SiteDir, unittest.TestCase): + def test_module_resources(self): + """ + A module can have resources found adjacent to the module. + """ + spec = { + 'mod.py': '', + 'res.txt': 'resources are the best', + } + _path.build(spec, self.site_dir) + import mod + + actual = resources.files(mod).joinpath('res.txt').read_text(encoding='utf-8') + assert actual == spec['res.txt'] + + +class ImplicitContextFilesTests(SiteDir, unittest.TestCase): + def test_implicit_files(self): + """ + Without any parameter, files() will infer the location as the caller. + """ + spec = { + 'somepkg': { + '__init__.py': textwrap.dedent( + """ + import importlib_resources as res + val = res.files().joinpath('res.txt').read_text(encoding='utf-8') + """ + ), + 'res.txt': 'resources are the best', + }, + } + _path.build(spec, self.site_dir) + assert importlib.import_module('somepkg').val == 'resources are the best' + + +if __name__ == '__main__': + unittest.main() diff --git a/libs/common/importlib_resources/tests/test_open.py b/libs/common/importlib_resources/tests/test_open.py new file mode 100644 index 00000000..44f1018a --- /dev/null +++ b/libs/common/importlib_resources/tests/test_open.py @@ -0,0 +1,89 @@ +import unittest + +import importlib_resources as resources +from . import data01 +from . import util + + +class CommonBinaryTests(util.CommonTests, unittest.TestCase): + def execute(self, package, path): + target = resources.files(package).joinpath(path) + with target.open('rb'): + pass + + +class CommonTextTests(util.CommonTests, unittest.TestCase): + def execute(self, package, path): + target = resources.files(package).joinpath(path) + with target.open(encoding='utf-8'): + pass + + +class OpenTests: + def test_open_binary(self): + target = resources.files(self.data) / 'binary.file' + with target.open('rb') as fp: + result = fp.read() + self.assertEqual(result, bytes(range(4))) + + def test_open_text_default_encoding(self): + target = resources.files(self.data) / 'utf-8.file' + with target.open(encoding='utf-8') as fp: + result = fp.read() + self.assertEqual(result, 'Hello, UTF-8 world!\n') + + def test_open_text_given_encoding(self): + target = resources.files(self.data) / 'utf-16.file' + with target.open(encoding='utf-16', errors='strict') as fp: + result = fp.read() + self.assertEqual(result, 'Hello, UTF-16 world!\n') + + def test_open_text_with_errors(self): + """ + Raises UnicodeError without the 'errors' argument. + """ + target = resources.files(self.data) / 'utf-16.file' + with target.open(encoding='utf-8', errors='strict') as fp: + self.assertRaises(UnicodeError, fp.read) + with target.open(encoding='utf-8', errors='ignore') as fp: + result = fp.read() + self.assertEqual( + result, + 'H\x00e\x00l\x00l\x00o\x00,\x00 ' + '\x00U\x00T\x00F\x00-\x001\x006\x00 ' + '\x00w\x00o\x00r\x00l\x00d\x00!\x00\n\x00', + ) + + def test_open_binary_FileNotFoundError(self): + target = resources.files(self.data) / 'does-not-exist' + with self.assertRaises(FileNotFoundError): + target.open('rb') + + def test_open_text_FileNotFoundError(self): + target = resources.files(self.data) / 'does-not-exist' + with self.assertRaises(FileNotFoundError): + target.open(encoding='utf-8') + + +class OpenDiskTests(OpenTests, unittest.TestCase): + def setUp(self): + self.data = data01 + + +class OpenDiskNamespaceTests(OpenTests, unittest.TestCase): + def setUp(self): + from . import namespacedata01 + + self.data = namespacedata01 + + +class OpenZipTests(OpenTests, util.ZipSetup, unittest.TestCase): + pass + + +class OpenNamespaceZipTests(OpenTests, util.ZipSetup, unittest.TestCase): + ZIP_MODULE = 'namespacedata01' + + +if __name__ == '__main__': + unittest.main() diff --git a/libs/common/importlib_resources/tests/test_path.py b/libs/common/importlib_resources/tests/test_path.py new file mode 100644 index 00000000..c3e1cbb4 --- /dev/null +++ b/libs/common/importlib_resources/tests/test_path.py @@ -0,0 +1,65 @@ +import io +import pathlib +import unittest + +import importlib_resources as resources +from . import data01 +from . import util + + +class CommonTests(util.CommonTests, unittest.TestCase): + def execute(self, package, path): + with resources.as_file(resources.files(package).joinpath(path)): + pass + + +class PathTests: + def test_reading(self): + """ + Path should be readable and a pathlib.Path instance. + """ + target = resources.files(self.data) / 'utf-8.file' + with resources.as_file(target) as path: + self.assertIsInstance(path, pathlib.Path) + self.assertTrue(path.name.endswith("utf-8.file"), repr(path)) + self.assertEqual('Hello, UTF-8 world!\n', path.read_text(encoding='utf-8')) + + +class PathDiskTests(PathTests, unittest.TestCase): + data = data01 + + def test_natural_path(self): + """ + Guarantee the internal implementation detail that + file-system-backed resources do not get the tempdir + treatment. + """ + target = resources.files(self.data) / 'utf-8.file' + with resources.as_file(target) as path: + assert 'data' in str(path) + + +class PathMemoryTests(PathTests, unittest.TestCase): + def setUp(self): + file = io.BytesIO(b'Hello, UTF-8 world!\n') + self.addCleanup(file.close) + self.data = util.create_package( + file=file, path=FileNotFoundError("package exists only in memory") + ) + self.data.__spec__.origin = None + self.data.__spec__.has_location = False + + +class PathZipTests(PathTests, util.ZipSetup, unittest.TestCase): + def test_remove_in_context_manager(self): + """ + It is not an error if the file that was temporarily stashed on the + file system is removed inside the `with` stanza. + """ + target = resources.files(self.data) / 'utf-8.file' + with resources.as_file(target) as path: + path.unlink() + + +if __name__ == '__main__': + unittest.main() diff --git a/libs/common/importlib_resources/tests/test_read.py b/libs/common/importlib_resources/tests/test_read.py new file mode 100644 index 00000000..97d90128 --- /dev/null +++ b/libs/common/importlib_resources/tests/test_read.py @@ -0,0 +1,97 @@ +import unittest +import importlib_resources as resources + +from . import data01 +from . import util +from importlib import import_module + + +class CommonBinaryTests(util.CommonTests, unittest.TestCase): + def execute(self, package, path): + resources.files(package).joinpath(path).read_bytes() + + +class CommonTextTests(util.CommonTests, unittest.TestCase): + def execute(self, package, path): + resources.files(package).joinpath(path).read_text(encoding='utf-8') + + +class ReadTests: + def test_read_bytes(self): + result = resources.files(self.data).joinpath('binary.file').read_bytes() + self.assertEqual(result, bytes(range(4))) + + def test_read_text_default_encoding(self): + result = ( + resources.files(self.data) + .joinpath('utf-8.file') + .read_text(encoding='utf-8') + ) + self.assertEqual(result, 'Hello, UTF-8 world!\n') + + def test_read_text_given_encoding(self): + result = ( + resources.files(self.data) + .joinpath('utf-16.file') + .read_text(encoding='utf-16') + ) + self.assertEqual(result, 'Hello, UTF-16 world!\n') + + def test_read_text_with_errors(self): + """ + Raises UnicodeError without the 'errors' argument. + """ + target = resources.files(self.data) / 'utf-16.file' + self.assertRaises(UnicodeError, target.read_text, encoding='utf-8') + result = target.read_text(encoding='utf-8', errors='ignore') + self.assertEqual( + result, + 'H\x00e\x00l\x00l\x00o\x00,\x00 ' + '\x00U\x00T\x00F\x00-\x001\x006\x00 ' + '\x00w\x00o\x00r\x00l\x00d\x00!\x00\n\x00', + ) + + +class ReadDiskTests(ReadTests, unittest.TestCase): + data = data01 + + +class ReadZipTests(ReadTests, util.ZipSetup, unittest.TestCase): + def test_read_submodule_resource(self): + submodule = import_module('data01.subdirectory') + result = resources.files(submodule).joinpath('binary.file').read_bytes() + self.assertEqual(result, bytes(range(4, 8))) + + def test_read_submodule_resource_by_name(self): + result = ( + resources.files('data01.subdirectory').joinpath('binary.file').read_bytes() + ) + self.assertEqual(result, bytes(range(4, 8))) + + +class ReadNamespaceTests(ReadTests, unittest.TestCase): + def setUp(self): + from . import namespacedata01 + + self.data = namespacedata01 + + +class ReadNamespaceZipTests(ReadTests, util.ZipSetup, unittest.TestCase): + ZIP_MODULE = 'namespacedata01' + + def test_read_submodule_resource(self): + submodule = import_module('namespacedata01.subdirectory') + result = resources.files(submodule).joinpath('binary.file').read_bytes() + self.assertEqual(result, bytes(range(12, 16))) + + def test_read_submodule_resource_by_name(self): + result = ( + resources.files('namespacedata01.subdirectory') + .joinpath('binary.file') + .read_bytes() + ) + self.assertEqual(result, bytes(range(12, 16))) + + +if __name__ == '__main__': + unittest.main() diff --git a/libs/common/importlib_resources/tests/test_reader.py b/libs/common/importlib_resources/tests/test_reader.py new file mode 100644 index 00000000..95c2fc85 --- /dev/null +++ b/libs/common/importlib_resources/tests/test_reader.py @@ -0,0 +1,145 @@ +import os.path +import sys +import pathlib +import unittest + +from importlib import import_module +from importlib_resources.readers import MultiplexedPath, NamespaceReader + + +class MultiplexedPathTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.folder = pathlib.Path(__file__).parent / 'namespacedata01' + + def test_init_no_paths(self): + with self.assertRaises(FileNotFoundError): + MultiplexedPath() + + def test_init_file(self): + with self.assertRaises(NotADirectoryError): + MultiplexedPath(self.folder / 'binary.file') + + def test_iterdir(self): + contents = {path.name for path in MultiplexedPath(self.folder).iterdir()} + try: + contents.remove('__pycache__') + except (KeyError, ValueError): + pass + self.assertEqual( + contents, {'subdirectory', 'binary.file', 'utf-16.file', 'utf-8.file'} + ) + + def test_iterdir_duplicate(self): + data01 = pathlib.Path(__file__).parent.joinpath('data01') + contents = { + path.name for path in MultiplexedPath(self.folder, data01).iterdir() + } + for remove in ('__pycache__', '__init__.pyc'): + try: + contents.remove(remove) + except (KeyError, ValueError): + pass + self.assertEqual( + contents, + {'__init__.py', 'binary.file', 'subdirectory', 'utf-16.file', 'utf-8.file'}, + ) + + def test_is_dir(self): + self.assertEqual(MultiplexedPath(self.folder).is_dir(), True) + + def test_is_file(self): + self.assertEqual(MultiplexedPath(self.folder).is_file(), False) + + def test_open_file(self): + path = MultiplexedPath(self.folder) + with self.assertRaises(FileNotFoundError): + path.read_bytes() + with self.assertRaises(FileNotFoundError): + path.read_text() + with self.assertRaises(FileNotFoundError): + path.open() + + def test_join_path(self): + data01 = pathlib.Path(__file__).parent.joinpath('data01') + prefix = str(data01.parent) + path = MultiplexedPath(self.folder, data01) + self.assertEqual( + str(path.joinpath('binary.file'))[len(prefix) + 1 :], + os.path.join('namespacedata01', 'binary.file'), + ) + sub = path.joinpath('subdirectory') + assert isinstance(sub, MultiplexedPath) + assert 'namespacedata01' in str(sub) + assert 'data01' in str(sub) + self.assertEqual( + str(path.joinpath('imaginary'))[len(prefix) + 1 :], + os.path.join('namespacedata01', 'imaginary'), + ) + self.assertEqual(path.joinpath(), path) + + def test_join_path_compound(self): + path = MultiplexedPath(self.folder) + assert not path.joinpath('imaginary/foo.py').exists() + + def test_join_path_common_subdir(self): + data01 = pathlib.Path(__file__).parent.joinpath('data01') + data02 = pathlib.Path(__file__).parent.joinpath('data02') + prefix = str(data01.parent) + path = MultiplexedPath(data01, data02) + self.assertIsInstance(path.joinpath('subdirectory'), MultiplexedPath) + self.assertEqual( + str(path.joinpath('subdirectory', 'subsubdir'))[len(prefix) + 1 :], + os.path.join('data02', 'subdirectory', 'subsubdir'), + ) + + def test_repr(self): + self.assertEqual( + repr(MultiplexedPath(self.folder)), + f"MultiplexedPath('{self.folder}')", + ) + + def test_name(self): + self.assertEqual( + MultiplexedPath(self.folder).name, + os.path.basename(self.folder), + ) + + +class NamespaceReaderTest(unittest.TestCase): + site_dir = str(pathlib.Path(__file__).parent) + + @classmethod + def setUpClass(cls): + sys.path.append(cls.site_dir) + + @classmethod + def tearDownClass(cls): + sys.path.remove(cls.site_dir) + + def test_init_error(self): + with self.assertRaises(ValueError): + NamespaceReader(['path1', 'path2']) + + def test_resource_path(self): + namespacedata01 = import_module('namespacedata01') + reader = NamespaceReader(namespacedata01.__spec__.submodule_search_locations) + + root = os.path.abspath(os.path.join(__file__, '..', 'namespacedata01')) + self.assertEqual( + reader.resource_path('binary.file'), os.path.join(root, 'binary.file') + ) + self.assertEqual( + reader.resource_path('imaginary'), os.path.join(root, 'imaginary') + ) + + def test_files(self): + namespacedata01 = import_module('namespacedata01') + reader = NamespaceReader(namespacedata01.__spec__.submodule_search_locations) + root = os.path.abspath(os.path.join(__file__, '..', 'namespacedata01')) + self.assertIsInstance(reader.files(), MultiplexedPath) + self.assertEqual(repr(reader.files()), f"MultiplexedPath('{root}')") + + +if __name__ == '__main__': + unittest.main() diff --git a/libs/common/importlib_resources/tests/test_resource.py b/libs/common/importlib_resources/tests/test_resource.py new file mode 100644 index 00000000..dc2a108c --- /dev/null +++ b/libs/common/importlib_resources/tests/test_resource.py @@ -0,0 +1,241 @@ +import sys +import unittest +import importlib_resources as resources +import pathlib + +from . import data01 +from . import util +from importlib import import_module + + +class ResourceTests: + # Subclasses are expected to set the `data` attribute. + + def test_is_file_exists(self): + target = resources.files(self.data) / 'binary.file' + self.assertTrue(target.is_file()) + + def test_is_file_missing(self): + target = resources.files(self.data) / 'not-a-file' + self.assertFalse(target.is_file()) + + def test_is_dir(self): + target = resources.files(self.data) / 'subdirectory' + self.assertFalse(target.is_file()) + self.assertTrue(target.is_dir()) + + +class ResourceDiskTests(ResourceTests, unittest.TestCase): + def setUp(self): + self.data = data01 + + +class ResourceZipTests(ResourceTests, util.ZipSetup, unittest.TestCase): + pass + + +def names(traversable): + return {item.name for item in traversable.iterdir()} + + +class ResourceLoaderTests(unittest.TestCase): + def test_resource_contents(self): + package = util.create_package( + file=data01, path=data01.__file__, contents=['A', 'B', 'C'] + ) + self.assertEqual(names(resources.files(package)), {'A', 'B', 'C'}) + + def test_is_file(self): + package = util.create_package( + file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F'] + ) + self.assertTrue(resources.files(package).joinpath('B').is_file()) + + def test_is_dir(self): + package = util.create_package( + file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F'] + ) + self.assertTrue(resources.files(package).joinpath('D').is_dir()) + + def test_resource_missing(self): + package = util.create_package( + file=data01, path=data01.__file__, contents=['A', 'B', 'C', 'D/E', 'D/F'] + ) + self.assertFalse(resources.files(package).joinpath('Z').is_file()) + + +class ResourceCornerCaseTests(unittest.TestCase): + def test_package_has_no_reader_fallback(self): + """ + Test odd ball packages which: + # 1. Do not have a ResourceReader as a loader + # 2. Are not on the file system + # 3. Are not in a zip file + """ + module = util.create_package( + file=data01, path=data01.__file__, contents=['A', 'B', 'C'] + ) + # Give the module a dummy loader. + module.__loader__ = object() + # Give the module a dummy origin. + module.__file__ = '/path/which/shall/not/be/named' + module.__spec__.loader = module.__loader__ + module.__spec__.origin = module.__file__ + self.assertFalse(resources.files(module).joinpath('A').is_file()) + + +class ResourceFromZipsTest01(util.ZipSetupBase, unittest.TestCase): + ZIP_MODULE = 'data01' + + def test_is_submodule_resource(self): + submodule = import_module('data01.subdirectory') + self.assertTrue(resources.files(submodule).joinpath('binary.file').is_file()) + + def test_read_submodule_resource_by_name(self): + self.assertTrue( + resources.files('data01.subdirectory').joinpath('binary.file').is_file() + ) + + def test_submodule_contents(self): + submodule = import_module('data01.subdirectory') + self.assertEqual( + names(resources.files(submodule)), {'__init__.py', 'binary.file'} + ) + + def test_submodule_contents_by_name(self): + self.assertEqual( + names(resources.files('data01.subdirectory')), + {'__init__.py', 'binary.file'}, + ) + + def test_as_file_directory(self): + with resources.as_file(resources.files('data01')) as data: + assert data.name == 'data01' + assert data.is_dir() + assert data.joinpath('subdirectory').is_dir() + assert len(list(data.iterdir())) + assert not data.parent.exists() + + +class ResourceFromZipsTest02(util.ZipSetupBase, unittest.TestCase): + ZIP_MODULE = 'data02' + + def test_unrelated_contents(self): + """ + Test thata zip with two unrelated subpackages return + distinct resources. Ref python/importlib_resources#44. + """ + self.assertEqual( + names(resources.files('data02.one')), + {'__init__.py', 'resource1.txt'}, + ) + self.assertEqual( + names(resources.files('data02.two')), + {'__init__.py', 'resource2.txt'}, + ) + + +class DeletingZipsTest(util.ZipSetupBase, unittest.TestCase): + """Having accessed resources in a zip file should not keep an open + reference to the zip. + """ + + def test_iterdir_does_not_keep_open(self): + [item.name for item in resources.files('data01').iterdir()] + + def test_is_file_does_not_keep_open(self): + resources.files('data01').joinpath('binary.file').is_file() + + def test_is_file_failure_does_not_keep_open(self): + resources.files('data01').joinpath('not-present').is_file() + + @unittest.skip("Desired but not supported.") + def test_as_file_does_not_keep_open(self): # pragma: no cover + resources.as_file(resources.files('data01') / 'binary.file') + + def test_entered_path_does_not_keep_open(self): + """ + Mimic what certifi does on import to make its bundle + available for the process duration. + """ + resources.as_file(resources.files('data01') / 'binary.file').__enter__() + + def test_read_binary_does_not_keep_open(self): + resources.files('data01').joinpath('binary.file').read_bytes() + + def test_read_text_does_not_keep_open(self): + resources.files('data01').joinpath('utf-8.file').read_text(encoding='utf-8') + + +class ResourceFromNamespaceTests: + def test_is_submodule_resource(self): + self.assertTrue( + resources.files(import_module('namespacedata01')) + .joinpath('binary.file') + .is_file() + ) + + def test_read_submodule_resource_by_name(self): + self.assertTrue( + resources.files('namespacedata01').joinpath('binary.file').is_file() + ) + + def test_submodule_contents(self): + contents = names(resources.files(import_module('namespacedata01'))) + try: + contents.remove('__pycache__') + except KeyError: + pass + self.assertEqual( + contents, {'subdirectory', 'binary.file', 'utf-8.file', 'utf-16.file'} + ) + + def test_submodule_contents_by_name(self): + contents = names(resources.files('namespacedata01')) + try: + contents.remove('__pycache__') + except KeyError: + pass + self.assertEqual( + contents, {'subdirectory', 'binary.file', 'utf-8.file', 'utf-16.file'} + ) + + def test_submodule_sub_contents(self): + contents = names(resources.files(import_module('namespacedata01.subdirectory'))) + try: + contents.remove('__pycache__') + except KeyError: + pass + self.assertEqual(contents, {'binary.file'}) + + def test_submodule_sub_contents_by_name(self): + contents = names(resources.files('namespacedata01.subdirectory')) + try: + contents.remove('__pycache__') + except KeyError: + pass + self.assertEqual(contents, {'binary.file'}) + + +class ResourceFromNamespaceDiskTests(ResourceFromNamespaceTests, unittest.TestCase): + site_dir = str(pathlib.Path(__file__).parent) + + @classmethod + def setUpClass(cls): + sys.path.append(cls.site_dir) + + @classmethod + def tearDownClass(cls): + sys.path.remove(cls.site_dir) + + +class ResourceFromNamespaceZipTests( + util.ZipSetupBase, + ResourceFromNamespaceTests, + unittest.TestCase, +): + ZIP_MODULE = 'namespacedata01' + + +if __name__ == '__main__': + unittest.main() diff --git a/libs/common/importlib_resources/tests/util.py b/libs/common/importlib_resources/tests/util.py new file mode 100644 index 00000000..066f4113 --- /dev/null +++ b/libs/common/importlib_resources/tests/util.py @@ -0,0 +1,165 @@ +import abc +import importlib +import io +import sys +import types +import pathlib +import contextlib + +from . import data01 +from ..abc import ResourceReader +from ._compat import import_helper, os_helper +from . import zip as zip_ + + +from importlib.machinery import ModuleSpec + + +class Reader(ResourceReader): + def __init__(self, **kwargs): + vars(self).update(kwargs) + + def get_resource_reader(self, package): + return self + + def open_resource(self, path): + self._path = path + if isinstance(self.file, Exception): + raise self.file + return self.file + + def resource_path(self, path_): + self._path = path_ + if isinstance(self.path, Exception): + raise self.path + return self.path + + def is_resource(self, path_): + self._path = path_ + if isinstance(self.path, Exception): + raise self.path + + def part(entry): + return entry.split('/') + + return any( + len(parts) == 1 and parts[0] == path_ for parts in map(part, self._contents) + ) + + def contents(self): + if isinstance(self.path, Exception): + raise self.path + yield from self._contents + + +def create_package_from_loader(loader, is_package=True): + name = 'testingpackage' + module = types.ModuleType(name) + spec = ModuleSpec(name, loader, origin='does-not-exist', is_package=is_package) + module.__spec__ = spec + module.__loader__ = loader + return module + + +def create_package(file=None, path=None, is_package=True, contents=()): + return create_package_from_loader( + Reader(file=file, path=path, _contents=contents), + is_package, + ) + + +class CommonTests(metaclass=abc.ABCMeta): + """ + Tests shared by test_open, test_path, and test_read. + """ + + @abc.abstractmethod + def execute(self, package, path): + """ + Call the pertinent legacy API function (e.g. open_text, path) + on package and path. + """ + + def test_package_name(self): + """ + Passing in the package name should succeed. + """ + self.execute(data01.__name__, 'utf-8.file') + + def test_package_object(self): + """ + Passing in the package itself should succeed. + """ + self.execute(data01, 'utf-8.file') + + def test_string_path(self): + """ + Passing in a string for the path should succeed. + """ + path = 'utf-8.file' + self.execute(data01, path) + + def test_pathlib_path(self): + """ + Passing in a pathlib.PurePath object for the path should succeed. + """ + path = pathlib.PurePath('utf-8.file') + self.execute(data01, path) + + def test_importing_module_as_side_effect(self): + """ + The anchor package can already be imported. + """ + del sys.modules[data01.__name__] + self.execute(data01.__name__, 'utf-8.file') + + def test_missing_path(self): + """ + Attempting to open or read or request the path for a + non-existent path should succeed if open_resource + can return a viable data stream. + """ + bytes_data = io.BytesIO(b'Hello, world!') + package = create_package(file=bytes_data, path=FileNotFoundError()) + self.execute(package, 'utf-8.file') + self.assertEqual(package.__loader__._path, 'utf-8.file') + + def test_extant_path(self): + # Attempting to open or read or request the path when the + # path does exist should still succeed. Does not assert + # anything about the result. + bytes_data = io.BytesIO(b'Hello, world!') + # any path that exists + path = __file__ + package = create_package(file=bytes_data, path=path) + self.execute(package, 'utf-8.file') + self.assertEqual(package.__loader__._path, 'utf-8.file') + + def test_useless_loader(self): + package = create_package(file=FileNotFoundError(), path=FileNotFoundError()) + with self.assertRaises(FileNotFoundError): + self.execute(package, 'utf-8.file') + + +class ZipSetupBase: + ZIP_MODULE = 'data01' + + def setUp(self): + self.fixtures = contextlib.ExitStack() + self.addCleanup(self.fixtures.close) + + modules = import_helper.modules_setup() + self.addCleanup(import_helper.modules_cleanup, *modules) + + temp_dir = self.fixtures.enter_context(os_helper.temp_dir()) + modules = pathlib.Path(temp_dir) / 'zipped modules.zip' + src_path = pathlib.Path(__file__).parent.joinpath(self.ZIP_MODULE) + self.fixtures.enter_context( + import_helper.DirsOnSysPath(str(zip_.make_zip_file(src_path, modules))) + ) + + self.data = importlib.import_module(self.ZIP_MODULE) + + +class ZipSetup(ZipSetupBase): + pass diff --git a/libs/common/importlib_resources/tests/zip.py b/libs/common/importlib_resources/tests/zip.py new file mode 100644 index 00000000..962195a9 --- /dev/null +++ b/libs/common/importlib_resources/tests/zip.py @@ -0,0 +1,32 @@ +""" +Generate zip test data files. +""" + +import contextlib +import os +import pathlib +import zipfile + +import zipp + + +def make_zip_file(src, dst): + """ + Zip the files in src into a new zipfile at dst. + """ + with zipfile.ZipFile(dst, 'w') as zf: + for src_path, rel in walk(src): + dst_name = src.name / pathlib.PurePosixPath(rel.as_posix()) + zf.write(src_path, dst_name) + zipp.CompleteDirs.inject(zf) + return dst + + +def walk(datapath): + for dirpath, dirnames, filenames in os.walk(datapath): + with contextlib.suppress(ValueError): + dirnames.remove('__pycache__') + for filename in filenames: + res = pathlib.Path(dirpath) / filename + rel = res.relative_to(datapath) + yield res, rel diff --git a/libs/common/jaraco/context.py b/libs/common/jaraco/context.py new file mode 100644 index 00000000..b0d1ef37 --- /dev/null +++ b/libs/common/jaraco/context.py @@ -0,0 +1,288 @@ +import os +import subprocess +import contextlib +import functools +import tempfile +import shutil +import operator +import warnings + + +@contextlib.contextmanager +def pushd(dir): + """ + >>> tmp_path = getfixture('tmp_path') + >>> with pushd(tmp_path): + ... assert os.getcwd() == os.fspath(tmp_path) + >>> assert os.getcwd() != os.fspath(tmp_path) + """ + + orig = os.getcwd() + os.chdir(dir) + try: + yield dir + finally: + os.chdir(orig) + + +@contextlib.contextmanager +def tarball_context(url, target_dir=None, runner=None, pushd=pushd): + """ + Get a tarball, extract it, change to that directory, yield, then + clean up. + `runner` is the function to invoke commands. + `pushd` is a context manager for changing the directory. + """ + if target_dir is None: + target_dir = os.path.basename(url).replace('.tar.gz', '').replace('.tgz', '') + if runner is None: + runner = functools.partial(subprocess.check_call, shell=True) + else: + warnings.warn("runner parameter is deprecated", DeprecationWarning) + # In the tar command, use --strip-components=1 to strip the first path and + # then + # use -C to cause the files to be extracted to {target_dir}. This ensures + # that we always know where the files were extracted. + runner('mkdir {target_dir}'.format(**vars())) + try: + getter = 'wget {url} -O -' + extract = 'tar x{compression} --strip-components=1 -C {target_dir}' + cmd = ' | '.join((getter, extract)) + runner(cmd.format(compression=infer_compression(url), **vars())) + with pushd(target_dir): + yield target_dir + finally: + runner('rm -Rf {target_dir}'.format(**vars())) + + +def infer_compression(url): + """ + Given a URL or filename, infer the compression code for tar. + + >>> infer_compression('http://foo/bar.tar.gz') + 'z' + >>> infer_compression('http://foo/bar.tgz') + 'z' + >>> infer_compression('file.bz') + 'j' + >>> infer_compression('file.xz') + 'J' + """ + # cheat and just assume it's the last two characters + compression_indicator = url[-2:] + mapping = dict(gz='z', bz='j', xz='J') + # Assume 'z' (gzip) if no match + return mapping.get(compression_indicator, 'z') + + +@contextlib.contextmanager +def temp_dir(remover=shutil.rmtree): + """ + Create a temporary directory context. Pass a custom remover + to override the removal behavior. + + >>> import pathlib + >>> with temp_dir() as the_dir: + ... assert os.path.isdir(the_dir) + ... _ = pathlib.Path(the_dir).joinpath('somefile').write_text('contents') + >>> assert not os.path.exists(the_dir) + """ + temp_dir = tempfile.mkdtemp() + try: + yield temp_dir + finally: + remover(temp_dir) + + +@contextlib.contextmanager +def repo_context(url, branch=None, quiet=True, dest_ctx=temp_dir): + """ + Check out the repo indicated by url. + + If dest_ctx is supplied, it should be a context manager + to yield the target directory for the check out. + """ + exe = 'git' if 'git' in url else 'hg' + with dest_ctx() as repo_dir: + cmd = [exe, 'clone', url, repo_dir] + if branch: + cmd.extend(['--branch', branch]) + devnull = open(os.path.devnull, 'w') + stdout = devnull if quiet else None + subprocess.check_call(cmd, stdout=stdout) + yield repo_dir + + +@contextlib.contextmanager +def null(): + """ + A null context suitable to stand in for a meaningful context. + + >>> with null() as value: + ... assert value is None + """ + yield + + +class ExceptionTrap: + """ + A context manager that will catch certain exceptions and provide an + indication they occurred. + + >>> with ExceptionTrap() as trap: + ... raise Exception() + >>> bool(trap) + True + + >>> with ExceptionTrap() as trap: + ... pass + >>> bool(trap) + False + + >>> with ExceptionTrap(ValueError) as trap: + ... raise ValueError("1 + 1 is not 3") + >>> bool(trap) + True + >>> trap.value + ValueError('1 + 1 is not 3') + >>> trap.tb + + + >>> with ExceptionTrap(ValueError) as trap: + ... raise Exception() + Traceback (most recent call last): + ... + Exception + + >>> bool(trap) + False + """ + + exc_info = None, None, None + + def __init__(self, exceptions=(Exception,)): + self.exceptions = exceptions + + def __enter__(self): + return self + + @property + def type(self): + return self.exc_info[0] + + @property + def value(self): + return self.exc_info[1] + + @property + def tb(self): + return self.exc_info[2] + + def __exit__(self, *exc_info): + type = exc_info[0] + matches = type and issubclass(type, self.exceptions) + if matches: + self.exc_info = exc_info + return matches + + def __bool__(self): + return bool(self.type) + + def raises(self, func, *, _test=bool): + """ + Wrap func and replace the result with the truth + value of the trap (True if an exception occurred). + + First, give the decorator an alias to support Python 3.8 + Syntax. + + >>> raises = ExceptionTrap(ValueError).raises + + Now decorate a function that always fails. + + >>> @raises + ... def fail(): + ... raise ValueError('failed') + >>> fail() + True + """ + + @functools.wraps(func) + def wrapper(*args, **kwargs): + with ExceptionTrap(self.exceptions) as trap: + func(*args, **kwargs) + return _test(trap) + + return wrapper + + def passes(self, func): + """ + Wrap func and replace the result with the truth + value of the trap (True if no exception). + + First, give the decorator an alias to support Python 3.8 + Syntax. + + >>> passes = ExceptionTrap(ValueError).passes + + Now decorate a function that always fails. + + >>> @passes + ... def fail(): + ... raise ValueError('failed') + + >>> fail() + False + """ + return self.raises(func, _test=operator.not_) + + +class suppress(contextlib.suppress, contextlib.ContextDecorator): + """ + A version of contextlib.suppress with decorator support. + + >>> @suppress(KeyError) + ... def key_error(): + ... {}[''] + >>> key_error() + """ + + +class on_interrupt(contextlib.ContextDecorator): + """ + Replace a KeyboardInterrupt with SystemExit(1) + + >>> def do_interrupt(): + ... raise KeyboardInterrupt() + >>> on_interrupt('error')(do_interrupt)() + Traceback (most recent call last): + ... + SystemExit: 1 + >>> on_interrupt('error', code=255)(do_interrupt)() + Traceback (most recent call last): + ... + SystemExit: 255 + >>> on_interrupt('suppress')(do_interrupt)() + >>> with __import__('pytest').raises(KeyboardInterrupt): + ... on_interrupt('ignore')(do_interrupt)() + """ + + def __init__( + self, + action='error', + # py3.7 compat + # /, + code=1, + ): + self.action = action + self.code = code + + def __enter__(self): + return self + + def __exit__(self, exctype, excinst, exctb): + if exctype is not KeyboardInterrupt or self.action == 'ignore': + return + elif self.action == 'error': + raise SystemExit(self.code) from excinst + return self.action == 'suppress' diff --git a/libs/common/jaraco/functools/__init__.py b/libs/common/jaraco/functools/__init__.py new file mode 100644 index 00000000..ca6c22fa --- /dev/null +++ b/libs/common/jaraco/functools/__init__.py @@ -0,0 +1,633 @@ +import collections.abc +import functools +import inspect +import itertools +import operator +import time +import types +import warnings + +import more_itertools + + +def compose(*funcs): + """ + Compose any number of unary functions into a single unary function. + + >>> import textwrap + >>> expected = str.strip(textwrap.dedent(compose.__doc__)) + >>> strip_and_dedent = compose(str.strip, textwrap.dedent) + >>> strip_and_dedent(compose.__doc__) == expected + True + + Compose also allows the innermost function to take arbitrary arguments. + + >>> round_three = lambda x: round(x, ndigits=3) + >>> f = compose(round_three, int.__truediv__) + >>> [f(3*x, x+1) for x in range(1,10)] + [1.5, 2.0, 2.25, 2.4, 2.5, 2.571, 2.625, 2.667, 2.7] + """ + + def compose_two(f1, f2): + return lambda *args, **kwargs: f1(f2(*args, **kwargs)) + + return functools.reduce(compose_two, funcs) + + +def once(func): + """ + Decorate func so it's only ever called the first time. + + This decorator can ensure that an expensive or non-idempotent function + will not be expensive on subsequent calls and is idempotent. + + >>> add_three = once(lambda a: a+3) + >>> add_three(3) + 6 + >>> add_three(9) + 6 + >>> add_three('12') + 6 + + To reset the stored value, simply clear the property ``saved_result``. + + >>> del add_three.saved_result + >>> add_three(9) + 12 + >>> add_three(8) + 12 + + Or invoke 'reset()' on it. + + >>> add_three.reset() + >>> add_three(-3) + 0 + >>> add_three(0) + 0 + """ + + @functools.wraps(func) + def wrapper(*args, **kwargs): + if not hasattr(wrapper, 'saved_result'): + wrapper.saved_result = func(*args, **kwargs) + return wrapper.saved_result + + wrapper.reset = lambda: vars(wrapper).__delitem__('saved_result') + return wrapper + + +def method_cache(method, cache_wrapper=functools.lru_cache()): + """ + Wrap lru_cache to support storing the cache data in the object instances. + + Abstracts the common paradigm where the method explicitly saves an + underscore-prefixed protected property on first call and returns that + subsequently. + + >>> class MyClass: + ... calls = 0 + ... + ... @method_cache + ... def method(self, value): + ... self.calls += 1 + ... return value + + >>> a = MyClass() + >>> a.method(3) + 3 + >>> for x in range(75): + ... res = a.method(x) + >>> a.calls + 75 + + Note that the apparent behavior will be exactly like that of lru_cache + except that the cache is stored on each instance, so values in one + instance will not flush values from another, and when an instance is + deleted, so are the cached values for that instance. + + >>> b = MyClass() + >>> for x in range(35): + ... res = b.method(x) + >>> b.calls + 35 + >>> a.method(0) + 0 + >>> a.calls + 75 + + Note that if method had been decorated with ``functools.lru_cache()``, + a.calls would have been 76 (due to the cached value of 0 having been + flushed by the 'b' instance). + + Clear the cache with ``.cache_clear()`` + + >>> a.method.cache_clear() + + Same for a method that hasn't yet been called. + + >>> c = MyClass() + >>> c.method.cache_clear() + + Another cache wrapper may be supplied: + + >>> cache = functools.lru_cache(maxsize=2) + >>> MyClass.method2 = method_cache(lambda self: 3, cache_wrapper=cache) + >>> a = MyClass() + >>> a.method2() + 3 + + Caution - do not subsequently wrap the method with another decorator, such + as ``@property``, which changes the semantics of the function. + + See also + http://code.activestate.com/recipes/577452-a-memoize-decorator-for-instance-methods/ + for another implementation and additional justification. + """ + + def wrapper(self, *args, **kwargs): + # it's the first call, replace the method with a cached, bound method + bound_method = types.MethodType(method, self) + cached_method = cache_wrapper(bound_method) + setattr(self, method.__name__, cached_method) + return cached_method(*args, **kwargs) + + # Support cache clear even before cache has been created. + wrapper.cache_clear = lambda: None + + return _special_method_cache(method, cache_wrapper) or wrapper + + +def _special_method_cache(method, cache_wrapper): + """ + Because Python treats special methods differently, it's not + possible to use instance attributes to implement the cached + methods. + + Instead, install the wrapper method under a different name + and return a simple proxy to that wrapper. + + https://github.com/jaraco/jaraco.functools/issues/5 + """ + name = method.__name__ + special_names = '__getattr__', '__getitem__' + + if name not in special_names: + return None + + wrapper_name = '__cached' + name + + def proxy(self, /, *args, **kwargs): + if wrapper_name not in vars(self): + bound = types.MethodType(method, self) + cache = cache_wrapper(bound) + setattr(self, wrapper_name, cache) + else: + cache = getattr(self, wrapper_name) + return cache(*args, **kwargs) + + return proxy + + +def apply(transform): + """ + Decorate a function with a transform function that is + invoked on results returned from the decorated function. + + >>> @apply(reversed) + ... def get_numbers(start): + ... "doc for get_numbers" + ... return range(start, start+3) + >>> list(get_numbers(4)) + [6, 5, 4] + >>> get_numbers.__doc__ + 'doc for get_numbers' + """ + + def wrap(func): + return functools.wraps(func)(compose(transform, func)) + + return wrap + + +def result_invoke(action): + r""" + Decorate a function with an action function that is + invoked on the results returned from the decorated + function (for its side effect), then return the original + result. + + >>> @result_invoke(print) + ... def add_two(a, b): + ... return a + b + >>> x = add_two(2, 3) + 5 + >>> x + 5 + """ + + def wrap(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + action(result) + return result + + return wrapper + + return wrap + + +def invoke(f, /, *args, **kwargs): + """ + Call a function for its side effect after initialization. + + The benefit of using the decorator instead of simply invoking a function + after defining it is that it makes explicit the author's intent for the + function to be called immediately. Whereas if one simply calls the + function immediately, it's less obvious if that was intentional or + incidental. It also avoids repeating the name - the two actions, defining + the function and calling it immediately are modeled separately, but linked + by the decorator construct. + + The benefit of having a function construct (opposed to just invoking some + behavior inline) is to serve as a scope in which the behavior occurs. It + avoids polluting the global namespace with local variables, provides an + anchor on which to attach documentation (docstring), keeps the behavior + logically separated (instead of conceptually separated or not separated at + all), and provides potential to re-use the behavior for testing or other + purposes. + + This function is named as a pithy way to communicate, "call this function + primarily for its side effect", or "while defining this function, also + take it aside and call it". It exists because there's no Python construct + for "define and call" (nor should there be, as decorators serve this need + just fine). The behavior happens immediately and synchronously. + + >>> @invoke + ... def func(): print("called") + called + >>> func() + called + + Use functools.partial to pass parameters to the initial call + + >>> @functools.partial(invoke, name='bingo') + ... def func(name): print('called with', name) + called with bingo + """ + f(*args, **kwargs) + return f + + +class Throttler: + """Rate-limit a function (or other callable).""" + + def __init__(self, func, max_rate=float('Inf')): + if isinstance(func, Throttler): + func = func.func + self.func = func + self.max_rate = max_rate + self.reset() + + def reset(self): + self.last_called = 0 + + def __call__(self, *args, **kwargs): + self._wait() + return self.func(*args, **kwargs) + + def _wait(self): + """Ensure at least 1/max_rate seconds from last call.""" + elapsed = time.time() - self.last_called + must_wait = 1 / self.max_rate - elapsed + time.sleep(max(0, must_wait)) + self.last_called = time.time() + + def __get__(self, obj, owner=None): + return first_invoke(self._wait, functools.partial(self.func, obj)) + + +def first_invoke(func1, func2): + """ + Return a function that when invoked will invoke func1 without + any parameters (for its side effect) and then invoke func2 + with whatever parameters were passed, returning its result. + """ + + def wrapper(*args, **kwargs): + func1() + return func2(*args, **kwargs) + + return wrapper + + +method_caller = first_invoke( + lambda: warnings.warn( + '`jaraco.functools.method_caller` is deprecated, ' + 'use `operator.methodcaller` instead', + DeprecationWarning, + stacklevel=3, + ), + operator.methodcaller, +) + + +def retry_call(func, cleanup=lambda: None, retries=0, trap=()): + """ + Given a callable func, trap the indicated exceptions + for up to 'retries' times, invoking cleanup on the + exception. On the final attempt, allow any exceptions + to propagate. + """ + attempts = itertools.count() if retries == float('inf') else range(retries) + for _ in attempts: + try: + return func() + except trap: + cleanup() + + return func() + + +def retry(*r_args, **r_kwargs): + """ + Decorator wrapper for retry_call. Accepts arguments to retry_call + except func and then returns a decorator for the decorated function. + + Ex: + + >>> @retry(retries=3) + ... def my_func(a, b): + ... "this is my funk" + ... print(a, b) + >>> my_func.__doc__ + 'this is my funk' + """ + + def decorate(func): + @functools.wraps(func) + def wrapper(*f_args, **f_kwargs): + bound = functools.partial(func, *f_args, **f_kwargs) + return retry_call(bound, *r_args, **r_kwargs) + + return wrapper + + return decorate + + +def print_yielded(func): + """ + Convert a generator into a function that prints all yielded elements. + + >>> @print_yielded + ... def x(): + ... yield 3; yield None + >>> x() + 3 + None + """ + print_all = functools.partial(map, print) + print_results = compose(more_itertools.consume, print_all, func) + return functools.wraps(func)(print_results) + + +def pass_none(func): + """ + Wrap func so it's not called if its first param is None. + + >>> print_text = pass_none(print) + >>> print_text('text') + text + >>> print_text(None) + """ + + @functools.wraps(func) + def wrapper(param, /, *args, **kwargs): + if param is not None: + return func(param, *args, **kwargs) + return None + + return wrapper + + +def assign_params(func, namespace): + """ + Assign parameters from namespace where func solicits. + + >>> def func(x, y=3): + ... print(x, y) + >>> assigned = assign_params(func, dict(x=2, z=4)) + >>> assigned() + 2 3 + + The usual errors are raised if a function doesn't receive + its required parameters: + + >>> assigned = assign_params(func, dict(y=3, z=4)) + >>> assigned() + Traceback (most recent call last): + TypeError: func() ...argument... + + It even works on methods: + + >>> class Handler: + ... def meth(self, arg): + ... print(arg) + >>> assign_params(Handler().meth, dict(arg='crystal', foo='clear'))() + crystal + """ + sig = inspect.signature(func) + params = sig.parameters.keys() + call_ns = {k: namespace[k] for k in params if k in namespace} + return functools.partial(func, **call_ns) + + +def save_method_args(method): + """ + Wrap a method such that when it is called, the args and kwargs are + saved on the method. + + >>> class MyClass: + ... @save_method_args + ... def method(self, a, b): + ... print(a, b) + >>> my_ob = MyClass() + >>> my_ob.method(1, 2) + 1 2 + >>> my_ob._saved_method.args + (1, 2) + >>> my_ob._saved_method.kwargs + {} + >>> my_ob.method(a=3, b='foo') + 3 foo + >>> my_ob._saved_method.args + () + >>> my_ob._saved_method.kwargs == dict(a=3, b='foo') + True + + The arguments are stored on the instance, allowing for + different instance to save different args. + + >>> your_ob = MyClass() + >>> your_ob.method({str('x'): 3}, b=[4]) + {'x': 3} [4] + >>> your_ob._saved_method.args + ({'x': 3},) + >>> my_ob._saved_method.args + () + """ + args_and_kwargs = collections.namedtuple('args_and_kwargs', 'args kwargs') + + @functools.wraps(method) + def wrapper(self, /, *args, **kwargs): + attr_name = '_saved_' + method.__name__ + attr = args_and_kwargs(args, kwargs) + setattr(self, attr_name, attr) + return method(self, *args, **kwargs) + + return wrapper + + +def except_(*exceptions, replace=None, use=None): + """ + Replace the indicated exceptions, if raised, with the indicated + literal replacement or evaluated expression (if present). + + >>> safe_int = except_(ValueError)(int) + >>> safe_int('five') + >>> safe_int('5') + 5 + + Specify a literal replacement with ``replace``. + + >>> safe_int_r = except_(ValueError, replace=0)(int) + >>> safe_int_r('five') + 0 + + Provide an expression to ``use`` to pass through particular parameters. + + >>> safe_int_pt = except_(ValueError, use='args[0]')(int) + >>> safe_int_pt('five') + 'five' + + """ + + def decorate(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except exceptions: + try: + return eval(use) + except TypeError: + return replace + + return wrapper + + return decorate + + +def identity(x): + """ + Return the argument. + + >>> o = object() + >>> identity(o) is o + True + """ + return x + + +def bypass_when(check, *, _op=identity): + """ + Decorate a function to return its parameter when ``check``. + + >>> bypassed = [] # False + + >>> @bypass_when(bypassed) + ... def double(x): + ... return x * 2 + >>> double(2) + 4 + >>> bypassed[:] = [object()] # True + >>> double(2) + 2 + """ + + def decorate(func): + @functools.wraps(func) + def wrapper(param, /): + return param if _op(check) else func(param) + + return wrapper + + return decorate + + +def bypass_unless(check): + """ + Decorate a function to return its parameter unless ``check``. + + >>> enabled = [object()] # True + + >>> @bypass_unless(enabled) + ... def double(x): + ... return x * 2 + >>> double(2) + 4 + >>> del enabled[:] # False + >>> double(2) + 2 + """ + return bypass_when(check, _op=operator.not_) + + +@functools.singledispatch +def _splat_inner(args, func): + """Splat args to func.""" + return func(*args) + + +@_splat_inner.register +def _(args: collections.abc.Mapping, func): + """Splat kargs to func as kwargs.""" + return func(**args) + + +def splat(func): + """ + Wrap func to expect its parameters to be passed positionally in a tuple. + + Has a similar effect to that of ``itertools.starmap`` over + simple ``map``. + + >>> pairs = [(-1, 1), (0, 2)] + >>> more_itertools.consume(itertools.starmap(print, pairs)) + -1 1 + 0 2 + >>> more_itertools.consume(map(splat(print), pairs)) + -1 1 + 0 2 + + The approach generalizes to other iterators that don't have a "star" + equivalent, such as a "starfilter". + + >>> list(filter(splat(operator.add), pairs)) + [(0, 2)] + + Splat also accepts a mapping argument. + + >>> def is_nice(msg, code): + ... return "smile" in msg or code == 0 + >>> msgs = [ + ... dict(msg='smile!', code=20), + ... dict(msg='error :(', code=1), + ... dict(msg='unknown', code=0), + ... ] + >>> for msg in filter(splat(is_nice), msgs): + ... print(msg) + {'msg': 'smile!', 'code': 20} + {'msg': 'unknown', 'code': 0} + """ + return functools.wraps(func)(functools.partial(_splat_inner, func=func)) diff --git a/libs/common/jaraco/functools/__init__.pyi b/libs/common/jaraco/functools/__init__.pyi new file mode 100644 index 00000000..c2b9ab17 --- /dev/null +++ b/libs/common/jaraco/functools/__init__.pyi @@ -0,0 +1,128 @@ +from collections.abc import Callable, Hashable, Iterator +from functools import partial +from operator import methodcaller +import sys +from typing import ( + Any, + Generic, + Protocol, + TypeVar, + overload, +) + +if sys.version_info >= (3, 10): + from typing import Concatenate, ParamSpec +else: + from typing_extensions import Concatenate, ParamSpec + +_P = ParamSpec('_P') +_R = TypeVar('_R') +_T = TypeVar('_T') +_R1 = TypeVar('_R1') +_R2 = TypeVar('_R2') +_V = TypeVar('_V') +_S = TypeVar('_S') +_R_co = TypeVar('_R_co', covariant=True) + +class _OnceCallable(Protocol[_P, _R]): + saved_result: _R + reset: Callable[[], None] + def __call__(self, *args: _P.args, **kwargs: _P.kwargs) -> _R: ... + +class _ProxyMethodCacheWrapper(Protocol[_R_co]): + cache_clear: Callable[[], None] + def __call__(self, *args: Hashable, **kwargs: Hashable) -> _R_co: ... + +class _MethodCacheWrapper(Protocol[_R_co]): + def cache_clear(self) -> None: ... + def __call__(self, *args: Hashable, **kwargs: Hashable) -> _R_co: ... + +# `compose()` overloads below will cover most use cases. + +@overload +def compose( + __func1: Callable[[_R], _T], + __func2: Callable[_P, _R], + /, +) -> Callable[_P, _T]: ... +@overload +def compose( + __func1: Callable[[_R], _T], + __func2: Callable[[_R1], _R], + __func3: Callable[_P, _R1], + /, +) -> Callable[_P, _T]: ... +@overload +def compose( + __func1: Callable[[_R], _T], + __func2: Callable[[_R2], _R], + __func3: Callable[[_R1], _R2], + __func4: Callable[_P, _R1], + /, +) -> Callable[_P, _T]: ... +def once(func: Callable[_P, _R]) -> _OnceCallable[_P, _R]: ... +def method_cache( + method: Callable[..., _R], + cache_wrapper: Callable[[Callable[..., _R]], _MethodCacheWrapper[_R]] = ..., +) -> _MethodCacheWrapper[_R] | _ProxyMethodCacheWrapper[_R]: ... +def apply( + transform: Callable[[_R], _T] +) -> Callable[[Callable[_P, _R]], Callable[_P, _T]]: ... +def result_invoke( + action: Callable[[_R], Any] +) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]: ... +def invoke( + f: Callable[_P, _R], /, *args: _P.args, **kwargs: _P.kwargs +) -> Callable[_P, _R]: ... +def call_aside( + f: Callable[_P, _R], *args: _P.args, **kwargs: _P.kwargs +) -> Callable[_P, _R]: ... + +class Throttler(Generic[_R]): + last_called: float + func: Callable[..., _R] + max_rate: float + def __init__( + self, func: Callable[..., _R] | Throttler[_R], max_rate: float = ... + ) -> None: ... + def reset(self) -> None: ... + def __call__(self, *args: Any, **kwargs: Any) -> _R: ... + def __get__(self, obj: Any, owner: type[Any] | None = ...) -> Callable[..., _R]: ... + +def first_invoke( + func1: Callable[..., Any], func2: Callable[_P, _R] +) -> Callable[_P, _R]: ... + +method_caller: Callable[..., methodcaller] + +def retry_call( + func: Callable[..., _R], + cleanup: Callable[..., None] = ..., + retries: int | float = ..., + trap: type[BaseException] | tuple[type[BaseException], ...] = ..., +) -> _R: ... +def retry( + cleanup: Callable[..., None] = ..., + retries: int | float = ..., + trap: type[BaseException] | tuple[type[BaseException], ...] = ..., +) -> Callable[[Callable[..., _R]], Callable[..., _R]]: ... +def print_yielded(func: Callable[_P, Iterator[Any]]) -> Callable[_P, None]: ... +def pass_none( + func: Callable[Concatenate[_T, _P], _R] +) -> Callable[Concatenate[_T, _P], _R]: ... +def assign_params( + func: Callable[..., _R], namespace: dict[str, Any] +) -> partial[_R]: ... +def save_method_args( + method: Callable[Concatenate[_S, _P], _R] +) -> Callable[Concatenate[_S, _P], _R]: ... +def except_( + *exceptions: type[BaseException], replace: Any = ..., use: Any = ... +) -> Callable[[Callable[_P, Any]], Callable[_P, Any]]: ... +def identity(x: _T) -> _T: ... +def bypass_when( + check: _V, *, _op: Callable[[_V], Any] = ... +) -> Callable[[Callable[[_T], _R]], Callable[[_T], _T | _R]]: ... +def bypass_unless( + check: Any, +) -> Callable[[Callable[[_T], _R]], Callable[[_T], _T | _R]]: ... diff --git a/libs/common/jaraco/functools/py.typed b/libs/common/jaraco/functools/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/libs/common/jaraco/text/Lorem ipsum.txt b/libs/common/jaraco/text/Lorem ipsum.txt new file mode 100644 index 00000000..986f944b --- /dev/null +++ b/libs/common/jaraco/text/Lorem ipsum.txt @@ -0,0 +1,2 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. +Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra, est eros bibendum elit, nec luctus magna felis sollicitudin mauris. Integer in mauris eu nibh euismod gravida. Duis ac tellus et risus vulputate vehicula. Donec lobortis risus a elit. Etiam tempor. Ut ullamcorper, ligula eu tempor congue, eros est euismod turpis, id tincidunt sapien risus a quam. Maecenas fermentum consequat mi. Donec fermentum. Pellentesque malesuada nulla a mi. Duis sapien sem, aliquet nec, commodo eget, consequat quis, neque. Aliquam faucibus, elit ut dictum aliquet, felis nisl adipiscing sapien, sed malesuada diam lacus eget erat. Cras mollis scelerisque nunc. Nullam arcu. Aliquam consequat. Curabitur augue lorem, dapibus quis, laoreet et, pretium ac, nisi. Aenean magna nisl, mollis quis, molestie eu, feugiat in, orci. In hac habitasse platea dictumst. diff --git a/libs/common/jaraco/text/__init__.py b/libs/common/jaraco/text/__init__.py new file mode 100644 index 00000000..0fabd0c3 --- /dev/null +++ b/libs/common/jaraco/text/__init__.py @@ -0,0 +1,624 @@ +import re +import itertools +import textwrap +import functools + +try: + from importlib.resources import files # type: ignore +except ImportError: # pragma: nocover + from importlib_resources import files # type: ignore + +from jaraco.functools import compose, method_cache +from jaraco.context import ExceptionTrap + + +def substitution(old, new): + """ + Return a function that will perform a substitution on a string + """ + return lambda s: s.replace(old, new) + + +def multi_substitution(*substitutions): + """ + Take a sequence of pairs specifying substitutions, and create + a function that performs those substitutions. + + >>> multi_substitution(('foo', 'bar'), ('bar', 'baz'))('foo') + 'baz' + """ + substitutions = itertools.starmap(substitution, substitutions) + # compose function applies last function first, so reverse the + # substitutions to get the expected order. + substitutions = reversed(tuple(substitutions)) + return compose(*substitutions) + + +class FoldedCase(str): + """ + A case insensitive string class; behaves just like str + except compares equal when the only variation is case. + + >>> s = FoldedCase('hello world') + + >>> s == 'Hello World' + True + + >>> 'Hello World' == s + True + + >>> s != 'Hello World' + False + + >>> s.index('O') + 4 + + >>> s.split('O') + ['hell', ' w', 'rld'] + + >>> sorted(map(FoldedCase, ['GAMMA', 'alpha', 'Beta'])) + ['alpha', 'Beta', 'GAMMA'] + + Sequence membership is straightforward. + + >>> "Hello World" in [s] + True + >>> s in ["Hello World"] + True + + Allows testing for set inclusion, but candidate and elements + must both be folded. + + >>> FoldedCase("Hello World") in {s} + True + >>> s in {FoldedCase("Hello World")} + True + + String inclusion works as long as the FoldedCase object + is on the right. + + >>> "hello" in FoldedCase("Hello World") + True + + But not if the FoldedCase object is on the left: + + >>> FoldedCase('hello') in 'Hello World' + False + + In that case, use ``in_``: + + >>> FoldedCase('hello').in_('Hello World') + True + + >>> FoldedCase('hello') > FoldedCase('Hello') + False + + >>> FoldedCase('ß') == FoldedCase('ss') + True + """ + + def __lt__(self, other): + return self.casefold() < other.casefold() + + def __gt__(self, other): + return self.casefold() > other.casefold() + + def __eq__(self, other): + return self.casefold() == other.casefold() + + def __ne__(self, other): + return self.casefold() != other.casefold() + + def __hash__(self): + return hash(self.casefold()) + + def __contains__(self, other): + return super().casefold().__contains__(other.casefold()) + + def in_(self, other): + "Does self appear in other?" + return self in FoldedCase(other) + + # cache casefold since it's likely to be called frequently. + @method_cache + def casefold(self): + return super().casefold() + + def index(self, sub): + return self.casefold().index(sub.casefold()) + + def split(self, splitter=' ', maxsplit=0): + pattern = re.compile(re.escape(splitter), re.I) + return pattern.split(self, maxsplit) + + +# Python 3.8 compatibility +_unicode_trap = ExceptionTrap(UnicodeDecodeError) + + +@_unicode_trap.passes +def is_decodable(value): + r""" + Return True if the supplied value is decodable (using the default + encoding). + + >>> is_decodable(b'\xff') + False + >>> is_decodable(b'\x32') + True + """ + value.decode() + + +def is_binary(value): + r""" + Return True if the value appears to be binary (that is, it's a byte + string and isn't decodable). + + >>> is_binary(b'\xff') + True + >>> is_binary('\xff') + False + """ + return isinstance(value, bytes) and not is_decodable(value) + + +def trim(s): + r""" + Trim something like a docstring to remove the whitespace that + is common due to indentation and formatting. + + >>> trim("\n\tfoo = bar\n\t\tbar = baz\n") + 'foo = bar\n\tbar = baz' + """ + return textwrap.dedent(s).strip() + + +def wrap(s): + """ + Wrap lines of text, retaining existing newlines as + paragraph markers. + + >>> print(wrap(lorem_ipsum)) + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do + eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad + minim veniam, quis nostrud exercitation ullamco laboris nisi ut + aliquip ex ea commodo consequat. Duis aute irure dolor in + reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla + pariatur. Excepteur sint occaecat cupidatat non proident, sunt in + culpa qui officia deserunt mollit anim id est laborum. + + Curabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam + varius, turpis et commodo pharetra, est eros bibendum elit, nec luctus + magna felis sollicitudin mauris. Integer in mauris eu nibh euismod + gravida. Duis ac tellus et risus vulputate vehicula. Donec lobortis + risus a elit. Etiam tempor. Ut ullamcorper, ligula eu tempor congue, + eros est euismod turpis, id tincidunt sapien risus a quam. Maecenas + fermentum consequat mi. Donec fermentum. Pellentesque malesuada nulla + a mi. Duis sapien sem, aliquet nec, commodo eget, consequat quis, + neque. Aliquam faucibus, elit ut dictum aliquet, felis nisl adipiscing + sapien, sed malesuada diam lacus eget erat. Cras mollis scelerisque + nunc. Nullam arcu. Aliquam consequat. Curabitur augue lorem, dapibus + quis, laoreet et, pretium ac, nisi. Aenean magna nisl, mollis quis, + molestie eu, feugiat in, orci. In hac habitasse platea dictumst. + """ + paragraphs = s.splitlines() + wrapped = ('\n'.join(textwrap.wrap(para)) for para in paragraphs) + return '\n\n'.join(wrapped) + + +def unwrap(s): + r""" + Given a multi-line string, return an unwrapped version. + + >>> wrapped = wrap(lorem_ipsum) + >>> wrapped.count('\n') + 20 + >>> unwrapped = unwrap(wrapped) + >>> unwrapped.count('\n') + 1 + >>> print(unwrapped) + Lorem ipsum dolor sit amet, consectetur adipiscing ... + Curabitur pretium tincidunt lacus. Nulla gravida orci ... + + """ + paragraphs = re.split(r'\n\n+', s) + cleaned = (para.replace('\n', ' ') for para in paragraphs) + return '\n'.join(cleaned) + + +lorem_ipsum: str = ( + files(__name__).joinpath('Lorem ipsum.txt').read_text(encoding='utf-8') +) + + +class Splitter: + """object that will split a string with the given arguments for each call + + >>> s = Splitter(',') + >>> s('hello, world, this is your, master calling') + ['hello', ' world', ' this is your', ' master calling'] + """ + + def __init__(self, *args): + self.args = args + + def __call__(self, s): + return s.split(*self.args) + + +def indent(string, prefix=' ' * 4): + """ + >>> indent('foo') + ' foo' + """ + return prefix + string + + +class WordSet(tuple): + """ + Given an identifier, return the words that identifier represents, + whether in camel case, underscore-separated, etc. + + >>> WordSet.parse("camelCase") + ('camel', 'Case') + + >>> WordSet.parse("under_sep") + ('under', 'sep') + + Acronyms should be retained + + >>> WordSet.parse("firstSNL") + ('first', 'SNL') + + >>> WordSet.parse("you_and_I") + ('you', 'and', 'I') + + >>> WordSet.parse("A simple test") + ('A', 'simple', 'test') + + Multiple caps should not interfere with the first cap of another word. + + >>> WordSet.parse("myABCClass") + ('my', 'ABC', 'Class') + + The result is a WordSet, providing access to other forms. + + >>> WordSet.parse("myABCClass").underscore_separated() + 'my_ABC_Class' + + >>> WordSet.parse('a-command').camel_case() + 'ACommand' + + >>> WordSet.parse('someIdentifier').lowered().space_separated() + 'some identifier' + + Slices of the result should return another WordSet. + + >>> WordSet.parse('taken-out-of-context')[1:].underscore_separated() + 'out_of_context' + + >>> WordSet.from_class_name(WordSet()).lowered().space_separated() + 'word set' + + >>> example = WordSet.parse('figured it out') + >>> example.headless_camel_case() + 'figuredItOut' + >>> example.dash_separated() + 'figured-it-out' + + """ + + _pattern = re.compile('([A-Z]?[a-z]+)|([A-Z]+(?![a-z]))') + + def capitalized(self): + return WordSet(word.capitalize() for word in self) + + def lowered(self): + return WordSet(word.lower() for word in self) + + def camel_case(self): + return ''.join(self.capitalized()) + + def headless_camel_case(self): + words = iter(self) + first = next(words).lower() + new_words = itertools.chain((first,), WordSet(words).camel_case()) + return ''.join(new_words) + + def underscore_separated(self): + return '_'.join(self) + + def dash_separated(self): + return '-'.join(self) + + def space_separated(self): + return ' '.join(self) + + def trim_right(self, item): + """ + Remove the item from the end of the set. + + >>> WordSet.parse('foo bar').trim_right('foo') + ('foo', 'bar') + >>> WordSet.parse('foo bar').trim_right('bar') + ('foo',) + >>> WordSet.parse('').trim_right('bar') + () + """ + return self[:-1] if self and self[-1] == item else self + + def trim_left(self, item): + """ + Remove the item from the beginning of the set. + + >>> WordSet.parse('foo bar').trim_left('foo') + ('bar',) + >>> WordSet.parse('foo bar').trim_left('bar') + ('foo', 'bar') + >>> WordSet.parse('').trim_left('bar') + () + """ + return self[1:] if self and self[0] == item else self + + def trim(self, item): + """ + >>> WordSet.parse('foo bar').trim('foo') + ('bar',) + """ + return self.trim_left(item).trim_right(item) + + def __getitem__(self, item): + result = super().__getitem__(item) + if isinstance(item, slice): + result = WordSet(result) + return result + + @classmethod + def parse(cls, identifier): + matches = cls._pattern.finditer(identifier) + return WordSet(match.group(0) for match in matches) + + @classmethod + def from_class_name(cls, subject): + return cls.parse(subject.__class__.__name__) + + +# for backward compatibility +words = WordSet.parse + + +def simple_html_strip(s): + r""" + Remove HTML from the string `s`. + + >>> str(simple_html_strip('')) + '' + + >>> print(simple_html_strip('A stormy day in paradise')) + A stormy day in paradise + + >>> print(simple_html_strip('Somebody tell the truth.')) + Somebody tell the truth. + + >>> print(simple_html_strip('What about
\nmultiple lines?')) + What about + multiple lines? + """ + html_stripper = re.compile('()|(<[^>]*>)|([^<]+)', re.DOTALL) + texts = (match.group(3) or '' for match in html_stripper.finditer(s)) + return ''.join(texts) + + +class SeparatedValues(str): + """ + A string separated by a separator. Overrides __iter__ for getting + the values. + + >>> list(SeparatedValues('a,b,c')) + ['a', 'b', 'c'] + + Whitespace is stripped and empty values are discarded. + + >>> list(SeparatedValues(' a, b , c, ')) + ['a', 'b', 'c'] + """ + + separator = ',' + + def __iter__(self): + parts = self.split(self.separator) + return filter(None, (part.strip() for part in parts)) + + +class Stripper: + r""" + Given a series of lines, find the common prefix and strip it from them. + + >>> lines = [ + ... 'abcdefg\n', + ... 'abc\n', + ... 'abcde\n', + ... ] + >>> res = Stripper.strip_prefix(lines) + >>> res.prefix + 'abc' + >>> list(res.lines) + ['defg\n', '\n', 'de\n'] + + If no prefix is common, nothing should be stripped. + + >>> lines = [ + ... 'abcd\n', + ... '1234\n', + ... ] + >>> res = Stripper.strip_prefix(lines) + >>> res.prefix = '' + >>> list(res.lines) + ['abcd\n', '1234\n'] + """ + + def __init__(self, prefix, lines): + self.prefix = prefix + self.lines = map(self, lines) + + @classmethod + def strip_prefix(cls, lines): + prefix_lines, lines = itertools.tee(lines) + prefix = functools.reduce(cls.common_prefix, prefix_lines) + return cls(prefix, lines) + + def __call__(self, line): + if not self.prefix: + return line + null, prefix, rest = line.partition(self.prefix) + return rest + + @staticmethod + def common_prefix(s1, s2): + """ + Return the common prefix of two lines. + """ + index = min(len(s1), len(s2)) + while s1[:index] != s2[:index]: + index -= 1 + return s1[:index] + + +def remove_prefix(text, prefix): + """ + Remove the prefix from the text if it exists. + + >>> remove_prefix('underwhelming performance', 'underwhelming ') + 'performance' + + >>> remove_prefix('something special', 'sample') + 'something special' + """ + null, prefix, rest = text.rpartition(prefix) + return rest + + +def remove_suffix(text, suffix): + """ + Remove the suffix from the text if it exists. + + >>> remove_suffix('name.git', '.git') + 'name' + + >>> remove_suffix('something special', 'sample') + 'something special' + """ + rest, suffix, null = text.partition(suffix) + return rest + + +def normalize_newlines(text): + r""" + Replace alternate newlines with the canonical newline. + + >>> normalize_newlines('Lorem Ipsum\u2029') + 'Lorem Ipsum\n' + >>> normalize_newlines('Lorem Ipsum\r\n') + 'Lorem Ipsum\n' + >>> normalize_newlines('Lorem Ipsum\x85') + 'Lorem Ipsum\n' + """ + newlines = ['\r\n', '\r', '\n', '\u0085', '\u2028', '\u2029'] + pattern = '|'.join(newlines) + return re.sub(pattern, '\n', text) + + +def _nonblank(str): + return str and not str.startswith('#') + + +@functools.singledispatch +def yield_lines(iterable): + r""" + Yield valid lines of a string or iterable. + + >>> list(yield_lines('')) + [] + >>> list(yield_lines(['foo', 'bar'])) + ['foo', 'bar'] + >>> list(yield_lines('foo\nbar')) + ['foo', 'bar'] + >>> list(yield_lines('\nfoo\n#bar\nbaz #comment')) + ['foo', 'baz #comment'] + >>> list(yield_lines(['foo\nbar', 'baz', 'bing\n\n\n'])) + ['foo', 'bar', 'baz', 'bing'] + """ + return itertools.chain.from_iterable(map(yield_lines, iterable)) + + +@yield_lines.register(str) +def _(text): + return filter(_nonblank, map(str.strip, text.splitlines())) + + +def drop_comment(line): + """ + Drop comments. + + >>> drop_comment('foo # bar') + 'foo' + + A hash without a space may be in a URL. + + >>> drop_comment('http://example.com/foo#bar') + 'http://example.com/foo#bar' + """ + return line.partition(' #')[0] + + +def join_continuation(lines): + r""" + Join lines continued by a trailing backslash. + + >>> list(join_continuation(['foo \\', 'bar', 'baz'])) + ['foobar', 'baz'] + >>> list(join_continuation(['foo \\', 'bar', 'baz'])) + ['foobar', 'baz'] + >>> list(join_continuation(['foo \\', 'bar \\', 'baz'])) + ['foobarbaz'] + + Not sure why, but... + The character preceding the backslash is also elided. + + >>> list(join_continuation(['goo\\', 'dly'])) + ['godly'] + + A terrible idea, but... + If no line is available to continue, suppress the lines. + + >>> list(join_continuation(['foo', 'bar\\', 'baz\\'])) + ['foo'] + """ + lines = iter(lines) + for item in lines: + while item.endswith('\\'): + try: + item = item[:-2].strip() + next(lines) + except StopIteration: + return + yield item + + +def read_newlines(filename, limit=1024): + r""" + >>> tmp_path = getfixture('tmp_path') + >>> filename = tmp_path / 'out.txt' + >>> _ = filename.write_text('foo\n', newline='', encoding='utf-8') + >>> read_newlines(filename) + '\n' + >>> _ = filename.write_text('foo\r\n', newline='', encoding='utf-8') + >>> read_newlines(filename) + '\r\n' + >>> _ = filename.write_text('foo\r\nbar\nbing\r', newline='', encoding='utf-8') + >>> read_newlines(filename) + ('\r', '\n', '\r\n') + """ + with open(filename, encoding='utf-8') as fp: + fp.read(limit) + return fp.newlines diff --git a/libs/common/jaraco/text/layouts.py b/libs/common/jaraco/text/layouts.py new file mode 100644 index 00000000..9636f0f7 --- /dev/null +++ b/libs/common/jaraco/text/layouts.py @@ -0,0 +1,25 @@ +qwerty = "-=qwertyuiop[]asdfghjkl;'zxcvbnm,./_+QWERTYUIOP{}ASDFGHJKL:\"ZXCVBNM<>?" +dvorak = "[]',.pyfgcrl/=aoeuidhtns-;qjkxbmwvz{}\"<>PYFGCRL?+AOEUIDHTNS_:QJKXBMWVZ" + + +to_dvorak = str.maketrans(qwerty, dvorak) +to_qwerty = str.maketrans(dvorak, qwerty) + + +def translate(input, translation): + """ + >>> translate('dvorak', to_dvorak) + 'ekrpat' + >>> translate('qwerty', to_qwerty) + 'x,dokt' + """ + return input.translate(translation) + + +def _translate_stream(stream, translation): + """ + >>> import io + >>> _translate_stream(io.StringIO('foo'), to_dvorak) + urr + """ + print(translate(stream.read(), translation)) diff --git a/libs/common/jaraco/text/show-newlines.py b/libs/common/jaraco/text/show-newlines.py new file mode 100644 index 00000000..e11d1ba4 --- /dev/null +++ b/libs/common/jaraco/text/show-newlines.py @@ -0,0 +1,33 @@ +import autocommand +import inflect + +from more_itertools import always_iterable + +import jaraco.text + + +def report_newlines(filename): + r""" + Report the newlines in the indicated file. + + >>> tmp_path = getfixture('tmp_path') + >>> filename = tmp_path / 'out.txt' + >>> _ = filename.write_text('foo\nbar\n', newline='', encoding='utf-8') + >>> report_newlines(filename) + newline is '\n' + >>> filename = tmp_path / 'out.txt' + >>> _ = filename.write_text('foo\nbar\r\n', newline='', encoding='utf-8') + >>> report_newlines(filename) + newlines are ('\n', '\r\n') + """ + newlines = jaraco.text.read_newlines(filename) + count = len(tuple(always_iterable(newlines))) + engine = inflect.engine() + print( + engine.plural_noun("newline", count), + engine.plural_verb("is", count), + repr(newlines), + ) + + +autocommand.autocommand(__name__)(report_newlines) diff --git a/libs/common/jaraco/text/strip-prefix.py b/libs/common/jaraco/text/strip-prefix.py new file mode 100644 index 00000000..761717a9 --- /dev/null +++ b/libs/common/jaraco/text/strip-prefix.py @@ -0,0 +1,21 @@ +import sys + +import autocommand + +from jaraco.text import Stripper + + +def strip_prefix(): + r""" + Strip any common prefix from stdin. + + >>> import io, pytest + >>> getfixture('monkeypatch').setattr('sys.stdin', io.StringIO('abcdef\nabc123')) + >>> strip_prefix() + def + 123 + """ + sys.stdout.writelines(Stripper.strip_prefix(sys.stdin).lines) + + +autocommand.autocommand(__name__)(strip_prefix) diff --git a/libs/common/jaraco/text/to-dvorak.py b/libs/common/jaraco/text/to-dvorak.py new file mode 100644 index 00000000..a6d5da80 --- /dev/null +++ b/libs/common/jaraco/text/to-dvorak.py @@ -0,0 +1,6 @@ +import sys + +from . import layouts + + +__name__ == '__main__' and layouts._translate_stream(sys.stdin, layouts.to_dvorak) diff --git a/libs/common/jaraco/text/to-qwerty.py b/libs/common/jaraco/text/to-qwerty.py new file mode 100644 index 00000000..abe27286 --- /dev/null +++ b/libs/common/jaraco/text/to-qwerty.py @@ -0,0 +1,6 @@ +import sys + +from . import layouts + + +__name__ == '__main__' and layouts._translate_stream(sys.stdin, layouts.to_qwerty) diff --git a/libs/common/pkg_resources/_vendor/packaging/__about__.py b/libs/common/packaging/__init__.py similarity index 54% rename from libs/common/pkg_resources/_vendor/packaging/__about__.py rename to libs/common/packaging/__init__.py index 95d330ef..22809cfd 100644 --- a/libs/common/pkg_resources/_vendor/packaging/__about__.py +++ b/libs/common/packaging/__init__.py @@ -1,21 +1,15 @@ # This file is dual licensed under the terms of the Apache License, Version # 2.0, and the BSD License. See the LICENSE file in the root of this repository # for complete details. -from __future__ import absolute_import, division, print_function - -__all__ = [ - "__title__", "__summary__", "__uri__", "__version__", "__author__", - "__email__", "__license__", "__copyright__", -] __title__ = "packaging" __summary__ = "Core utilities for Python packages" __uri__ = "https://github.com/pypa/packaging" -__version__ = "16.8" +__version__ = "23.2" __author__ = "Donald Stufft and individual contributors" __email__ = "donald@stufft.io" -__license__ = "BSD or Apache License, Version 2.0" -__copyright__ = "Copyright 2014-2016 %s" % __author__ +__license__ = "BSD-2-Clause or Apache-2.0" +__copyright__ = "2014 %s" % __author__ diff --git a/libs/common/packaging/_elffile.py b/libs/common/packaging/_elffile.py new file mode 100644 index 00000000..6fb19b30 --- /dev/null +++ b/libs/common/packaging/_elffile.py @@ -0,0 +1,108 @@ +""" +ELF file parser. + +This provides a class ``ELFFile`` that parses an ELF executable in a similar +interface to ``ZipFile``. Only the read interface is implemented. + +Based on: https://gist.github.com/lyssdod/f51579ae8d93c8657a5564aefc2ffbca +ELF header: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html +""" + +import enum +import os +import struct +from typing import IO, Optional, Tuple + + +class ELFInvalid(ValueError): + pass + + +class EIClass(enum.IntEnum): + C32 = 1 + C64 = 2 + + +class EIData(enum.IntEnum): + Lsb = 1 + Msb = 2 + + +class EMachine(enum.IntEnum): + I386 = 3 + S390 = 22 + Arm = 40 + X8664 = 62 + AArc64 = 183 + + +class ELFFile: + """ + Representation of an ELF executable. + """ + + def __init__(self, f: IO[bytes]) -> None: + self._f = f + + try: + ident = self._read("16B") + except struct.error: + raise ELFInvalid("unable to parse identification") + magic = bytes(ident[:4]) + if magic != b"\x7fELF": + raise ELFInvalid(f"invalid magic: {magic!r}") + + self.capacity = ident[4] # Format for program header (bitness). + self.encoding = ident[5] # Data structure encoding (endianness). + + try: + # e_fmt: Format for program header. + # p_fmt: Format for section header. + # p_idx: Indexes to find p_type, p_offset, and p_filesz. + e_fmt, self._p_fmt, self._p_idx = { + (1, 1): ("HHIIIIIHHH", ">IIIIIIII", (0, 1, 4)), # 32-bit MSB. + (2, 1): ("HHIQQQIHHH", ">IIQQQQQQ", (0, 2, 5)), # 64-bit MSB. + }[(self.capacity, self.encoding)] + except KeyError: + raise ELFInvalid( + f"unrecognized capacity ({self.capacity}) or " + f"encoding ({self.encoding})" + ) + + try: + ( + _, + self.machine, # Architecture type. + _, + _, + self._e_phoff, # Offset of program header. + _, + self.flags, # Processor-specific flags. + _, + self._e_phentsize, # Size of section. + self._e_phnum, # Number of sections. + ) = self._read(e_fmt) + except struct.error as e: + raise ELFInvalid("unable to parse machine and section information") from e + + def _read(self, fmt: str) -> Tuple[int, ...]: + return struct.unpack(fmt, self._f.read(struct.calcsize(fmt))) + + @property + def interpreter(self) -> Optional[str]: + """ + The path recorded in the ``PT_INTERP`` section header. + """ + for index in range(self._e_phnum): + self._f.seek(self._e_phoff + self._e_phentsize * index) + try: + data = self._read(self._p_fmt) + except struct.error: + continue + if data[self._p_idx[0]] != 3: # Not PT_INTERP. + continue + self._f.seek(data[self._p_idx[1]]) + return os.fsdecode(self._f.read(data[self._p_idx[2]])).strip("\0") + return None diff --git a/libs/common/packaging/_manylinux.py b/libs/common/packaging/_manylinux.py new file mode 100644 index 00000000..3705d50d --- /dev/null +++ b/libs/common/packaging/_manylinux.py @@ -0,0 +1,252 @@ +import collections +import contextlib +import functools +import os +import re +import sys +import warnings +from typing import Dict, Generator, Iterator, NamedTuple, Optional, Sequence, Tuple + +from ._elffile import EIClass, EIData, ELFFile, EMachine + +EF_ARM_ABIMASK = 0xFF000000 +EF_ARM_ABI_VER5 = 0x05000000 +EF_ARM_ABI_FLOAT_HARD = 0x00000400 + + +# `os.PathLike` not a generic type until Python 3.9, so sticking with `str` +# as the type for `path` until then. +@contextlib.contextmanager +def _parse_elf(path: str) -> Generator[Optional[ELFFile], None, None]: + try: + with open(path, "rb") as f: + yield ELFFile(f) + except (OSError, TypeError, ValueError): + yield None + + +def _is_linux_armhf(executable: str) -> bool: + # hard-float ABI can be detected from the ELF header of the running + # process + # https://static.docs.arm.com/ihi0044/g/aaelf32.pdf + with _parse_elf(executable) as f: + return ( + f is not None + and f.capacity == EIClass.C32 + and f.encoding == EIData.Lsb + and f.machine == EMachine.Arm + and f.flags & EF_ARM_ABIMASK == EF_ARM_ABI_VER5 + and f.flags & EF_ARM_ABI_FLOAT_HARD == EF_ARM_ABI_FLOAT_HARD + ) + + +def _is_linux_i686(executable: str) -> bool: + with _parse_elf(executable) as f: + return ( + f is not None + and f.capacity == EIClass.C32 + and f.encoding == EIData.Lsb + and f.machine == EMachine.I386 + ) + + +def _have_compatible_abi(executable: str, archs: Sequence[str]) -> bool: + if "armv7l" in archs: + return _is_linux_armhf(executable) + if "i686" in archs: + return _is_linux_i686(executable) + allowed_archs = {"x86_64", "aarch64", "ppc64", "ppc64le", "s390x", "loongarch64"} + return any(arch in allowed_archs for arch in archs) + + +# If glibc ever changes its major version, we need to know what the last +# minor version was, so we can build the complete list of all versions. +# For now, guess what the highest minor version might be, assume it will +# be 50 for testing. Once this actually happens, update the dictionary +# with the actual value. +_LAST_GLIBC_MINOR: Dict[int, int] = collections.defaultdict(lambda: 50) + + +class _GLibCVersion(NamedTuple): + major: int + minor: int + + +def _glibc_version_string_confstr() -> Optional[str]: + """ + Primary implementation of glibc_version_string using os.confstr. + """ + # os.confstr is quite a bit faster than ctypes.DLL. It's also less likely + # to be broken or missing. This strategy is used in the standard library + # platform module. + # https://github.com/python/cpython/blob/fcf1d003bf4f0100c/Lib/platform.py#L175-L183 + try: + # Should be a string like "glibc 2.17". + version_string: str = getattr(os, "confstr")("CS_GNU_LIBC_VERSION") + assert version_string is not None + _, version = version_string.rsplit() + except (AssertionError, AttributeError, OSError, ValueError): + # os.confstr() or CS_GNU_LIBC_VERSION not available (or a bad value)... + return None + return version + + +def _glibc_version_string_ctypes() -> Optional[str]: + """ + Fallback implementation of glibc_version_string using ctypes. + """ + try: + import ctypes + except ImportError: + return None + + # ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen + # manpage says, "If filename is NULL, then the returned handle is for the + # main program". This way we can let the linker do the work to figure out + # which libc our process is actually using. + # + # We must also handle the special case where the executable is not a + # dynamically linked executable. This can occur when using musl libc, + # for example. In this situation, dlopen() will error, leading to an + # OSError. Interestingly, at least in the case of musl, there is no + # errno set on the OSError. The single string argument used to construct + # OSError comes from libc itself and is therefore not portable to + # hard code here. In any case, failure to call dlopen() means we + # can proceed, so we bail on our attempt. + try: + process_namespace = ctypes.CDLL(None) + except OSError: + return None + + try: + gnu_get_libc_version = process_namespace.gnu_get_libc_version + except AttributeError: + # Symbol doesn't exist -> therefore, we are not linked to + # glibc. + return None + + # Call gnu_get_libc_version, which returns a string like "2.5" + gnu_get_libc_version.restype = ctypes.c_char_p + version_str: str = gnu_get_libc_version() + # py2 / py3 compatibility: + if not isinstance(version_str, str): + version_str = version_str.decode("ascii") + + return version_str + + +def _glibc_version_string() -> Optional[str]: + """Returns glibc version string, or None if not using glibc.""" + return _glibc_version_string_confstr() or _glibc_version_string_ctypes() + + +def _parse_glibc_version(version_str: str) -> Tuple[int, int]: + """Parse glibc version. + + We use a regexp instead of str.split because we want to discard any + random junk that might come after the minor version -- this might happen + in patched/forked versions of glibc (e.g. Linaro's version of glibc + uses version strings like "2.20-2014.11"). See gh-3588. + """ + m = re.match(r"(?P[0-9]+)\.(?P[0-9]+)", version_str) + if not m: + warnings.warn( + f"Expected glibc version with 2 components major.minor," + f" got: {version_str}", + RuntimeWarning, + ) + return -1, -1 + return int(m.group("major")), int(m.group("minor")) + + +@functools.lru_cache() +def _get_glibc_version() -> Tuple[int, int]: + version_str = _glibc_version_string() + if version_str is None: + return (-1, -1) + return _parse_glibc_version(version_str) + + +# From PEP 513, PEP 600 +def _is_compatible(arch: str, version: _GLibCVersion) -> bool: + sys_glibc = _get_glibc_version() + if sys_glibc < version: + return False + # Check for presence of _manylinux module. + try: + import _manylinux # noqa + except ImportError: + return True + if hasattr(_manylinux, "manylinux_compatible"): + result = _manylinux.manylinux_compatible(version[0], version[1], arch) + if result is not None: + return bool(result) + return True + if version == _GLibCVersion(2, 5): + if hasattr(_manylinux, "manylinux1_compatible"): + return bool(_manylinux.manylinux1_compatible) + if version == _GLibCVersion(2, 12): + if hasattr(_manylinux, "manylinux2010_compatible"): + return bool(_manylinux.manylinux2010_compatible) + if version == _GLibCVersion(2, 17): + if hasattr(_manylinux, "manylinux2014_compatible"): + return bool(_manylinux.manylinux2014_compatible) + return True + + +_LEGACY_MANYLINUX_MAP = { + # CentOS 7 w/ glibc 2.17 (PEP 599) + (2, 17): "manylinux2014", + # CentOS 6 w/ glibc 2.12 (PEP 571) + (2, 12): "manylinux2010", + # CentOS 5 w/ glibc 2.5 (PEP 513) + (2, 5): "manylinux1", +} + + +def platform_tags(archs: Sequence[str]) -> Iterator[str]: + """Generate manylinux tags compatible to the current platform. + + :param archs: Sequence of compatible architectures. + The first one shall be the closest to the actual architecture and be the part of + platform tag after the ``linux_`` prefix, e.g. ``x86_64``. + The ``linux_`` prefix is assumed as a prerequisite for the current platform to + be manylinux-compatible. + + :returns: An iterator of compatible manylinux tags. + """ + if not _have_compatible_abi(sys.executable, archs): + return + # Oldest glibc to be supported regardless of architecture is (2, 17). + too_old_glibc2 = _GLibCVersion(2, 16) + if set(archs) & {"x86_64", "i686"}: + # On x86/i686 also oldest glibc to be supported is (2, 5). + too_old_glibc2 = _GLibCVersion(2, 4) + current_glibc = _GLibCVersion(*_get_glibc_version()) + glibc_max_list = [current_glibc] + # We can assume compatibility across glibc major versions. + # https://sourceware.org/bugzilla/show_bug.cgi?id=24636 + # + # Build a list of maximum glibc versions so that we can + # output the canonical list of all glibc from current_glibc + # down to too_old_glibc2, including all intermediary versions. + for glibc_major in range(current_glibc.major - 1, 1, -1): + glibc_minor = _LAST_GLIBC_MINOR[glibc_major] + glibc_max_list.append(_GLibCVersion(glibc_major, glibc_minor)) + for arch in archs: + for glibc_max in glibc_max_list: + if glibc_max.major == too_old_glibc2.major: + min_minor = too_old_glibc2.minor + else: + # For other glibc major versions oldest supported is (x, 0). + min_minor = -1 + for glibc_minor in range(glibc_max.minor, min_minor, -1): + glibc_version = _GLibCVersion(glibc_max.major, glibc_minor) + tag = "manylinux_{}_{}".format(*glibc_version) + if _is_compatible(arch, glibc_version): + yield f"{tag}_{arch}" + # Handle the legacy manylinux1, manylinux2010, manylinux2014 tags. + if glibc_version in _LEGACY_MANYLINUX_MAP: + legacy_tag = _LEGACY_MANYLINUX_MAP[glibc_version] + if _is_compatible(arch, glibc_version): + yield f"{legacy_tag}_{arch}" diff --git a/libs/common/packaging/_musllinux.py b/libs/common/packaging/_musllinux.py new file mode 100644 index 00000000..86419df9 --- /dev/null +++ b/libs/common/packaging/_musllinux.py @@ -0,0 +1,83 @@ +"""PEP 656 support. + +This module implements logic to detect if the currently running Python is +linked against musl, and what musl version is used. +""" + +import functools +import re +import subprocess +import sys +from typing import Iterator, NamedTuple, Optional, Sequence + +from ._elffile import ELFFile + + +class _MuslVersion(NamedTuple): + major: int + minor: int + + +def _parse_musl_version(output: str) -> Optional[_MuslVersion]: + lines = [n for n in (n.strip() for n in output.splitlines()) if n] + if len(lines) < 2 or lines[0][:4] != "musl": + return None + m = re.match(r"Version (\d+)\.(\d+)", lines[1]) + if not m: + return None + return _MuslVersion(major=int(m.group(1)), minor=int(m.group(2))) + + +@functools.lru_cache() +def _get_musl_version(executable: str) -> Optional[_MuslVersion]: + """Detect currently-running musl runtime version. + + This is done by checking the specified executable's dynamic linking + information, and invoking the loader to parse its output for a version + string. If the loader is musl, the output would be something like:: + + musl libc (x86_64) + Version 1.2.2 + Dynamic Program Loader + """ + try: + with open(executable, "rb") as f: + ld = ELFFile(f).interpreter + except (OSError, TypeError, ValueError): + return None + if ld is None or "musl" not in ld: + return None + proc = subprocess.run([ld], stderr=subprocess.PIPE, text=True) + return _parse_musl_version(proc.stderr) + + +def platform_tags(archs: Sequence[str]) -> Iterator[str]: + """Generate musllinux tags compatible to the current platform. + + :param archs: Sequence of compatible architectures. + The first one shall be the closest to the actual architecture and be the part of + platform tag after the ``linux_`` prefix, e.g. ``x86_64``. + The ``linux_`` prefix is assumed as a prerequisite for the current platform to + be musllinux-compatible. + + :returns: An iterator of compatible musllinux tags. + """ + sys_musl = _get_musl_version(sys.executable) + if sys_musl is None: # Python not dynamically linked against musl. + return + for arch in archs: + for minor in range(sys_musl.minor, -1, -1): + yield f"musllinux_{sys_musl.major}_{minor}_{arch}" + + +if __name__ == "__main__": # pragma: no cover + import sysconfig + + plat = sysconfig.get_platform() + assert plat.startswith("linux-"), "not linux" + + print("plat:", plat) + print("musl:", _get_musl_version(sys.executable)) + print("tags:", end=" ") + for t in platform_tags(re.sub(r"[.-]", "_", plat.split("-", 1)[-1])): + print(t, end="\n ") diff --git a/libs/common/packaging/_parser.py b/libs/common/packaging/_parser.py new file mode 100644 index 00000000..4576981c --- /dev/null +++ b/libs/common/packaging/_parser.py @@ -0,0 +1,359 @@ +"""Handwritten parser of dependency specifiers. + +The docstring for each __parse_* function contains ENBF-inspired grammar representing +the implementation. +""" + +import ast +from typing import Any, List, NamedTuple, Optional, Tuple, Union + +from ._tokenizer import DEFAULT_RULES, Tokenizer + + +class Node: + def __init__(self, value: str) -> None: + self.value = value + + def __str__(self) -> str: + return self.value + + def __repr__(self) -> str: + return f"<{self.__class__.__name__}('{self}')>" + + def serialize(self) -> str: + raise NotImplementedError + + +class Variable(Node): + def serialize(self) -> str: + return str(self) + + +class Value(Node): + def serialize(self) -> str: + return f'"{self}"' + + +class Op(Node): + def serialize(self) -> str: + return str(self) + + +MarkerVar = Union[Variable, Value] +MarkerItem = Tuple[MarkerVar, Op, MarkerVar] +# MarkerAtom = Union[MarkerItem, List["MarkerAtom"]] +# MarkerList = List[Union["MarkerList", MarkerAtom, str]] +# mypy does not support recursive type definition +# https://github.com/python/mypy/issues/731 +MarkerAtom = Any +MarkerList = List[Any] + + +class ParsedRequirement(NamedTuple): + name: str + url: str + extras: List[str] + specifier: str + marker: Optional[MarkerList] + + +# -------------------------------------------------------------------------------------- +# Recursive descent parser for dependency specifier +# -------------------------------------------------------------------------------------- +def parse_requirement(source: str) -> ParsedRequirement: + return _parse_requirement(Tokenizer(source, rules=DEFAULT_RULES)) + + +def _parse_requirement(tokenizer: Tokenizer) -> ParsedRequirement: + """ + requirement = WS? IDENTIFIER WS? extras WS? requirement_details + """ + tokenizer.consume("WS") + + name_token = tokenizer.expect( + "IDENTIFIER", expected="package name at the start of dependency specifier" + ) + name = name_token.text + tokenizer.consume("WS") + + extras = _parse_extras(tokenizer) + tokenizer.consume("WS") + + url, specifier, marker = _parse_requirement_details(tokenizer) + tokenizer.expect("END", expected="end of dependency specifier") + + return ParsedRequirement(name, url, extras, specifier, marker) + + +def _parse_requirement_details( + tokenizer: Tokenizer, +) -> Tuple[str, str, Optional[MarkerList]]: + """ + requirement_details = AT URL (WS requirement_marker?)? + | specifier WS? (requirement_marker)? + """ + + specifier = "" + url = "" + marker = None + + if tokenizer.check("AT"): + tokenizer.read() + tokenizer.consume("WS") + + url_start = tokenizer.position + url = tokenizer.expect("URL", expected="URL after @").text + if tokenizer.check("END", peek=True): + return (url, specifier, marker) + + tokenizer.expect("WS", expected="whitespace after URL") + + # The input might end after whitespace. + if tokenizer.check("END", peek=True): + return (url, specifier, marker) + + marker = _parse_requirement_marker( + tokenizer, span_start=url_start, after="URL and whitespace" + ) + else: + specifier_start = tokenizer.position + specifier = _parse_specifier(tokenizer) + tokenizer.consume("WS") + + if tokenizer.check("END", peek=True): + return (url, specifier, marker) + + marker = _parse_requirement_marker( + tokenizer, + span_start=specifier_start, + after=( + "version specifier" + if specifier + else "name and no valid version specifier" + ), + ) + + return (url, specifier, marker) + + +def _parse_requirement_marker( + tokenizer: Tokenizer, *, span_start: int, after: str +) -> MarkerList: + """ + requirement_marker = SEMICOLON marker WS? + """ + + if not tokenizer.check("SEMICOLON"): + tokenizer.raise_syntax_error( + f"Expected end or semicolon (after {after})", + span_start=span_start, + ) + tokenizer.read() + + marker = _parse_marker(tokenizer) + tokenizer.consume("WS") + + return marker + + +def _parse_extras(tokenizer: Tokenizer) -> List[str]: + """ + extras = (LEFT_BRACKET wsp* extras_list? wsp* RIGHT_BRACKET)? + """ + if not tokenizer.check("LEFT_BRACKET", peek=True): + return [] + + with tokenizer.enclosing_tokens( + "LEFT_BRACKET", + "RIGHT_BRACKET", + around="extras", + ): + tokenizer.consume("WS") + extras = _parse_extras_list(tokenizer) + tokenizer.consume("WS") + + return extras + + +def _parse_extras_list(tokenizer: Tokenizer) -> List[str]: + """ + extras_list = identifier (wsp* ',' wsp* identifier)* + """ + extras: List[str] = [] + + if not tokenizer.check("IDENTIFIER"): + return extras + + extras.append(tokenizer.read().text) + + while True: + tokenizer.consume("WS") + if tokenizer.check("IDENTIFIER", peek=True): + tokenizer.raise_syntax_error("Expected comma between extra names") + elif not tokenizer.check("COMMA"): + break + + tokenizer.read() + tokenizer.consume("WS") + + extra_token = tokenizer.expect("IDENTIFIER", expected="extra name after comma") + extras.append(extra_token.text) + + return extras + + +def _parse_specifier(tokenizer: Tokenizer) -> str: + """ + specifier = LEFT_PARENTHESIS WS? version_many WS? RIGHT_PARENTHESIS + | WS? version_many WS? + """ + with tokenizer.enclosing_tokens( + "LEFT_PARENTHESIS", + "RIGHT_PARENTHESIS", + around="version specifier", + ): + tokenizer.consume("WS") + parsed_specifiers = _parse_version_many(tokenizer) + tokenizer.consume("WS") + + return parsed_specifiers + + +def _parse_version_many(tokenizer: Tokenizer) -> str: + """ + version_many = (SPECIFIER (WS? COMMA WS? SPECIFIER)*)? + """ + parsed_specifiers = "" + while tokenizer.check("SPECIFIER"): + span_start = tokenizer.position + parsed_specifiers += tokenizer.read().text + if tokenizer.check("VERSION_PREFIX_TRAIL", peek=True): + tokenizer.raise_syntax_error( + ".* suffix can only be used with `==` or `!=` operators", + span_start=span_start, + span_end=tokenizer.position + 1, + ) + if tokenizer.check("VERSION_LOCAL_LABEL_TRAIL", peek=True): + tokenizer.raise_syntax_error( + "Local version label can only be used with `==` or `!=` operators", + span_start=span_start, + span_end=tokenizer.position, + ) + tokenizer.consume("WS") + if not tokenizer.check("COMMA"): + break + parsed_specifiers += tokenizer.read().text + tokenizer.consume("WS") + + return parsed_specifiers + + +# -------------------------------------------------------------------------------------- +# Recursive descent parser for marker expression +# -------------------------------------------------------------------------------------- +def parse_marker(source: str) -> MarkerList: + return _parse_full_marker(Tokenizer(source, rules=DEFAULT_RULES)) + + +def _parse_full_marker(tokenizer: Tokenizer) -> MarkerList: + retval = _parse_marker(tokenizer) + tokenizer.expect("END", expected="end of marker expression") + return retval + + +def _parse_marker(tokenizer: Tokenizer) -> MarkerList: + """ + marker = marker_atom (BOOLOP marker_atom)+ + """ + expression = [_parse_marker_atom(tokenizer)] + while tokenizer.check("BOOLOP"): + token = tokenizer.read() + expr_right = _parse_marker_atom(tokenizer) + expression.extend((token.text, expr_right)) + return expression + + +def _parse_marker_atom(tokenizer: Tokenizer) -> MarkerAtom: + """ + marker_atom = WS? LEFT_PARENTHESIS WS? marker WS? RIGHT_PARENTHESIS WS? + | WS? marker_item WS? + """ + + tokenizer.consume("WS") + if tokenizer.check("LEFT_PARENTHESIS", peek=True): + with tokenizer.enclosing_tokens( + "LEFT_PARENTHESIS", + "RIGHT_PARENTHESIS", + around="marker expression", + ): + tokenizer.consume("WS") + marker: MarkerAtom = _parse_marker(tokenizer) + tokenizer.consume("WS") + else: + marker = _parse_marker_item(tokenizer) + tokenizer.consume("WS") + return marker + + +def _parse_marker_item(tokenizer: Tokenizer) -> MarkerItem: + """ + marker_item = WS? marker_var WS? marker_op WS? marker_var WS? + """ + tokenizer.consume("WS") + marker_var_left = _parse_marker_var(tokenizer) + tokenizer.consume("WS") + marker_op = _parse_marker_op(tokenizer) + tokenizer.consume("WS") + marker_var_right = _parse_marker_var(tokenizer) + tokenizer.consume("WS") + return (marker_var_left, marker_op, marker_var_right) + + +def _parse_marker_var(tokenizer: Tokenizer) -> MarkerVar: + """ + marker_var = VARIABLE | QUOTED_STRING + """ + if tokenizer.check("VARIABLE"): + return process_env_var(tokenizer.read().text.replace(".", "_")) + elif tokenizer.check("QUOTED_STRING"): + return process_python_str(tokenizer.read().text) + else: + tokenizer.raise_syntax_error( + message="Expected a marker variable or quoted string" + ) + + +def process_env_var(env_var: str) -> Variable: + if ( + env_var == "platform_python_implementation" + or env_var == "python_implementation" + ): + return Variable("platform_python_implementation") + else: + return Variable(env_var) + + +def process_python_str(python_str: str) -> Value: + value = ast.literal_eval(python_str) + return Value(str(value)) + + +def _parse_marker_op(tokenizer: Tokenizer) -> Op: + """ + marker_op = IN | NOT IN | OP + """ + if tokenizer.check("IN"): + tokenizer.read() + return Op("in") + elif tokenizer.check("NOT"): + tokenizer.read() + tokenizer.expect("WS", expected="whitespace after 'not'") + tokenizer.expect("IN", expected="'in' after 'not'") + return Op("not in") + elif tokenizer.check("OP"): + return Op(tokenizer.read().text) + else: + return tokenizer.raise_syntax_error( + "Expected marker operator, one of " + "<=, <, !=, ==, >=, >, ~=, ===, in, not in" + ) diff --git a/libs/common/packaging/_structures.py b/libs/common/packaging/_structures.py new file mode 100644 index 00000000..90a6465f --- /dev/null +++ b/libs/common/packaging/_structures.py @@ -0,0 +1,61 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + + +class InfinityType: + def __repr__(self) -> str: + return "Infinity" + + def __hash__(self) -> int: + return hash(repr(self)) + + def __lt__(self, other: object) -> bool: + return False + + def __le__(self, other: object) -> bool: + return False + + def __eq__(self, other: object) -> bool: + return isinstance(other, self.__class__) + + def __gt__(self, other: object) -> bool: + return True + + def __ge__(self, other: object) -> bool: + return True + + def __neg__(self: object) -> "NegativeInfinityType": + return NegativeInfinity + + +Infinity = InfinityType() + + +class NegativeInfinityType: + def __repr__(self) -> str: + return "-Infinity" + + def __hash__(self) -> int: + return hash(repr(self)) + + def __lt__(self, other: object) -> bool: + return True + + def __le__(self, other: object) -> bool: + return True + + def __eq__(self, other: object) -> bool: + return isinstance(other, self.__class__) + + def __gt__(self, other: object) -> bool: + return False + + def __ge__(self, other: object) -> bool: + return False + + def __neg__(self: object) -> InfinityType: + return Infinity + + +NegativeInfinity = NegativeInfinityType() diff --git a/libs/common/packaging/_tokenizer.py b/libs/common/packaging/_tokenizer.py new file mode 100644 index 00000000..dd0d648d --- /dev/null +++ b/libs/common/packaging/_tokenizer.py @@ -0,0 +1,192 @@ +import contextlib +import re +from dataclasses import dataclass +from typing import Dict, Iterator, NoReturn, Optional, Tuple, Union + +from .specifiers import Specifier + + +@dataclass +class Token: + name: str + text: str + position: int + + +class ParserSyntaxError(Exception): + """The provided source text could not be parsed correctly.""" + + def __init__( + self, + message: str, + *, + source: str, + span: Tuple[int, int], + ) -> None: + self.span = span + self.message = message + self.source = source + + super().__init__() + + def __str__(self) -> str: + marker = " " * self.span[0] + "~" * (self.span[1] - self.span[0]) + "^" + return "\n ".join([self.message, self.source, marker]) + + +DEFAULT_RULES: "Dict[str, Union[str, re.Pattern[str]]]" = { + "LEFT_PARENTHESIS": r"\(", + "RIGHT_PARENTHESIS": r"\)", + "LEFT_BRACKET": r"\[", + "RIGHT_BRACKET": r"\]", + "SEMICOLON": r";", + "COMMA": r",", + "QUOTED_STRING": re.compile( + r""" + ( + ('[^']*') + | + ("[^"]*") + ) + """, + re.VERBOSE, + ), + "OP": r"(===|==|~=|!=|<=|>=|<|>)", + "BOOLOP": r"\b(or|and)\b", + "IN": r"\bin\b", + "NOT": r"\bnot\b", + "VARIABLE": re.compile( + r""" + \b( + python_version + |python_full_version + |os[._]name + |sys[._]platform + |platform_(release|system) + |platform[._](version|machine|python_implementation) + |python_implementation + |implementation_(name|version) + |extra + )\b + """, + re.VERBOSE, + ), + "SPECIFIER": re.compile( + Specifier._operator_regex_str + Specifier._version_regex_str, + re.VERBOSE | re.IGNORECASE, + ), + "AT": r"\@", + "URL": r"[^ \t]+", + "IDENTIFIER": r"\b[a-zA-Z0-9][a-zA-Z0-9._-]*\b", + "VERSION_PREFIX_TRAIL": r"\.\*", + "VERSION_LOCAL_LABEL_TRAIL": r"\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*", + "WS": r"[ \t]+", + "END": r"$", +} + + +class Tokenizer: + """Context-sensitive token parsing. + + Provides methods to examine the input stream to check whether the next token + matches. + """ + + def __init__( + self, + source: str, + *, + rules: "Dict[str, Union[str, re.Pattern[str]]]", + ) -> None: + self.source = source + self.rules: Dict[str, re.Pattern[str]] = { + name: re.compile(pattern) for name, pattern in rules.items() + } + self.next_token: Optional[Token] = None + self.position = 0 + + def consume(self, name: str) -> None: + """Move beyond provided token name, if at current position.""" + if self.check(name): + self.read() + + def check(self, name: str, *, peek: bool = False) -> bool: + """Check whether the next token has the provided name. + + By default, if the check succeeds, the token *must* be read before + another check. If `peek` is set to `True`, the token is not loaded and + would need to be checked again. + """ + assert ( + self.next_token is None + ), f"Cannot check for {name!r}, already have {self.next_token!r}" + assert name in self.rules, f"Unknown token name: {name!r}" + + expression = self.rules[name] + + match = expression.match(self.source, self.position) + if match is None: + return False + if not peek: + self.next_token = Token(name, match[0], self.position) + return True + + def expect(self, name: str, *, expected: str) -> Token: + """Expect a certain token name next, failing with a syntax error otherwise. + + The token is *not* read. + """ + if not self.check(name): + raise self.raise_syntax_error(f"Expected {expected}") + return self.read() + + def read(self) -> Token: + """Consume the next token and return it.""" + token = self.next_token + assert token is not None + + self.position += len(token.text) + self.next_token = None + + return token + + def raise_syntax_error( + self, + message: str, + *, + span_start: Optional[int] = None, + span_end: Optional[int] = None, + ) -> NoReturn: + """Raise ParserSyntaxError at the given position.""" + span = ( + self.position if span_start is None else span_start, + self.position if span_end is None else span_end, + ) + raise ParserSyntaxError( + message, + source=self.source, + span=span, + ) + + @contextlib.contextmanager + def enclosing_tokens( + self, open_token: str, close_token: str, *, around: str + ) -> Iterator[None]: + if self.check(open_token): + open_position = self.position + self.read() + else: + open_position = None + + yield + + if open_position is None: + return + + if not self.check(close_token): + self.raise_syntax_error( + f"Expected matching {close_token} for {open_token}, after {around}", + span_start=open_position, + ) + + self.read() diff --git a/libs/common/packaging/markers.py b/libs/common/packaging/markers.py new file mode 100644 index 00000000..8b98fca7 --- /dev/null +++ b/libs/common/packaging/markers.py @@ -0,0 +1,252 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +import operator +import os +import platform +import sys +from typing import Any, Callable, Dict, List, Optional, Tuple, Union + +from ._parser import ( + MarkerAtom, + MarkerList, + Op, + Value, + Variable, + parse_marker as _parse_marker, +) +from ._tokenizer import ParserSyntaxError +from .specifiers import InvalidSpecifier, Specifier +from .utils import canonicalize_name + +__all__ = [ + "InvalidMarker", + "UndefinedComparison", + "UndefinedEnvironmentName", + "Marker", + "default_environment", +] + +Operator = Callable[[str, str], bool] + + +class InvalidMarker(ValueError): + """ + An invalid marker was found, users should refer to PEP 508. + """ + + +class UndefinedComparison(ValueError): + """ + An invalid operation was attempted on a value that doesn't support it. + """ + + +class UndefinedEnvironmentName(ValueError): + """ + A name was attempted to be used that does not exist inside of the + environment. + """ + + +def _normalize_extra_values(results: Any) -> Any: + """ + Normalize extra values. + """ + if isinstance(results[0], tuple): + lhs, op, rhs = results[0] + if isinstance(lhs, Variable) and lhs.value == "extra": + normalized_extra = canonicalize_name(rhs.value) + rhs = Value(normalized_extra) + elif isinstance(rhs, Variable) and rhs.value == "extra": + normalized_extra = canonicalize_name(lhs.value) + lhs = Value(normalized_extra) + results[0] = lhs, op, rhs + return results + + +def _format_marker( + marker: Union[List[str], MarkerAtom, str], first: Optional[bool] = True +) -> str: + + assert isinstance(marker, (list, tuple, str)) + + # Sometimes we have a structure like [[...]] which is a single item list + # where the single item is itself it's own list. In that case we want skip + # the rest of this function so that we don't get extraneous () on the + # outside. + if ( + isinstance(marker, list) + and len(marker) == 1 + and isinstance(marker[0], (list, tuple)) + ): + return _format_marker(marker[0]) + + if isinstance(marker, list): + inner = (_format_marker(m, first=False) for m in marker) + if first: + return " ".join(inner) + else: + return "(" + " ".join(inner) + ")" + elif isinstance(marker, tuple): + return " ".join([m.serialize() for m in marker]) + else: + return marker + + +_operators: Dict[str, Operator] = { + "in": lambda lhs, rhs: lhs in rhs, + "not in": lambda lhs, rhs: lhs not in rhs, + "<": operator.lt, + "<=": operator.le, + "==": operator.eq, + "!=": operator.ne, + ">=": operator.ge, + ">": operator.gt, +} + + +def _eval_op(lhs: str, op: Op, rhs: str) -> bool: + try: + spec = Specifier("".join([op.serialize(), rhs])) + except InvalidSpecifier: + pass + else: + return spec.contains(lhs, prereleases=True) + + oper: Optional[Operator] = _operators.get(op.serialize()) + if oper is None: + raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.") + + return oper(lhs, rhs) + + +def _normalize(*values: str, key: str) -> Tuple[str, ...]: + # PEP 685 – Comparison of extra names for optional distribution dependencies + # https://peps.python.org/pep-0685/ + # > When comparing extra names, tools MUST normalize the names being + # > compared using the semantics outlined in PEP 503 for names + if key == "extra": + return tuple(canonicalize_name(v) for v in values) + + # other environment markers don't have such standards + return values + + +def _evaluate_markers(markers: MarkerList, environment: Dict[str, str]) -> bool: + groups: List[List[bool]] = [[]] + + for marker in markers: + assert isinstance(marker, (list, tuple, str)) + + if isinstance(marker, list): + groups[-1].append(_evaluate_markers(marker, environment)) + elif isinstance(marker, tuple): + lhs, op, rhs = marker + + if isinstance(lhs, Variable): + environment_key = lhs.value + lhs_value = environment[environment_key] + rhs_value = rhs.value + else: + lhs_value = lhs.value + environment_key = rhs.value + rhs_value = environment[environment_key] + + lhs_value, rhs_value = _normalize(lhs_value, rhs_value, key=environment_key) + groups[-1].append(_eval_op(lhs_value, op, rhs_value)) + else: + assert marker in ["and", "or"] + if marker == "or": + groups.append([]) + + return any(all(item) for item in groups) + + +def format_full_version(info: "sys._version_info") -> str: + version = "{0.major}.{0.minor}.{0.micro}".format(info) + kind = info.releaselevel + if kind != "final": + version += kind[0] + str(info.serial) + return version + + +def default_environment() -> Dict[str, str]: + iver = format_full_version(sys.implementation.version) + implementation_name = sys.implementation.name + return { + "implementation_name": implementation_name, + "implementation_version": iver, + "os_name": os.name, + "platform_machine": platform.machine(), + "platform_release": platform.release(), + "platform_system": platform.system(), + "platform_version": platform.version(), + "python_full_version": platform.python_version(), + "platform_python_implementation": platform.python_implementation(), + "python_version": ".".join(platform.python_version_tuple()[:2]), + "sys_platform": sys.platform, + } + + +class Marker: + def __init__(self, marker: str) -> None: + # Note: We create a Marker object without calling this constructor in + # packaging.requirements.Requirement. If any additional logic is + # added here, make sure to mirror/adapt Requirement. + try: + self._markers = _normalize_extra_values(_parse_marker(marker)) + # The attribute `_markers` can be described in terms of a recursive type: + # MarkerList = List[Union[Tuple[Node, ...], str, MarkerList]] + # + # For example, the following expression: + # python_version > "3.6" or (python_version == "3.6" and os_name == "unix") + # + # is parsed into: + # [ + # (, ')>, ), + # 'and', + # [ + # (, , ), + # 'or', + # (, , ) + # ] + # ] + except ParserSyntaxError as e: + raise InvalidMarker(str(e)) from e + + def __str__(self) -> str: + return _format_marker(self._markers) + + def __repr__(self) -> str: + return f"" + + def __hash__(self) -> int: + return hash((self.__class__.__name__, str(self))) + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, Marker): + return NotImplemented + + return str(self) == str(other) + + def evaluate(self, environment: Optional[Dict[str, str]] = None) -> bool: + """Evaluate a marker. + + Return the boolean from evaluating the given marker against the + environment. environment is an optional argument to override all or + part of the determined environment. + + The environment is determined from the current Python process. + """ + current_environment = default_environment() + current_environment["extra"] = "" + if environment is not None: + current_environment.update(environment) + # The API used to allow setting extra to None. We need to handle this + # case for backwards compatibility. + if current_environment["extra"] is None: + current_environment["extra"] = "" + + return _evaluate_markers(self._markers, current_environment) diff --git a/libs/common/packaging/metadata.py b/libs/common/packaging/metadata.py new file mode 100644 index 00000000..7b0e6a9c --- /dev/null +++ b/libs/common/packaging/metadata.py @@ -0,0 +1,822 @@ +import email.feedparser +import email.header +import email.message +import email.parser +import email.policy +import sys +import typing +from typing import ( + Any, + Callable, + Dict, + Generic, + List, + Optional, + Tuple, + Type, + Union, + cast, +) + +from . import requirements, specifiers, utils, version as version_module + +T = typing.TypeVar("T") +if sys.version_info[:2] >= (3, 8): # pragma: no cover + from typing import Literal, TypedDict +else: # pragma: no cover + if typing.TYPE_CHECKING: + from typing_extensions import Literal, TypedDict + else: + try: + from typing_extensions import Literal, TypedDict + except ImportError: + + class Literal: + def __init_subclass__(*_args, **_kwargs): + pass + + class TypedDict: + def __init_subclass__(*_args, **_kwargs): + pass + + +try: + ExceptionGroup = __builtins__.ExceptionGroup # type: ignore[attr-defined] +except AttributeError: + + class ExceptionGroup(Exception): # type: ignore[no-redef] # noqa: N818 + """A minimal implementation of :external:exc:`ExceptionGroup` from Python 3.11. + + If :external:exc:`ExceptionGroup` is already defined by Python itself, + that version is used instead. + """ + + message: str + exceptions: List[Exception] + + def __init__(self, message: str, exceptions: List[Exception]) -> None: + self.message = message + self.exceptions = exceptions + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.message!r}, {self.exceptions!r})" + + +class InvalidMetadata(ValueError): + """A metadata field contains invalid data.""" + + field: str + """The name of the field that contains invalid data.""" + + def __init__(self, field: str, message: str) -> None: + self.field = field + super().__init__(message) + + +# The RawMetadata class attempts to make as few assumptions about the underlying +# serialization formats as possible. The idea is that as long as a serialization +# formats offer some very basic primitives in *some* way then we can support +# serializing to and from that format. +class RawMetadata(TypedDict, total=False): + """A dictionary of raw core metadata. + + Each field in core metadata maps to a key of this dictionary (when data is + provided). The key is lower-case and underscores are used instead of dashes + compared to the equivalent core metadata field. Any core metadata field that + can be specified multiple times or can hold multiple values in a single + field have a key with a plural name. See :class:`Metadata` whose attributes + match the keys of this dictionary. + + Core metadata fields that can be specified multiple times are stored as a + list or dict depending on which is appropriate for the field. Any fields + which hold multiple values in a single field are stored as a list. + + """ + + # Metadata 1.0 - PEP 241 + metadata_version: str + name: str + version: str + platforms: List[str] + summary: str + description: str + keywords: List[str] + home_page: str + author: str + author_email: str + license: str + + # Metadata 1.1 - PEP 314 + supported_platforms: List[str] + download_url: str + classifiers: List[str] + requires: List[str] + provides: List[str] + obsoletes: List[str] + + # Metadata 1.2 - PEP 345 + maintainer: str + maintainer_email: str + requires_dist: List[str] + provides_dist: List[str] + obsoletes_dist: List[str] + requires_python: str + requires_external: List[str] + project_urls: Dict[str, str] + + # Metadata 2.0 + # PEP 426 attempted to completely revamp the metadata format + # but got stuck without ever being able to build consensus on + # it and ultimately ended up withdrawn. + # + # However, a number of tools had started emitting METADATA with + # `2.0` Metadata-Version, so for historical reasons, this version + # was skipped. + + # Metadata 2.1 - PEP 566 + description_content_type: str + provides_extra: List[str] + + # Metadata 2.2 - PEP 643 + dynamic: List[str] + + # Metadata 2.3 - PEP 685 + # No new fields were added in PEP 685, just some edge case were + # tightened up to provide better interoptability. + + +_STRING_FIELDS = { + "author", + "author_email", + "description", + "description_content_type", + "download_url", + "home_page", + "license", + "maintainer", + "maintainer_email", + "metadata_version", + "name", + "requires_python", + "summary", + "version", +} + +_LIST_FIELDS = { + "classifiers", + "dynamic", + "obsoletes", + "obsoletes_dist", + "platforms", + "provides", + "provides_dist", + "provides_extra", + "requires", + "requires_dist", + "requires_external", + "supported_platforms", +} + +_DICT_FIELDS = { + "project_urls", +} + + +def _parse_keywords(data: str) -> List[str]: + """Split a string of comma-separate keyboards into a list of keywords.""" + return [k.strip() for k in data.split(",")] + + +def _parse_project_urls(data: List[str]) -> Dict[str, str]: + """Parse a list of label/URL string pairings separated by a comma.""" + urls = {} + for pair in data: + # Our logic is slightly tricky here as we want to try and do + # *something* reasonable with malformed data. + # + # The main thing that we have to worry about, is data that does + # not have a ',' at all to split the label from the Value. There + # isn't a singular right answer here, and we will fail validation + # later on (if the caller is validating) so it doesn't *really* + # matter, but since the missing value has to be an empty str + # and our return value is dict[str, str], if we let the key + # be the missing value, then they'd have multiple '' values that + # overwrite each other in a accumulating dict. + # + # The other potentional issue is that it's possible to have the + # same label multiple times in the metadata, with no solid "right" + # answer with what to do in that case. As such, we'll do the only + # thing we can, which is treat the field as unparseable and add it + # to our list of unparsed fields. + parts = [p.strip() for p in pair.split(",", 1)] + parts.extend([""] * (max(0, 2 - len(parts)))) # Ensure 2 items + + # TODO: The spec doesn't say anything about if the keys should be + # considered case sensitive or not... logically they should + # be case-preserving and case-insensitive, but doing that + # would open up more cases where we might have duplicate + # entries. + label, url = parts + if label in urls: + # The label already exists in our set of urls, so this field + # is unparseable, and we can just add the whole thing to our + # unparseable data and stop processing it. + raise KeyError("duplicate labels in project urls") + urls[label] = url + + return urls + + +def _get_payload(msg: email.message.Message, source: Union[bytes, str]) -> str: + """Get the body of the message.""" + # If our source is a str, then our caller has managed encodings for us, + # and we don't need to deal with it. + if isinstance(source, str): + payload: str = msg.get_payload() + return payload + # If our source is a bytes, then we're managing the encoding and we need + # to deal with it. + else: + bpayload: bytes = msg.get_payload(decode=True) + try: + return bpayload.decode("utf8", "strict") + except UnicodeDecodeError: + raise ValueError("payload in an invalid encoding") + + +# The various parse_FORMAT functions here are intended to be as lenient as +# possible in their parsing, while still returning a correctly typed +# RawMetadata. +# +# To aid in this, we also generally want to do as little touching of the +# data as possible, except where there are possibly some historic holdovers +# that make valid data awkward to work with. +# +# While this is a lower level, intermediate format than our ``Metadata`` +# class, some light touch ups can make a massive difference in usability. + +# Map METADATA fields to RawMetadata. +_EMAIL_TO_RAW_MAPPING = { + "author": "author", + "author-email": "author_email", + "classifier": "classifiers", + "description": "description", + "description-content-type": "description_content_type", + "download-url": "download_url", + "dynamic": "dynamic", + "home-page": "home_page", + "keywords": "keywords", + "license": "license", + "maintainer": "maintainer", + "maintainer-email": "maintainer_email", + "metadata-version": "metadata_version", + "name": "name", + "obsoletes": "obsoletes", + "obsoletes-dist": "obsoletes_dist", + "platform": "platforms", + "project-url": "project_urls", + "provides": "provides", + "provides-dist": "provides_dist", + "provides-extra": "provides_extra", + "requires": "requires", + "requires-dist": "requires_dist", + "requires-external": "requires_external", + "requires-python": "requires_python", + "summary": "summary", + "supported-platform": "supported_platforms", + "version": "version", +} +_RAW_TO_EMAIL_MAPPING = {raw: email for email, raw in _EMAIL_TO_RAW_MAPPING.items()} + + +def parse_email(data: Union[bytes, str]) -> Tuple[RawMetadata, Dict[str, List[str]]]: + """Parse a distribution's metadata stored as email headers (e.g. from ``METADATA``). + + This function returns a two-item tuple of dicts. The first dict is of + recognized fields from the core metadata specification. Fields that can be + parsed and translated into Python's built-in types are converted + appropriately. All other fields are left as-is. Fields that are allowed to + appear multiple times are stored as lists. + + The second dict contains all other fields from the metadata. This includes + any unrecognized fields. It also includes any fields which are expected to + be parsed into a built-in type but were not formatted appropriately. Finally, + any fields that are expected to appear only once but are repeated are + included in this dict. + + """ + raw: Dict[str, Union[str, List[str], Dict[str, str]]] = {} + unparsed: Dict[str, List[str]] = {} + + if isinstance(data, str): + parsed = email.parser.Parser(policy=email.policy.compat32).parsestr(data) + else: + parsed = email.parser.BytesParser(policy=email.policy.compat32).parsebytes(data) + + # We have to wrap parsed.keys() in a set, because in the case of multiple + # values for a key (a list), the key will appear multiple times in the + # list of keys, but we're avoiding that by using get_all(). + for name in frozenset(parsed.keys()): + # Header names in RFC are case insensitive, so we'll normalize to all + # lower case to make comparisons easier. + name = name.lower() + + # We use get_all() here, even for fields that aren't multiple use, + # because otherwise someone could have e.g. two Name fields, and we + # would just silently ignore it rather than doing something about it. + headers = parsed.get_all(name) or [] + + # The way the email module works when parsing bytes is that it + # unconditionally decodes the bytes as ascii using the surrogateescape + # handler. When you pull that data back out (such as with get_all() ), + # it looks to see if the str has any surrogate escapes, and if it does + # it wraps it in a Header object instead of returning the string. + # + # As such, we'll look for those Header objects, and fix up the encoding. + value = [] + # Flag if we have run into any issues processing the headers, thus + # signalling that the data belongs in 'unparsed'. + valid_encoding = True + for h in headers: + # It's unclear if this can return more types than just a Header or + # a str, so we'll just assert here to make sure. + assert isinstance(h, (email.header.Header, str)) + + # If it's a header object, we need to do our little dance to get + # the real data out of it. In cases where there is invalid data + # we're going to end up with mojibake, but there's no obvious, good + # way around that without reimplementing parts of the Header object + # ourselves. + # + # That should be fine since, if mojibacked happens, this key is + # going into the unparsed dict anyways. + if isinstance(h, email.header.Header): + # The Header object stores it's data as chunks, and each chunk + # can be independently encoded, so we'll need to check each + # of them. + chunks: List[Tuple[bytes, Optional[str]]] = [] + for bin, encoding in email.header.decode_header(h): + try: + bin.decode("utf8", "strict") + except UnicodeDecodeError: + # Enable mojibake. + encoding = "latin1" + valid_encoding = False + else: + encoding = "utf8" + chunks.append((bin, encoding)) + + # Turn our chunks back into a Header object, then let that + # Header object do the right thing to turn them into a + # string for us. + value.append(str(email.header.make_header(chunks))) + # This is already a string, so just add it. + else: + value.append(h) + + # We've processed all of our values to get them into a list of str, + # but we may have mojibake data, in which case this is an unparsed + # field. + if not valid_encoding: + unparsed[name] = value + continue + + raw_name = _EMAIL_TO_RAW_MAPPING.get(name) + if raw_name is None: + # This is a bit of a weird situation, we've encountered a key that + # we don't know what it means, so we don't know whether it's meant + # to be a list or not. + # + # Since we can't really tell one way or another, we'll just leave it + # as a list, even though it may be a single item list, because that's + # what makes the most sense for email headers. + unparsed[name] = value + continue + + # If this is one of our string fields, then we'll check to see if our + # value is a list of a single item. If it is then we'll assume that + # it was emitted as a single string, and unwrap the str from inside + # the list. + # + # If it's any other kind of data, then we haven't the faintest clue + # what we should parse it as, and we have to just add it to our list + # of unparsed stuff. + if raw_name in _STRING_FIELDS and len(value) == 1: + raw[raw_name] = value[0] + # If this is one of our list of string fields, then we can just assign + # the value, since email *only* has strings, and our get_all() call + # above ensures that this is a list. + elif raw_name in _LIST_FIELDS: + raw[raw_name] = value + # Special Case: Keywords + # The keywords field is implemented in the metadata spec as a str, + # but it conceptually is a list of strings, and is serialized using + # ", ".join(keywords), so we'll do some light data massaging to turn + # this into what it logically is. + elif raw_name == "keywords" and len(value) == 1: + raw[raw_name] = _parse_keywords(value[0]) + # Special Case: Project-URL + # The project urls is implemented in the metadata spec as a list of + # specially-formatted strings that represent a key and a value, which + # is fundamentally a mapping, however the email format doesn't support + # mappings in a sane way, so it was crammed into a list of strings + # instead. + # + # We will do a little light data massaging to turn this into a map as + # it logically should be. + elif raw_name == "project_urls": + try: + raw[raw_name] = _parse_project_urls(value) + except KeyError: + unparsed[name] = value + # Nothing that we've done has managed to parse this, so it'll just + # throw it in our unparseable data and move on. + else: + unparsed[name] = value + + # We need to support getting the Description from the message payload in + # addition to getting it from the the headers. This does mean, though, there + # is the possibility of it being set both ways, in which case we put both + # in 'unparsed' since we don't know which is right. + try: + payload = _get_payload(parsed, data) + except ValueError: + unparsed.setdefault("description", []).append( + parsed.get_payload(decode=isinstance(data, bytes)) + ) + else: + if payload: + # Check to see if we've already got a description, if so then both + # it, and this body move to unparseable. + if "description" in raw: + description_header = cast(str, raw.pop("description")) + unparsed.setdefault("description", []).extend( + [description_header, payload] + ) + elif "description" in unparsed: + unparsed["description"].append(payload) + else: + raw["description"] = payload + + # We need to cast our `raw` to a metadata, because a TypedDict only support + # literal key names, but we're computing our key names on purpose, but the + # way this function is implemented, our `TypedDict` can only have valid key + # names. + return cast(RawMetadata, raw), unparsed + + +_NOT_FOUND = object() + + +# Keep the two values in sync. +_VALID_METADATA_VERSIONS = ["1.0", "1.1", "1.2", "2.1", "2.2", "2.3"] +_MetadataVersion = Literal["1.0", "1.1", "1.2", "2.1", "2.2", "2.3"] + +_REQUIRED_ATTRS = frozenset(["metadata_version", "name", "version"]) + + +class _Validator(Generic[T]): + """Validate a metadata field. + + All _process_*() methods correspond to a core metadata field. The method is + called with the field's raw value. If the raw value is valid it is returned + in its "enriched" form (e.g. ``version.Version`` for the ``Version`` field). + If the raw value is invalid, :exc:`InvalidMetadata` is raised (with a cause + as appropriate). + """ + + name: str + raw_name: str + added: _MetadataVersion + + def __init__( + self, + *, + added: _MetadataVersion = "1.0", + ) -> None: + self.added = added + + def __set_name__(self, _owner: "Metadata", name: str) -> None: + self.name = name + self.raw_name = _RAW_TO_EMAIL_MAPPING[name] + + def __get__(self, instance: "Metadata", _owner: Type["Metadata"]) -> T: + # With Python 3.8, the caching can be replaced with functools.cached_property(). + # No need to check the cache as attribute lookup will resolve into the + # instance's __dict__ before __get__ is called. + cache = instance.__dict__ + try: + value = instance._raw[self.name] # type: ignore[literal-required] + except KeyError: + if self.name in _STRING_FIELDS: + value = "" + elif self.name in _LIST_FIELDS: + value = [] + elif self.name in _DICT_FIELDS: + value = {} + else: # pragma: no cover + assert False + + try: + converter: Callable[[Any], T] = getattr(self, f"_process_{self.name}") + except AttributeError: + pass + else: + value = converter(value) + + cache[self.name] = value + try: + del instance._raw[self.name] # type: ignore[misc] + except KeyError: + pass + + return cast(T, value) + + def _invalid_metadata( + self, msg: str, cause: Optional[Exception] = None + ) -> InvalidMetadata: + exc = InvalidMetadata( + self.raw_name, msg.format_map({"field": repr(self.raw_name)}) + ) + exc.__cause__ = cause + return exc + + def _process_metadata_version(self, value: str) -> _MetadataVersion: + # Implicitly makes Metadata-Version required. + if value not in _VALID_METADATA_VERSIONS: + raise self._invalid_metadata(f"{value!r} is not a valid metadata version") + return cast(_MetadataVersion, value) + + def _process_name(self, value: str) -> str: + if not value: + raise self._invalid_metadata("{field} is a required field") + # Validate the name as a side-effect. + try: + utils.canonicalize_name(value, validate=True) + except utils.InvalidName as exc: + raise self._invalid_metadata( + f"{value!r} is invalid for {{field}}", cause=exc + ) + else: + return value + + def _process_version(self, value: str) -> version_module.Version: + if not value: + raise self._invalid_metadata("{field} is a required field") + try: + return version_module.parse(value) + except version_module.InvalidVersion as exc: + raise self._invalid_metadata( + f"{value!r} is invalid for {{field}}", cause=exc + ) + + def _process_summary(self, value: str) -> str: + """Check the field contains no newlines.""" + if "\n" in value: + raise self._invalid_metadata("{field} must be a single line") + return value + + def _process_description_content_type(self, value: str) -> str: + content_types = {"text/plain", "text/x-rst", "text/markdown"} + message = email.message.EmailMessage() + message["content-type"] = value + + content_type, parameters = ( + # Defaults to `text/plain` if parsing failed. + message.get_content_type().lower(), + message["content-type"].params, + ) + # Check if content-type is valid or defaulted to `text/plain` and thus was + # not parseable. + if content_type not in content_types or content_type not in value.lower(): + raise self._invalid_metadata( + f"{{field}} must be one of {list(content_types)}, not {value!r}" + ) + + charset = parameters.get("charset", "UTF-8") + if charset != "UTF-8": + raise self._invalid_metadata( + f"{{field}} can only specify the UTF-8 charset, not {list(charset)}" + ) + + markdown_variants = {"GFM", "CommonMark"} + variant = parameters.get("variant", "GFM") # Use an acceptable default. + if content_type == "text/markdown" and variant not in markdown_variants: + raise self._invalid_metadata( + f"valid Markdown variants for {{field}} are {list(markdown_variants)}, " + f"not {variant!r}", + ) + return value + + def _process_dynamic(self, value: List[str]) -> List[str]: + for dynamic_field in map(str.lower, value): + if dynamic_field in {"name", "version", "metadata-version"}: + raise self._invalid_metadata( + f"{value!r} is not allowed as a dynamic field" + ) + elif dynamic_field not in _EMAIL_TO_RAW_MAPPING: + raise self._invalid_metadata(f"{value!r} is not a valid dynamic field") + return list(map(str.lower, value)) + + def _process_provides_extra( + self, + value: List[str], + ) -> List[utils.NormalizedName]: + normalized_names = [] + try: + for name in value: + normalized_names.append(utils.canonicalize_name(name, validate=True)) + except utils.InvalidName as exc: + raise self._invalid_metadata( + f"{name!r} is invalid for {{field}}", cause=exc + ) + else: + return normalized_names + + def _process_requires_python(self, value: str) -> specifiers.SpecifierSet: + try: + return specifiers.SpecifierSet(value) + except specifiers.InvalidSpecifier as exc: + raise self._invalid_metadata( + f"{value!r} is invalid for {{field}}", cause=exc + ) + + def _process_requires_dist( + self, + value: List[str], + ) -> List[requirements.Requirement]: + reqs = [] + try: + for req in value: + reqs.append(requirements.Requirement(req)) + except requirements.InvalidRequirement as exc: + raise self._invalid_metadata(f"{req!r} is invalid for {{field}}", cause=exc) + else: + return reqs + + +class Metadata: + """Representation of distribution metadata. + + Compared to :class:`RawMetadata`, this class provides objects representing + metadata fields instead of only using built-in types. Any invalid metadata + will cause :exc:`InvalidMetadata` to be raised (with a + :py:attr:`~BaseException.__cause__` attribute as appropriate). + """ + + _raw: RawMetadata + + @classmethod + def from_raw(cls, data: RawMetadata, *, validate: bool = True) -> "Metadata": + """Create an instance from :class:`RawMetadata`. + + If *validate* is true, all metadata will be validated. All exceptions + related to validation will be gathered and raised as an :class:`ExceptionGroup`. + """ + ins = cls() + ins._raw = data.copy() # Mutations occur due to caching enriched values. + + if validate: + exceptions: List[InvalidMetadata] = [] + try: + metadata_version = ins.metadata_version + metadata_age = _VALID_METADATA_VERSIONS.index(metadata_version) + except InvalidMetadata as metadata_version_exc: + exceptions.append(metadata_version_exc) + metadata_version = None + + # Make sure to check for the fields that are present, the required + # fields (so their absence can be reported). + fields_to_check = frozenset(ins._raw) | _REQUIRED_ATTRS + # Remove fields that have already been checked. + fields_to_check -= {"metadata_version"} + + for key in fields_to_check: + try: + if metadata_version: + # Can't use getattr() as that triggers descriptor protocol which + # will fail due to no value for the instance argument. + try: + field_metadata_version = cls.__dict__[key].added + except KeyError: + exc = InvalidMetadata(key, f"unrecognized field: {key!r}") + exceptions.append(exc) + continue + field_age = _VALID_METADATA_VERSIONS.index( + field_metadata_version + ) + if field_age > metadata_age: + field = _RAW_TO_EMAIL_MAPPING[key] + exc = InvalidMetadata( + field, + "{field} introduced in metadata version " + "{field_metadata_version}, not {metadata_version}", + ) + exceptions.append(exc) + continue + getattr(ins, key) + except InvalidMetadata as exc: + exceptions.append(exc) + + if exceptions: + raise ExceptionGroup("invalid metadata", exceptions) + + return ins + + @classmethod + def from_email( + cls, data: Union[bytes, str], *, validate: bool = True + ) -> "Metadata": + """Parse metadata from email headers. + + If *validate* is true, the metadata will be validated. All exceptions + related to validation will be gathered and raised as an :class:`ExceptionGroup`. + """ + exceptions: list[InvalidMetadata] = [] + raw, unparsed = parse_email(data) + + if validate: + for unparsed_key in unparsed: + if unparsed_key in _EMAIL_TO_RAW_MAPPING: + message = f"{unparsed_key!r} has invalid data" + else: + message = f"unrecognized field: {unparsed_key!r}" + exceptions.append(InvalidMetadata(unparsed_key, message)) + + if exceptions: + raise ExceptionGroup("unparsed", exceptions) + + try: + return cls.from_raw(raw, validate=validate) + except ExceptionGroup as exc_group: + exceptions.extend(exc_group.exceptions) + raise ExceptionGroup("invalid or unparsed metadata", exceptions) from None + + metadata_version: _Validator[_MetadataVersion] = _Validator() + """:external:ref:`core-metadata-metadata-version` + (required; validated to be a valid metadata version)""" + name: _Validator[str] = _Validator() + """:external:ref:`core-metadata-name` + (required; validated using :func:`~packaging.utils.canonicalize_name` and its + *validate* parameter)""" + version: _Validator[version_module.Version] = _Validator() + """:external:ref:`core-metadata-version` (required)""" + dynamic: _Validator[List[str]] = _Validator( + added="2.2", + ) + """:external:ref:`core-metadata-dynamic` + (validated against core metadata field names and lowercased)""" + platforms: _Validator[List[str]] = _Validator() + """:external:ref:`core-metadata-platform`""" + supported_platforms: _Validator[List[str]] = _Validator(added="1.1") + """:external:ref:`core-metadata-supported-platform`""" + summary: _Validator[str] = _Validator() + """:external:ref:`core-metadata-summary` (validated to contain no newlines)""" + description: _Validator[str] = _Validator() # TODO 2.1: can be in body + """:external:ref:`core-metadata-description`""" + description_content_type: _Validator[str] = _Validator(added="2.1") + """:external:ref:`core-metadata-description-content-type` (validated)""" + keywords: _Validator[List[str]] = _Validator() + """:external:ref:`core-metadata-keywords`""" + home_page: _Validator[str] = _Validator() + """:external:ref:`core-metadata-home-page`""" + download_url: _Validator[str] = _Validator(added="1.1") + """:external:ref:`core-metadata-download-url`""" + author: _Validator[str] = _Validator() + """:external:ref:`core-metadata-author`""" + author_email: _Validator[str] = _Validator() + """:external:ref:`core-metadata-author-email`""" + maintainer: _Validator[str] = _Validator(added="1.2") + """:external:ref:`core-metadata-maintainer`""" + maintainer_email: _Validator[str] = _Validator(added="1.2") + """:external:ref:`core-metadata-maintainer-email`""" + license: _Validator[str] = _Validator() + """:external:ref:`core-metadata-license`""" + classifiers: _Validator[List[str]] = _Validator(added="1.1") + """:external:ref:`core-metadata-classifier`""" + requires_dist: _Validator[List[requirements.Requirement]] = _Validator(added="1.2") + """:external:ref:`core-metadata-requires-dist`""" + requires_python: _Validator[specifiers.SpecifierSet] = _Validator(added="1.2") + """:external:ref:`core-metadata-requires-python`""" + # Because `Requires-External` allows for non-PEP 440 version specifiers, we + # don't do any processing on the values. + requires_external: _Validator[List[str]] = _Validator(added="1.2") + """:external:ref:`core-metadata-requires-external`""" + project_urls: _Validator[Dict[str, str]] = _Validator(added="1.2") + """:external:ref:`core-metadata-project-url`""" + # PEP 685 lets us raise an error if an extra doesn't pass `Name` validation + # regardless of metadata version. + provides_extra: _Validator[List[utils.NormalizedName]] = _Validator( + added="2.1", + ) + """:external:ref:`core-metadata-provides-extra`""" + provides_dist: _Validator[List[str]] = _Validator(added="1.2") + """:external:ref:`core-metadata-provides-dist`""" + obsoletes_dist: _Validator[List[str]] = _Validator(added="1.2") + """:external:ref:`core-metadata-obsoletes-dist`""" + requires: _Validator[List[str]] = _Validator(added="1.1") + """``Requires`` (deprecated)""" + provides: _Validator[List[str]] = _Validator(added="1.1") + """``Provides`` (deprecated)""" + obsoletes: _Validator[List[str]] = _Validator(added="1.1") + """``Obsoletes`` (deprecated)""" diff --git a/libs/common/packaging/py.typed b/libs/common/packaging/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/libs/common/packaging/requirements.py b/libs/common/packaging/requirements.py new file mode 100644 index 00000000..0c00eba3 --- /dev/null +++ b/libs/common/packaging/requirements.py @@ -0,0 +1,90 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from typing import Any, Iterator, Optional, Set + +from ._parser import parse_requirement as _parse_requirement +from ._tokenizer import ParserSyntaxError +from .markers import Marker, _normalize_extra_values +from .specifiers import SpecifierSet +from .utils import canonicalize_name + + +class InvalidRequirement(ValueError): + """ + An invalid requirement was found, users should refer to PEP 508. + """ + + +class Requirement: + """Parse a requirement. + + Parse a given requirement string into its parts, such as name, specifier, + URL, and extras. Raises InvalidRequirement on a badly-formed requirement + string. + """ + + # TODO: Can we test whether something is contained within a requirement? + # If so how do we do that? Do we need to test against the _name_ of + # the thing as well as the version? What about the markers? + # TODO: Can we normalize the name and extra name? + + def __init__(self, requirement_string: str) -> None: + try: + parsed = _parse_requirement(requirement_string) + except ParserSyntaxError as e: + raise InvalidRequirement(str(e)) from e + + self.name: str = parsed.name + self.url: Optional[str] = parsed.url or None + self.extras: Set[str] = set(parsed.extras if parsed.extras else []) + self.specifier: SpecifierSet = SpecifierSet(parsed.specifier) + self.marker: Optional[Marker] = None + if parsed.marker is not None: + self.marker = Marker.__new__(Marker) + self.marker._markers = _normalize_extra_values(parsed.marker) + + def _iter_parts(self, name: str) -> Iterator[str]: + yield name + + if self.extras: + formatted_extras = ",".join(sorted(self.extras)) + yield f"[{formatted_extras}]" + + if self.specifier: + yield str(self.specifier) + + if self.url: + yield f"@ {self.url}" + if self.marker: + yield " " + + if self.marker: + yield f"; {self.marker}" + + def __str__(self) -> str: + return "".join(self._iter_parts(self.name)) + + def __repr__(self) -> str: + return f"" + + def __hash__(self) -> int: + return hash( + ( + self.__class__.__name__, + *self._iter_parts(canonicalize_name(self.name)), + ) + ) + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, Requirement): + return NotImplemented + + return ( + canonicalize_name(self.name) == canonicalize_name(other.name) + and self.extras == other.extras + and self.specifier == other.specifier + and self.url == other.url + and self.marker == other.marker + ) diff --git a/libs/common/packaging/specifiers.py b/libs/common/packaging/specifiers.py new file mode 100644 index 00000000..ba8fe37b --- /dev/null +++ b/libs/common/packaging/specifiers.py @@ -0,0 +1,1008 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. +""" +.. testsetup:: + + from packaging.specifiers import Specifier, SpecifierSet, InvalidSpecifier + from packaging.version import Version +""" + +import abc +import itertools +import re +from typing import ( + Callable, + Iterable, + Iterator, + List, + Optional, + Set, + Tuple, + TypeVar, + Union, +) + +from .utils import canonicalize_version +from .version import Version + +UnparsedVersion = Union[Version, str] +UnparsedVersionVar = TypeVar("UnparsedVersionVar", bound=UnparsedVersion) +CallableOperator = Callable[[Version, str], bool] + + +def _coerce_version(version: UnparsedVersion) -> Version: + if not isinstance(version, Version): + version = Version(version) + return version + + +class InvalidSpecifier(ValueError): + """ + Raised when attempting to create a :class:`Specifier` with a specifier + string that is invalid. + + >>> Specifier("lolwat") + Traceback (most recent call last): + ... + packaging.specifiers.InvalidSpecifier: Invalid specifier: 'lolwat' + """ + + +class BaseSpecifier(metaclass=abc.ABCMeta): + @abc.abstractmethod + def __str__(self) -> str: + """ + Returns the str representation of this Specifier-like object. This + should be representative of the Specifier itself. + """ + + @abc.abstractmethod + def __hash__(self) -> int: + """ + Returns a hash value for this Specifier-like object. + """ + + @abc.abstractmethod + def __eq__(self, other: object) -> bool: + """ + Returns a boolean representing whether or not the two Specifier-like + objects are equal. + + :param other: The other object to check against. + """ + + @property + @abc.abstractmethod + def prereleases(self) -> Optional[bool]: + """Whether or not pre-releases as a whole are allowed. + + This can be set to either ``True`` or ``False`` to explicitly enable or disable + prereleases or it can be set to ``None`` (the default) to use default semantics. + """ + + @prereleases.setter + def prereleases(self, value: bool) -> None: + """Setter for :attr:`prereleases`. + + :param value: The value to set. + """ + + @abc.abstractmethod + def contains(self, item: str, prereleases: Optional[bool] = None) -> bool: + """ + Determines if the given item is contained within this specifier. + """ + + @abc.abstractmethod + def filter( + self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None + ) -> Iterator[UnparsedVersionVar]: + """ + Takes an iterable of items and filters them so that only items which + are contained within this specifier are allowed in it. + """ + + +class Specifier(BaseSpecifier): + """This class abstracts handling of version specifiers. + + .. tip:: + + It is generally not required to instantiate this manually. You should instead + prefer to work with :class:`SpecifierSet` instead, which can parse + comma-separated version specifiers (which is what package metadata contains). + """ + + _operator_regex_str = r""" + (?P(~=|==|!=|<=|>=|<|>|===)) + """ + _version_regex_str = r""" + (?P + (?: + # The identity operators allow for an escape hatch that will + # do an exact string match of the version you wish to install. + # This will not be parsed by PEP 440 and we cannot determine + # any semantic meaning from it. This operator is discouraged + # but included entirely as an escape hatch. + (?<====) # Only match for the identity operator + \s* + [^\s;)]* # The arbitrary version can be just about anything, + # we match everything except for whitespace, a + # semi-colon for marker support, and a closing paren + # since versions can be enclosed in them. + ) + | + (?: + # The (non)equality operators allow for wild card and local + # versions to be specified so we have to define these two + # operators separately to enable that. + (?<===|!=) # Only match for equals and not equals + + \s* + v? + (?:[0-9]+!)? # epoch + [0-9]+(?:\.[0-9]+)* # release + + # You cannot use a wild card and a pre-release, post-release, a dev or + # local version together so group them with a | and make them optional. + (?: + \.\* # Wild card syntax of .* + | + (?: # pre release + [-_\.]? + (alpha|beta|preview|pre|a|b|c|rc) + [-_\.]? + [0-9]* + )? + (?: # post release + (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*) + )? + (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release + (?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)? # local + )? + ) + | + (?: + # The compatible operator requires at least two digits in the + # release segment. + (?<=~=) # Only match for the compatible operator + + \s* + v? + (?:[0-9]+!)? # epoch + [0-9]+(?:\.[0-9]+)+ # release (We have a + instead of a *) + (?: # pre release + [-_\.]? + (alpha|beta|preview|pre|a|b|c|rc) + [-_\.]? + [0-9]* + )? + (?: # post release + (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*) + )? + (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release + ) + | + (?: + # All other operators only allow a sub set of what the + # (non)equality operators do. Specifically they do not allow + # local versions to be specified nor do they allow the prefix + # matching wild cards. + (?=": "greater_than_equal", + "<": "less_than", + ">": "greater_than", + "===": "arbitrary", + } + + def __init__(self, spec: str = "", prereleases: Optional[bool] = None) -> None: + """Initialize a Specifier instance. + + :param spec: + The string representation of a specifier which will be parsed and + normalized before use. + :param prereleases: + This tells the specifier if it should accept prerelease versions if + applicable or not. The default of ``None`` will autodetect it from the + given specifiers. + :raises InvalidSpecifier: + If the given specifier is invalid (i.e. bad syntax). + """ + match = self._regex.search(spec) + if not match: + raise InvalidSpecifier(f"Invalid specifier: '{spec}'") + + self._spec: Tuple[str, str] = ( + match.group("operator").strip(), + match.group("version").strip(), + ) + + # Store whether or not this Specifier should accept prereleases + self._prereleases = prereleases + + # https://github.com/python/mypy/pull/13475#pullrequestreview-1079784515 + @property # type: ignore[override] + def prereleases(self) -> bool: + # If there is an explicit prereleases set for this, then we'll just + # blindly use that. + if self._prereleases is not None: + return self._prereleases + + # Look at all of our specifiers and determine if they are inclusive + # operators, and if they are if they are including an explicit + # prerelease. + operator, version = self._spec + if operator in ["==", ">=", "<=", "~=", "==="]: + # The == specifier can include a trailing .*, if it does we + # want to remove before parsing. + if operator == "==" and version.endswith(".*"): + version = version[:-2] + + # Parse the version, and if it is a pre-release than this + # specifier allows pre-releases. + if Version(version).is_prerelease: + return True + + return False + + @prereleases.setter + def prereleases(self, value: bool) -> None: + self._prereleases = value + + @property + def operator(self) -> str: + """The operator of this specifier. + + >>> Specifier("==1.2.3").operator + '==' + """ + return self._spec[0] + + @property + def version(self) -> str: + """The version of this specifier. + + >>> Specifier("==1.2.3").version + '1.2.3' + """ + return self._spec[1] + + def __repr__(self) -> str: + """A representation of the Specifier that shows all internal state. + + >>> Specifier('>=1.0.0') + =1.0.0')> + >>> Specifier('>=1.0.0', prereleases=False) + =1.0.0', prereleases=False)> + >>> Specifier('>=1.0.0', prereleases=True) + =1.0.0', prereleases=True)> + """ + pre = ( + f", prereleases={self.prereleases!r}" + if self._prereleases is not None + else "" + ) + + return f"<{self.__class__.__name__}({str(self)!r}{pre})>" + + def __str__(self) -> str: + """A string representation of the Specifier that can be round-tripped. + + >>> str(Specifier('>=1.0.0')) + '>=1.0.0' + >>> str(Specifier('>=1.0.0', prereleases=False)) + '>=1.0.0' + """ + return "{}{}".format(*self._spec) + + @property + def _canonical_spec(self) -> Tuple[str, str]: + canonical_version = canonicalize_version( + self._spec[1], + strip_trailing_zero=(self._spec[0] != "~="), + ) + return self._spec[0], canonical_version + + def __hash__(self) -> int: + return hash(self._canonical_spec) + + def __eq__(self, other: object) -> bool: + """Whether or not the two Specifier-like objects are equal. + + :param other: The other object to check against. + + The value of :attr:`prereleases` is ignored. + + >>> Specifier("==1.2.3") == Specifier("== 1.2.3.0") + True + >>> (Specifier("==1.2.3", prereleases=False) == + ... Specifier("==1.2.3", prereleases=True)) + True + >>> Specifier("==1.2.3") == "==1.2.3" + True + >>> Specifier("==1.2.3") == Specifier("==1.2.4") + False + >>> Specifier("==1.2.3") == Specifier("~=1.2.3") + False + """ + if isinstance(other, str): + try: + other = self.__class__(str(other)) + except InvalidSpecifier: + return NotImplemented + elif not isinstance(other, self.__class__): + return NotImplemented + + return self._canonical_spec == other._canonical_spec + + def _get_operator(self, op: str) -> CallableOperator: + operator_callable: CallableOperator = getattr( + self, f"_compare_{self._operators[op]}" + ) + return operator_callable + + def _compare_compatible(self, prospective: Version, spec: str) -> bool: + + # Compatible releases have an equivalent combination of >= and ==. That + # is that ~=2.2 is equivalent to >=2.2,==2.*. This allows us to + # implement this in terms of the other specifiers instead of + # implementing it ourselves. The only thing we need to do is construct + # the other specifiers. + + # We want everything but the last item in the version, but we want to + # ignore suffix segments. + prefix = ".".join( + list(itertools.takewhile(_is_not_suffix, _version_split(spec)))[:-1] + ) + + # Add the prefix notation to the end of our string + prefix += ".*" + + return self._get_operator(">=")(prospective, spec) and self._get_operator("==")( + prospective, prefix + ) + + def _compare_equal(self, prospective: Version, spec: str) -> bool: + + # We need special logic to handle prefix matching + if spec.endswith(".*"): + # In the case of prefix matching we want to ignore local segment. + normalized_prospective = canonicalize_version( + prospective.public, strip_trailing_zero=False + ) + # Get the normalized version string ignoring the trailing .* + normalized_spec = canonicalize_version(spec[:-2], strip_trailing_zero=False) + # Split the spec out by dots, and pretend that there is an implicit + # dot in between a release segment and a pre-release segment. + split_spec = _version_split(normalized_spec) + + # Split the prospective version out by dots, and pretend that there + # is an implicit dot in between a release segment and a pre-release + # segment. + split_prospective = _version_split(normalized_prospective) + + # 0-pad the prospective version before shortening it to get the correct + # shortened version. + padded_prospective, _ = _pad_version(split_prospective, split_spec) + + # Shorten the prospective version to be the same length as the spec + # so that we can determine if the specifier is a prefix of the + # prospective version or not. + shortened_prospective = padded_prospective[: len(split_spec)] + + return shortened_prospective == split_spec + else: + # Convert our spec string into a Version + spec_version = Version(spec) + + # If the specifier does not have a local segment, then we want to + # act as if the prospective version also does not have a local + # segment. + if not spec_version.local: + prospective = Version(prospective.public) + + return prospective == spec_version + + def _compare_not_equal(self, prospective: Version, spec: str) -> bool: + return not self._compare_equal(prospective, spec) + + def _compare_less_than_equal(self, prospective: Version, spec: str) -> bool: + + # NB: Local version identifiers are NOT permitted in the version + # specifier, so local version labels can be universally removed from + # the prospective version. + return Version(prospective.public) <= Version(spec) + + def _compare_greater_than_equal(self, prospective: Version, spec: str) -> bool: + + # NB: Local version identifiers are NOT permitted in the version + # specifier, so local version labels can be universally removed from + # the prospective version. + return Version(prospective.public) >= Version(spec) + + def _compare_less_than(self, prospective: Version, spec_str: str) -> bool: + + # Convert our spec to a Version instance, since we'll want to work with + # it as a version. + spec = Version(spec_str) + + # Check to see if the prospective version is less than the spec + # version. If it's not we can short circuit and just return False now + # instead of doing extra unneeded work. + if not prospective < spec: + return False + + # This special case is here so that, unless the specifier itself + # includes is a pre-release version, that we do not accept pre-release + # versions for the version mentioned in the specifier (e.g. <3.1 should + # not match 3.1.dev0, but should match 3.0.dev0). + if not spec.is_prerelease and prospective.is_prerelease: + if Version(prospective.base_version) == Version(spec.base_version): + return False + + # If we've gotten to here, it means that prospective version is both + # less than the spec version *and* it's not a pre-release of the same + # version in the spec. + return True + + def _compare_greater_than(self, prospective: Version, spec_str: str) -> bool: + + # Convert our spec to a Version instance, since we'll want to work with + # it as a version. + spec = Version(spec_str) + + # Check to see if the prospective version is greater than the spec + # version. If it's not we can short circuit and just return False now + # instead of doing extra unneeded work. + if not prospective > spec: + return False + + # This special case is here so that, unless the specifier itself + # includes is a post-release version, that we do not accept + # post-release versions for the version mentioned in the specifier + # (e.g. >3.1 should not match 3.0.post0, but should match 3.2.post0). + if not spec.is_postrelease and prospective.is_postrelease: + if Version(prospective.base_version) == Version(spec.base_version): + return False + + # Ensure that we do not allow a local version of the version mentioned + # in the specifier, which is technically greater than, to match. + if prospective.local is not None: + if Version(prospective.base_version) == Version(spec.base_version): + return False + + # If we've gotten to here, it means that prospective version is both + # greater than the spec version *and* it's not a pre-release of the + # same version in the spec. + return True + + def _compare_arbitrary(self, prospective: Version, spec: str) -> bool: + return str(prospective).lower() == str(spec).lower() + + def __contains__(self, item: Union[str, Version]) -> bool: + """Return whether or not the item is contained in this specifier. + + :param item: The item to check for. + + This is used for the ``in`` operator and behaves the same as + :meth:`contains` with no ``prereleases`` argument passed. + + >>> "1.2.3" in Specifier(">=1.2.3") + True + >>> Version("1.2.3") in Specifier(">=1.2.3") + True + >>> "1.0.0" in Specifier(">=1.2.3") + False + >>> "1.3.0a1" in Specifier(">=1.2.3") + False + >>> "1.3.0a1" in Specifier(">=1.2.3", prereleases=True) + True + """ + return self.contains(item) + + def contains( + self, item: UnparsedVersion, prereleases: Optional[bool] = None + ) -> bool: + """Return whether or not the item is contained in this specifier. + + :param item: + The item to check for, which can be a version string or a + :class:`Version` instance. + :param prereleases: + Whether or not to match prereleases with this Specifier. If set to + ``None`` (the default), it uses :attr:`prereleases` to determine + whether or not prereleases are allowed. + + >>> Specifier(">=1.2.3").contains("1.2.3") + True + >>> Specifier(">=1.2.3").contains(Version("1.2.3")) + True + >>> Specifier(">=1.2.3").contains("1.0.0") + False + >>> Specifier(">=1.2.3").contains("1.3.0a1") + False + >>> Specifier(">=1.2.3", prereleases=True).contains("1.3.0a1") + True + >>> Specifier(">=1.2.3").contains("1.3.0a1", prereleases=True) + True + """ + + # Determine if prereleases are to be allowed or not. + if prereleases is None: + prereleases = self.prereleases + + # Normalize item to a Version, this allows us to have a shortcut for + # "2.0" in Specifier(">=2") + normalized_item = _coerce_version(item) + + # Determine if we should be supporting prereleases in this specifier + # or not, if we do not support prereleases than we can short circuit + # logic if this version is a prereleases. + if normalized_item.is_prerelease and not prereleases: + return False + + # Actually do the comparison to determine if this item is contained + # within this Specifier or not. + operator_callable: CallableOperator = self._get_operator(self.operator) + return operator_callable(normalized_item, self.version) + + def filter( + self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None + ) -> Iterator[UnparsedVersionVar]: + """Filter items in the given iterable, that match the specifier. + + :param iterable: + An iterable that can contain version strings and :class:`Version` instances. + The items in the iterable will be filtered according to the specifier. + :param prereleases: + Whether or not to allow prereleases in the returned iterator. If set to + ``None`` (the default), it will be intelligently decide whether to allow + prereleases or not (based on the :attr:`prereleases` attribute, and + whether the only versions matching are prereleases). + + This method is smarter than just ``filter(Specifier().contains, [...])`` + because it implements the rule from :pep:`440` that a prerelease item + SHOULD be accepted if no other versions match the given specifier. + + >>> list(Specifier(">=1.2.3").filter(["1.2", "1.3", "1.5a1"])) + ['1.3'] + >>> list(Specifier(">=1.2.3").filter(["1.2", "1.2.3", "1.3", Version("1.4")])) + ['1.2.3', '1.3', ] + >>> list(Specifier(">=1.2.3").filter(["1.2", "1.5a1"])) + ['1.5a1'] + >>> list(Specifier(">=1.2.3").filter(["1.3", "1.5a1"], prereleases=True)) + ['1.3', '1.5a1'] + >>> list(Specifier(">=1.2.3", prereleases=True).filter(["1.3", "1.5a1"])) + ['1.3', '1.5a1'] + """ + + yielded = False + found_prereleases = [] + + kw = {"prereleases": prereleases if prereleases is not None else True} + + # Attempt to iterate over all the values in the iterable and if any of + # them match, yield them. + for version in iterable: + parsed_version = _coerce_version(version) + + if self.contains(parsed_version, **kw): + # If our version is a prerelease, and we were not set to allow + # prereleases, then we'll store it for later in case nothing + # else matches this specifier. + if parsed_version.is_prerelease and not ( + prereleases or self.prereleases + ): + found_prereleases.append(version) + # Either this is not a prerelease, or we should have been + # accepting prereleases from the beginning. + else: + yielded = True + yield version + + # Now that we've iterated over everything, determine if we've yielded + # any values, and if we have not and we have any prereleases stored up + # then we will go ahead and yield the prereleases. + if not yielded and found_prereleases: + for version in found_prereleases: + yield version + + +_prefix_regex = re.compile(r"^([0-9]+)((?:a|b|c|rc)[0-9]+)$") + + +def _version_split(version: str) -> List[str]: + result: List[str] = [] + for item in version.split("."): + match = _prefix_regex.search(item) + if match: + result.extend(match.groups()) + else: + result.append(item) + return result + + +def _is_not_suffix(segment: str) -> bool: + return not any( + segment.startswith(prefix) for prefix in ("dev", "a", "b", "rc", "post") + ) + + +def _pad_version(left: List[str], right: List[str]) -> Tuple[List[str], List[str]]: + left_split, right_split = [], [] + + # Get the release segment of our versions + left_split.append(list(itertools.takewhile(lambda x: x.isdigit(), left))) + right_split.append(list(itertools.takewhile(lambda x: x.isdigit(), right))) + + # Get the rest of our versions + left_split.append(left[len(left_split[0]) :]) + right_split.append(right[len(right_split[0]) :]) + + # Insert our padding + left_split.insert(1, ["0"] * max(0, len(right_split[0]) - len(left_split[0]))) + right_split.insert(1, ["0"] * max(0, len(left_split[0]) - len(right_split[0]))) + + return (list(itertools.chain(*left_split)), list(itertools.chain(*right_split))) + + +class SpecifierSet(BaseSpecifier): + """This class abstracts handling of a set of version specifiers. + + It can be passed a single specifier (``>=3.0``), a comma-separated list of + specifiers (``>=3.0,!=3.1``), or no specifier at all. + """ + + def __init__( + self, specifiers: str = "", prereleases: Optional[bool] = None + ) -> None: + """Initialize a SpecifierSet instance. + + :param specifiers: + The string representation of a specifier or a comma-separated list of + specifiers which will be parsed and normalized before use. + :param prereleases: + This tells the SpecifierSet if it should accept prerelease versions if + applicable or not. The default of ``None`` will autodetect it from the + given specifiers. + + :raises InvalidSpecifier: + If the given ``specifiers`` are not parseable than this exception will be + raised. + """ + + # Split on `,` to break each individual specifier into it's own item, and + # strip each item to remove leading/trailing whitespace. + split_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()] + + # Parsed each individual specifier, attempting first to make it a + # Specifier. + parsed: Set[Specifier] = set() + for specifier in split_specifiers: + parsed.add(Specifier(specifier)) + + # Turn our parsed specifiers into a frozen set and save them for later. + self._specs = frozenset(parsed) + + # Store our prereleases value so we can use it later to determine if + # we accept prereleases or not. + self._prereleases = prereleases + + @property + def prereleases(self) -> Optional[bool]: + # If we have been given an explicit prerelease modifier, then we'll + # pass that through here. + if self._prereleases is not None: + return self._prereleases + + # If we don't have any specifiers, and we don't have a forced value, + # then we'll just return None since we don't know if this should have + # pre-releases or not. + if not self._specs: + return None + + # Otherwise we'll see if any of the given specifiers accept + # prereleases, if any of them do we'll return True, otherwise False. + return any(s.prereleases for s in self._specs) + + @prereleases.setter + def prereleases(self, value: bool) -> None: + self._prereleases = value + + def __repr__(self) -> str: + """A representation of the specifier set that shows all internal state. + + Note that the ordering of the individual specifiers within the set may not + match the input string. + + >>> SpecifierSet('>=1.0.0,!=2.0.0') + =1.0.0')> + >>> SpecifierSet('>=1.0.0,!=2.0.0', prereleases=False) + =1.0.0', prereleases=False)> + >>> SpecifierSet('>=1.0.0,!=2.0.0', prereleases=True) + =1.0.0', prereleases=True)> + """ + pre = ( + f", prereleases={self.prereleases!r}" + if self._prereleases is not None + else "" + ) + + return f"" + + def __str__(self) -> str: + """A string representation of the specifier set that can be round-tripped. + + Note that the ordering of the individual specifiers within the set may not + match the input string. + + >>> str(SpecifierSet(">=1.0.0,!=1.0.1")) + '!=1.0.1,>=1.0.0' + >>> str(SpecifierSet(">=1.0.0,!=1.0.1", prereleases=False)) + '!=1.0.1,>=1.0.0' + """ + return ",".join(sorted(str(s) for s in self._specs)) + + def __hash__(self) -> int: + return hash(self._specs) + + def __and__(self, other: Union["SpecifierSet", str]) -> "SpecifierSet": + """Return a SpecifierSet which is a combination of the two sets. + + :param other: The other object to combine with. + + >>> SpecifierSet(">=1.0.0,!=1.0.1") & '<=2.0.0,!=2.0.1' + =1.0.0')> + >>> SpecifierSet(">=1.0.0,!=1.0.1") & SpecifierSet('<=2.0.0,!=2.0.1') + =1.0.0')> + """ + if isinstance(other, str): + other = SpecifierSet(other) + elif not isinstance(other, SpecifierSet): + return NotImplemented + + specifier = SpecifierSet() + specifier._specs = frozenset(self._specs | other._specs) + + if self._prereleases is None and other._prereleases is not None: + specifier._prereleases = other._prereleases + elif self._prereleases is not None and other._prereleases is None: + specifier._prereleases = self._prereleases + elif self._prereleases == other._prereleases: + specifier._prereleases = self._prereleases + else: + raise ValueError( + "Cannot combine SpecifierSets with True and False prerelease " + "overrides." + ) + + return specifier + + def __eq__(self, other: object) -> bool: + """Whether or not the two SpecifierSet-like objects are equal. + + :param other: The other object to check against. + + The value of :attr:`prereleases` is ignored. + + >>> SpecifierSet(">=1.0.0,!=1.0.1") == SpecifierSet(">=1.0.0,!=1.0.1") + True + >>> (SpecifierSet(">=1.0.0,!=1.0.1", prereleases=False) == + ... SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True)) + True + >>> SpecifierSet(">=1.0.0,!=1.0.1") == ">=1.0.0,!=1.0.1" + True + >>> SpecifierSet(">=1.0.0,!=1.0.1") == SpecifierSet(">=1.0.0") + False + >>> SpecifierSet(">=1.0.0,!=1.0.1") == SpecifierSet(">=1.0.0,!=1.0.2") + False + """ + if isinstance(other, (str, Specifier)): + other = SpecifierSet(str(other)) + elif not isinstance(other, SpecifierSet): + return NotImplemented + + return self._specs == other._specs + + def __len__(self) -> int: + """Returns the number of specifiers in this specifier set.""" + return len(self._specs) + + def __iter__(self) -> Iterator[Specifier]: + """ + Returns an iterator over all the underlying :class:`Specifier` instances + in this specifier set. + + >>> sorted(SpecifierSet(">=1.0.0,!=1.0.1"), key=str) + [, =1.0.0')>] + """ + return iter(self._specs) + + def __contains__(self, item: UnparsedVersion) -> bool: + """Return whether or not the item is contained in this specifier. + + :param item: The item to check for. + + This is used for the ``in`` operator and behaves the same as + :meth:`contains` with no ``prereleases`` argument passed. + + >>> "1.2.3" in SpecifierSet(">=1.0.0,!=1.0.1") + True + >>> Version("1.2.3") in SpecifierSet(">=1.0.0,!=1.0.1") + True + >>> "1.0.1" in SpecifierSet(">=1.0.0,!=1.0.1") + False + >>> "1.3.0a1" in SpecifierSet(">=1.0.0,!=1.0.1") + False + >>> "1.3.0a1" in SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True) + True + """ + return self.contains(item) + + def contains( + self, + item: UnparsedVersion, + prereleases: Optional[bool] = None, + installed: Optional[bool] = None, + ) -> bool: + """Return whether or not the item is contained in this SpecifierSet. + + :param item: + The item to check for, which can be a version string or a + :class:`Version` instance. + :param prereleases: + Whether or not to match prereleases with this SpecifierSet. If set to + ``None`` (the default), it uses :attr:`prereleases` to determine + whether or not prereleases are allowed. + + >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.2.3") + True + >>> SpecifierSet(">=1.0.0,!=1.0.1").contains(Version("1.2.3")) + True + >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.0.1") + False + >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.3.0a1") + False + >>> SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True).contains("1.3.0a1") + True + >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.3.0a1", prereleases=True) + True + """ + # Ensure that our item is a Version instance. + if not isinstance(item, Version): + item = Version(item) + + # Determine if we're forcing a prerelease or not, if we're not forcing + # one for this particular filter call, then we'll use whatever the + # SpecifierSet thinks for whether or not we should support prereleases. + if prereleases is None: + prereleases = self.prereleases + + # We can determine if we're going to allow pre-releases by looking to + # see if any of the underlying items supports them. If none of them do + # and this item is a pre-release then we do not allow it and we can + # short circuit that here. + # Note: This means that 1.0.dev1 would not be contained in something + # like >=1.0.devabc however it would be in >=1.0.debabc,>0.0.dev0 + if not prereleases and item.is_prerelease: + return False + + if installed and item.is_prerelease: + item = Version(item.base_version) + + # We simply dispatch to the underlying specs here to make sure that the + # given version is contained within all of them. + # Note: This use of all() here means that an empty set of specifiers + # will always return True, this is an explicit design decision. + return all(s.contains(item, prereleases=prereleases) for s in self._specs) + + def filter( + self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None + ) -> Iterator[UnparsedVersionVar]: + """Filter items in the given iterable, that match the specifiers in this set. + + :param iterable: + An iterable that can contain version strings and :class:`Version` instances. + The items in the iterable will be filtered according to the specifier. + :param prereleases: + Whether or not to allow prereleases in the returned iterator. If set to + ``None`` (the default), it will be intelligently decide whether to allow + prereleases or not (based on the :attr:`prereleases` attribute, and + whether the only versions matching are prereleases). + + This method is smarter than just ``filter(SpecifierSet(...).contains, [...])`` + because it implements the rule from :pep:`440` that a prerelease item + SHOULD be accepted if no other versions match the given specifier. + + >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.3", "1.5a1"])) + ['1.3'] + >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.3", Version("1.4")])) + ['1.3', ] + >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.5a1"])) + [] + >>> list(SpecifierSet(">=1.2.3").filter(["1.3", "1.5a1"], prereleases=True)) + ['1.3', '1.5a1'] + >>> list(SpecifierSet(">=1.2.3", prereleases=True).filter(["1.3", "1.5a1"])) + ['1.3', '1.5a1'] + + An "empty" SpecifierSet will filter items based on the presence of prerelease + versions in the set. + + >>> list(SpecifierSet("").filter(["1.3", "1.5a1"])) + ['1.3'] + >>> list(SpecifierSet("").filter(["1.5a1"])) + ['1.5a1'] + >>> list(SpecifierSet("", prereleases=True).filter(["1.3", "1.5a1"])) + ['1.3', '1.5a1'] + >>> list(SpecifierSet("").filter(["1.3", "1.5a1"], prereleases=True)) + ['1.3', '1.5a1'] + """ + # Determine if we're forcing a prerelease or not, if we're not forcing + # one for this particular filter call, then we'll use whatever the + # SpecifierSet thinks for whether or not we should support prereleases. + if prereleases is None: + prereleases = self.prereleases + + # If we have any specifiers, then we want to wrap our iterable in the + # filter method for each one, this will act as a logical AND amongst + # each specifier. + if self._specs: + for spec in self._specs: + iterable = spec.filter(iterable, prereleases=bool(prereleases)) + return iter(iterable) + # If we do not have any specifiers, then we need to have a rough filter + # which will filter out any pre-releases, unless there are no final + # releases. + else: + filtered: List[UnparsedVersionVar] = [] + found_prereleases: List[UnparsedVersionVar] = [] + + for item in iterable: + parsed_version = _coerce_version(item) + + # Store any item which is a pre-release for later unless we've + # already found a final version or we are accepting prereleases + if parsed_version.is_prerelease and not prereleases: + if not filtered: + found_prereleases.append(item) + else: + filtered.append(item) + + # If we've found no items except for pre-releases, then we'll go + # ahead and use the pre-releases + if not filtered and found_prereleases and prereleases is None: + return iter(found_prereleases) + + return iter(filtered) diff --git a/libs/common/packaging/tags.py b/libs/common/packaging/tags.py new file mode 100644 index 00000000..37f33b1e --- /dev/null +++ b/libs/common/packaging/tags.py @@ -0,0 +1,553 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +import logging +import platform +import struct +import subprocess +import sys +import sysconfig +from importlib.machinery import EXTENSION_SUFFIXES +from typing import ( + Dict, + FrozenSet, + Iterable, + Iterator, + List, + Optional, + Sequence, + Tuple, + Union, + cast, +) + +from . import _manylinux, _musllinux + +logger = logging.getLogger(__name__) + +PythonVersion = Sequence[int] +MacVersion = Tuple[int, int] + +INTERPRETER_SHORT_NAMES: Dict[str, str] = { + "python": "py", # Generic. + "cpython": "cp", + "pypy": "pp", + "ironpython": "ip", + "jython": "jy", +} + + +_32_BIT_INTERPRETER = struct.calcsize("P") == 4 + + +class Tag: + """ + A representation of the tag triple for a wheel. + + Instances are considered immutable and thus are hashable. Equality checking + is also supported. + """ + + __slots__ = ["_interpreter", "_abi", "_platform", "_hash"] + + def __init__(self, interpreter: str, abi: str, platform: str) -> None: + self._interpreter = interpreter.lower() + self._abi = abi.lower() + self._platform = platform.lower() + # The __hash__ of every single element in a Set[Tag] will be evaluated each time + # that a set calls its `.disjoint()` method, which may be called hundreds of + # times when scanning a page of links for packages with tags matching that + # Set[Tag]. Pre-computing the value here produces significant speedups for + # downstream consumers. + self._hash = hash((self._interpreter, self._abi, self._platform)) + + @property + def interpreter(self) -> str: + return self._interpreter + + @property + def abi(self) -> str: + return self._abi + + @property + def platform(self) -> str: + return self._platform + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Tag): + return NotImplemented + + return ( + (self._hash == other._hash) # Short-circuit ASAP for perf reasons. + and (self._platform == other._platform) + and (self._abi == other._abi) + and (self._interpreter == other._interpreter) + ) + + def __hash__(self) -> int: + return self._hash + + def __str__(self) -> str: + return f"{self._interpreter}-{self._abi}-{self._platform}" + + def __repr__(self) -> str: + return f"<{self} @ {id(self)}>" + + +def parse_tag(tag: str) -> FrozenSet[Tag]: + """ + Parses the provided tag (e.g. `py3-none-any`) into a frozenset of Tag instances. + + Returning a set is required due to the possibility that the tag is a + compressed tag set. + """ + tags = set() + interpreters, abis, platforms = tag.split("-") + for interpreter in interpreters.split("."): + for abi in abis.split("."): + for platform_ in platforms.split("."): + tags.add(Tag(interpreter, abi, platform_)) + return frozenset(tags) + + +def _get_config_var(name: str, warn: bool = False) -> Union[int, str, None]: + value: Union[int, str, None] = sysconfig.get_config_var(name) + if value is None and warn: + logger.debug( + "Config variable '%s' is unset, Python ABI tag may be incorrect", name + ) + return value + + +def _normalize_string(string: str) -> str: + return string.replace(".", "_").replace("-", "_").replace(" ", "_") + + +def _abi3_applies(python_version: PythonVersion) -> bool: + """ + Determine if the Python version supports abi3. + + PEP 384 was first implemented in Python 3.2. + """ + return len(python_version) > 1 and tuple(python_version) >= (3, 2) + + +def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> List[str]: + py_version = tuple(py_version) # To allow for version comparison. + abis = [] + version = _version_nodot(py_version[:2]) + debug = pymalloc = ucs4 = "" + with_debug = _get_config_var("Py_DEBUG", warn) + has_refcount = hasattr(sys, "gettotalrefcount") + # Windows doesn't set Py_DEBUG, so checking for support of debug-compiled + # extension modules is the best option. + # https://github.com/pypa/pip/issues/3383#issuecomment-173267692 + has_ext = "_d.pyd" in EXTENSION_SUFFIXES + if with_debug or (with_debug is None and (has_refcount or has_ext)): + debug = "d" + if py_version < (3, 8): + with_pymalloc = _get_config_var("WITH_PYMALLOC", warn) + if with_pymalloc or with_pymalloc is None: + pymalloc = "m" + if py_version < (3, 3): + unicode_size = _get_config_var("Py_UNICODE_SIZE", warn) + if unicode_size == 4 or ( + unicode_size is None and sys.maxunicode == 0x10FFFF + ): + ucs4 = "u" + elif debug: + # Debug builds can also load "normal" extension modules. + # We can also assume no UCS-4 or pymalloc requirement. + abis.append(f"cp{version}") + abis.insert( + 0, + "cp{version}{debug}{pymalloc}{ucs4}".format( + version=version, debug=debug, pymalloc=pymalloc, ucs4=ucs4 + ), + ) + return abis + + +def cpython_tags( + python_version: Optional[PythonVersion] = None, + abis: Optional[Iterable[str]] = None, + platforms: Optional[Iterable[str]] = None, + *, + warn: bool = False, +) -> Iterator[Tag]: + """ + Yields the tags for a CPython interpreter. + + The tags consist of: + - cp-- + - cp-abi3- + - cp-none- + - cp-abi3- # Older Python versions down to 3.2. + + If python_version only specifies a major version then user-provided ABIs and + the 'none' ABItag will be used. + + If 'abi3' or 'none' are specified in 'abis' then they will be yielded at + their normal position and not at the beginning. + """ + if not python_version: + python_version = sys.version_info[:2] + + interpreter = f"cp{_version_nodot(python_version[:2])}" + + if abis is None: + if len(python_version) > 1: + abis = _cpython_abis(python_version, warn) + else: + abis = [] + abis = list(abis) + # 'abi3' and 'none' are explicitly handled later. + for explicit_abi in ("abi3", "none"): + try: + abis.remove(explicit_abi) + except ValueError: + pass + + platforms = list(platforms or platform_tags()) + for abi in abis: + for platform_ in platforms: + yield Tag(interpreter, abi, platform_) + if _abi3_applies(python_version): + yield from (Tag(interpreter, "abi3", platform_) for platform_ in platforms) + yield from (Tag(interpreter, "none", platform_) for platform_ in platforms) + + if _abi3_applies(python_version): + for minor_version in range(python_version[1] - 1, 1, -1): + for platform_ in platforms: + interpreter = "cp{version}".format( + version=_version_nodot((python_version[0], minor_version)) + ) + yield Tag(interpreter, "abi3", platform_) + + +def _generic_abi() -> List[str]: + """ + Return the ABI tag based on EXT_SUFFIX. + """ + # The following are examples of `EXT_SUFFIX`. + # We want to keep the parts which are related to the ABI and remove the + # parts which are related to the platform: + # - linux: '.cpython-310-x86_64-linux-gnu.so' => cp310 + # - mac: '.cpython-310-darwin.so' => cp310 + # - win: '.cp310-win_amd64.pyd' => cp310 + # - win: '.pyd' => cp37 (uses _cpython_abis()) + # - pypy: '.pypy38-pp73-x86_64-linux-gnu.so' => pypy38_pp73 + # - graalpy: '.graalpy-38-native-x86_64-darwin.dylib' + # => graalpy_38_native + + ext_suffix = _get_config_var("EXT_SUFFIX", warn=True) + if not isinstance(ext_suffix, str) or ext_suffix[0] != ".": + raise SystemError("invalid sysconfig.get_config_var('EXT_SUFFIX')") + parts = ext_suffix.split(".") + if len(parts) < 3: + # CPython3.7 and earlier uses ".pyd" on Windows. + return _cpython_abis(sys.version_info[:2]) + soabi = parts[1] + if soabi.startswith("cpython"): + # non-windows + abi = "cp" + soabi.split("-")[1] + elif soabi.startswith("cp"): + # windows + abi = soabi.split("-")[0] + elif soabi.startswith("pypy"): + abi = "-".join(soabi.split("-")[:2]) + elif soabi.startswith("graalpy"): + abi = "-".join(soabi.split("-")[:3]) + elif soabi: + # pyston, ironpython, others? + abi = soabi + else: + return [] + return [_normalize_string(abi)] + + +def generic_tags( + interpreter: Optional[str] = None, + abis: Optional[Iterable[str]] = None, + platforms: Optional[Iterable[str]] = None, + *, + warn: bool = False, +) -> Iterator[Tag]: + """ + Yields the tags for a generic interpreter. + + The tags consist of: + - -- + + The "none" ABI will be added if it was not explicitly provided. + """ + if not interpreter: + interp_name = interpreter_name() + interp_version = interpreter_version(warn=warn) + interpreter = "".join([interp_name, interp_version]) + if abis is None: + abis = _generic_abi() + else: + abis = list(abis) + platforms = list(platforms or platform_tags()) + if "none" not in abis: + abis.append("none") + for abi in abis: + for platform_ in platforms: + yield Tag(interpreter, abi, platform_) + + +def _py_interpreter_range(py_version: PythonVersion) -> Iterator[str]: + """ + Yields Python versions in descending order. + + After the latest version, the major-only version will be yielded, and then + all previous versions of that major version. + """ + if len(py_version) > 1: + yield f"py{_version_nodot(py_version[:2])}" + yield f"py{py_version[0]}" + if len(py_version) > 1: + for minor in range(py_version[1] - 1, -1, -1): + yield f"py{_version_nodot((py_version[0], minor))}" + + +def compatible_tags( + python_version: Optional[PythonVersion] = None, + interpreter: Optional[str] = None, + platforms: Optional[Iterable[str]] = None, +) -> Iterator[Tag]: + """ + Yields the sequence of tags that are compatible with a specific version of Python. + + The tags consist of: + - py*-none- + - -none-any # ... if `interpreter` is provided. + - py*-none-any + """ + if not python_version: + python_version = sys.version_info[:2] + platforms = list(platforms or platform_tags()) + for version in _py_interpreter_range(python_version): + for platform_ in platforms: + yield Tag(version, "none", platform_) + if interpreter: + yield Tag(interpreter, "none", "any") + for version in _py_interpreter_range(python_version): + yield Tag(version, "none", "any") + + +def _mac_arch(arch: str, is_32bit: bool = _32_BIT_INTERPRETER) -> str: + if not is_32bit: + return arch + + if arch.startswith("ppc"): + return "ppc" + + return "i386" + + +def _mac_binary_formats(version: MacVersion, cpu_arch: str) -> List[str]: + formats = [cpu_arch] + if cpu_arch == "x86_64": + if version < (10, 4): + return [] + formats.extend(["intel", "fat64", "fat32"]) + + elif cpu_arch == "i386": + if version < (10, 4): + return [] + formats.extend(["intel", "fat32", "fat"]) + + elif cpu_arch == "ppc64": + # TODO: Need to care about 32-bit PPC for ppc64 through 10.2? + if version > (10, 5) or version < (10, 4): + return [] + formats.append("fat64") + + elif cpu_arch == "ppc": + if version > (10, 6): + return [] + formats.extend(["fat32", "fat"]) + + if cpu_arch in {"arm64", "x86_64"}: + formats.append("universal2") + + if cpu_arch in {"x86_64", "i386", "ppc64", "ppc", "intel"}: + formats.append("universal") + + return formats + + +def mac_platforms( + version: Optional[MacVersion] = None, arch: Optional[str] = None +) -> Iterator[str]: + """ + Yields the platform tags for a macOS system. + + The `version` parameter is a two-item tuple specifying the macOS version to + generate platform tags for. The `arch` parameter is the CPU architecture to + generate platform tags for. Both parameters default to the appropriate value + for the current system. + """ + version_str, _, cpu_arch = platform.mac_ver() + if version is None: + version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2]))) + if version == (10, 16): + # When built against an older macOS SDK, Python will report macOS 10.16 + # instead of the real version. + version_str = subprocess.run( + [ + sys.executable, + "-sS", + "-c", + "import platform; print(platform.mac_ver()[0])", + ], + check=True, + env={"SYSTEM_VERSION_COMPAT": "0"}, + stdout=subprocess.PIPE, + text=True, + ).stdout + version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2]))) + else: + version = version + if arch is None: + arch = _mac_arch(cpu_arch) + else: + arch = arch + + if (10, 0) <= version and version < (11, 0): + # Prior to Mac OS 11, each yearly release of Mac OS bumped the + # "minor" version number. The major version was always 10. + for minor_version in range(version[1], -1, -1): + compat_version = 10, minor_version + binary_formats = _mac_binary_formats(compat_version, arch) + for binary_format in binary_formats: + yield "macosx_{major}_{minor}_{binary_format}".format( + major=10, minor=minor_version, binary_format=binary_format + ) + + if version >= (11, 0): + # Starting with Mac OS 11, each yearly release bumps the major version + # number. The minor versions are now the midyear updates. + for major_version in range(version[0], 10, -1): + compat_version = major_version, 0 + binary_formats = _mac_binary_formats(compat_version, arch) + for binary_format in binary_formats: + yield "macosx_{major}_{minor}_{binary_format}".format( + major=major_version, minor=0, binary_format=binary_format + ) + + if version >= (11, 0): + # Mac OS 11 on x86_64 is compatible with binaries from previous releases. + # Arm64 support was introduced in 11.0, so no Arm binaries from previous + # releases exist. + # + # However, the "universal2" binary format can have a + # macOS version earlier than 11.0 when the x86_64 part of the binary supports + # that version of macOS. + if arch == "x86_64": + for minor_version in range(16, 3, -1): + compat_version = 10, minor_version + binary_formats = _mac_binary_formats(compat_version, arch) + for binary_format in binary_formats: + yield "macosx_{major}_{minor}_{binary_format}".format( + major=compat_version[0], + minor=compat_version[1], + binary_format=binary_format, + ) + else: + for minor_version in range(16, 3, -1): + compat_version = 10, minor_version + binary_format = "universal2" + yield "macosx_{major}_{minor}_{binary_format}".format( + major=compat_version[0], + minor=compat_version[1], + binary_format=binary_format, + ) + + +def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]: + linux = _normalize_string(sysconfig.get_platform()) + if not linux.startswith("linux_"): + # we should never be here, just yield the sysconfig one and return + yield linux + return + if is_32bit: + if linux == "linux_x86_64": + linux = "linux_i686" + elif linux == "linux_aarch64": + linux = "linux_armv8l" + _, arch = linux.split("_", 1) + archs = {"armv8l": ["armv8l", "armv7l"]}.get(arch, [arch]) + yield from _manylinux.platform_tags(archs) + yield from _musllinux.platform_tags(archs) + for arch in archs: + yield f"linux_{arch}" + + +def _generic_platforms() -> Iterator[str]: + yield _normalize_string(sysconfig.get_platform()) + + +def platform_tags() -> Iterator[str]: + """ + Provides the platform tags for this installation. + """ + if platform.system() == "Darwin": + return mac_platforms() + elif platform.system() == "Linux": + return _linux_platforms() + else: + return _generic_platforms() + + +def interpreter_name() -> str: + """ + Returns the name of the running interpreter. + + Some implementations have a reserved, two-letter abbreviation which will + be returned when appropriate. + """ + name = sys.implementation.name + return INTERPRETER_SHORT_NAMES.get(name) or name + + +def interpreter_version(*, warn: bool = False) -> str: + """ + Returns the version of the running interpreter. + """ + version = _get_config_var("py_version_nodot", warn=warn) + if version: + version = str(version) + else: + version = _version_nodot(sys.version_info[:2]) + return version + + +def _version_nodot(version: PythonVersion) -> str: + return "".join(map(str, version)) + + +def sys_tags(*, warn: bool = False) -> Iterator[Tag]: + """ + Returns the sequence of tag triples for the running interpreter. + + The order of the sequence corresponds to priority order for the + interpreter, from most to least important. + """ + + interp_name = interpreter_name() + if interp_name == "cp": + yield from cpython_tags(warn=warn) + else: + yield from generic_tags() + + if interp_name == "pp": + interp = "pp3" + elif interp_name == "cp": + interp = "cp" + interpreter_version(warn=warn) + else: + interp = None + yield from compatible_tags(interpreter=interp) diff --git a/libs/common/packaging/utils.py b/libs/common/packaging/utils.py new file mode 100644 index 00000000..c2c2f75a --- /dev/null +++ b/libs/common/packaging/utils.py @@ -0,0 +1,172 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +import re +from typing import FrozenSet, NewType, Tuple, Union, cast + +from .tags import Tag, parse_tag +from .version import InvalidVersion, Version + +BuildTag = Union[Tuple[()], Tuple[int, str]] +NormalizedName = NewType("NormalizedName", str) + + +class InvalidName(ValueError): + """ + An invalid distribution name; users should refer to the packaging user guide. + """ + + +class InvalidWheelFilename(ValueError): + """ + An invalid wheel filename was found, users should refer to PEP 427. + """ + + +class InvalidSdistFilename(ValueError): + """ + An invalid sdist filename was found, users should refer to the packaging user guide. + """ + + +# Core metadata spec for `Name` +_validate_regex = re.compile( + r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.IGNORECASE +) +_canonicalize_regex = re.compile(r"[-_.]+") +_normalized_regex = re.compile(r"^([a-z0-9]|[a-z0-9]([a-z0-9-](?!--))*[a-z0-9])$") +# PEP 427: The build number must start with a digit. +_build_tag_regex = re.compile(r"(\d+)(.*)") + + +def canonicalize_name(name: str, *, validate: bool = False) -> NormalizedName: + if validate and not _validate_regex.match(name): + raise InvalidName(f"name is invalid: {name!r}") + # This is taken from PEP 503. + value = _canonicalize_regex.sub("-", name).lower() + return cast(NormalizedName, value) + + +def is_normalized_name(name: str) -> bool: + return _normalized_regex.match(name) is not None + + +def canonicalize_version( + version: Union[Version, str], *, strip_trailing_zero: bool = True +) -> str: + """ + This is very similar to Version.__str__, but has one subtle difference + with the way it handles the release segment. + """ + if isinstance(version, str): + try: + parsed = Version(version) + except InvalidVersion: + # Legacy versions cannot be normalized + return version + else: + parsed = version + + parts = [] + + # Epoch + if parsed.epoch != 0: + parts.append(f"{parsed.epoch}!") + + # Release segment + release_segment = ".".join(str(x) for x in parsed.release) + if strip_trailing_zero: + # NB: This strips trailing '.0's to normalize + release_segment = re.sub(r"(\.0)+$", "", release_segment) + parts.append(release_segment) + + # Pre-release + if parsed.pre is not None: + parts.append("".join(str(x) for x in parsed.pre)) + + # Post-release + if parsed.post is not None: + parts.append(f".post{parsed.post}") + + # Development release + if parsed.dev is not None: + parts.append(f".dev{parsed.dev}") + + # Local version segment + if parsed.local is not None: + parts.append(f"+{parsed.local}") + + return "".join(parts) + + +def parse_wheel_filename( + filename: str, +) -> Tuple[NormalizedName, Version, BuildTag, FrozenSet[Tag]]: + if not filename.endswith(".whl"): + raise InvalidWheelFilename( + f"Invalid wheel filename (extension must be '.whl'): {filename}" + ) + + filename = filename[:-4] + dashes = filename.count("-") + if dashes not in (4, 5): + raise InvalidWheelFilename( + f"Invalid wheel filename (wrong number of parts): {filename}" + ) + + parts = filename.split("-", dashes - 2) + name_part = parts[0] + # See PEP 427 for the rules on escaping the project name. + if "__" in name_part or re.match(r"^[\w\d._]*$", name_part, re.UNICODE) is None: + raise InvalidWheelFilename(f"Invalid project name: {filename}") + name = canonicalize_name(name_part) + + try: + version = Version(parts[1]) + except InvalidVersion as e: + raise InvalidWheelFilename( + f"Invalid wheel filename (invalid version): {filename}" + ) from e + + if dashes == 5: + build_part = parts[2] + build_match = _build_tag_regex.match(build_part) + if build_match is None: + raise InvalidWheelFilename( + f"Invalid build number: {build_part} in '{filename}'" + ) + build = cast(BuildTag, (int(build_match.group(1)), build_match.group(2))) + else: + build = () + tags = parse_tag(parts[-1]) + return (name, version, build, tags) + + +def parse_sdist_filename(filename: str) -> Tuple[NormalizedName, Version]: + if filename.endswith(".tar.gz"): + file_stem = filename[: -len(".tar.gz")] + elif filename.endswith(".zip"): + file_stem = filename[: -len(".zip")] + else: + raise InvalidSdistFilename( + f"Invalid sdist filename (extension must be '.tar.gz' or '.zip'):" + f" {filename}" + ) + + # We are requiring a PEP 440 version, which cannot contain dashes, + # so we split on the last dash. + name_part, sep, version_part = file_stem.rpartition("-") + if not sep: + raise InvalidSdistFilename(f"Invalid sdist filename: {filename}") + + name = canonicalize_name(name_part) + + try: + version = Version(version_part) + except InvalidVersion as e: + raise InvalidSdistFilename( + f"Invalid sdist filename (invalid version): {filename}" + ) from e + + return (name, version) diff --git a/libs/common/packaging/version.py b/libs/common/packaging/version.py new file mode 100644 index 00000000..5faab9bd --- /dev/null +++ b/libs/common/packaging/version.py @@ -0,0 +1,563 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. +""" +.. testsetup:: + + from packaging.version import parse, Version +""" + +import itertools +import re +from typing import Any, Callable, NamedTuple, Optional, SupportsInt, Tuple, Union + +from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType + +__all__ = ["VERSION_PATTERN", "parse", "Version", "InvalidVersion"] + +LocalType = Tuple[Union[int, str], ...] + +CmpPrePostDevType = Union[InfinityType, NegativeInfinityType, Tuple[str, int]] +CmpLocalType = Union[ + NegativeInfinityType, + Tuple[Union[Tuple[int, str], Tuple[NegativeInfinityType, Union[int, str]]], ...], +] +CmpKey = Tuple[ + int, + Tuple[int, ...], + CmpPrePostDevType, + CmpPrePostDevType, + CmpPrePostDevType, + CmpLocalType, +] +VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool] + + +class _Version(NamedTuple): + epoch: int + release: Tuple[int, ...] + dev: Optional[Tuple[str, int]] + pre: Optional[Tuple[str, int]] + post: Optional[Tuple[str, int]] + local: Optional[LocalType] + + +def parse(version: str) -> "Version": + """Parse the given version string. + + >>> parse('1.0.dev1') + + + :param version: The version string to parse. + :raises InvalidVersion: When the version string is not a valid version. + """ + return Version(version) + + +class InvalidVersion(ValueError): + """Raised when a version string is not a valid version. + + >>> Version("invalid") + Traceback (most recent call last): + ... + packaging.version.InvalidVersion: Invalid version: 'invalid' + """ + + +class _BaseVersion: + _key: Tuple[Any, ...] + + def __hash__(self) -> int: + return hash(self._key) + + # Please keep the duplicated `isinstance` check + # in the six comparisons hereunder + # unless you find a way to avoid adding overhead function calls. + def __lt__(self, other: "_BaseVersion") -> bool: + if not isinstance(other, _BaseVersion): + return NotImplemented + + return self._key < other._key + + def __le__(self, other: "_BaseVersion") -> bool: + if not isinstance(other, _BaseVersion): + return NotImplemented + + return self._key <= other._key + + def __eq__(self, other: object) -> bool: + if not isinstance(other, _BaseVersion): + return NotImplemented + + return self._key == other._key + + def __ge__(self, other: "_BaseVersion") -> bool: + if not isinstance(other, _BaseVersion): + return NotImplemented + + return self._key >= other._key + + def __gt__(self, other: "_BaseVersion") -> bool: + if not isinstance(other, _BaseVersion): + return NotImplemented + + return self._key > other._key + + def __ne__(self, other: object) -> bool: + if not isinstance(other, _BaseVersion): + return NotImplemented + + return self._key != other._key + + +# Deliberately not anchored to the start and end of the string, to make it +# easier for 3rd party code to reuse +_VERSION_PATTERN = r""" + v? + (?: + (?:(?P[0-9]+)!)? # epoch + (?P[0-9]+(?:\.[0-9]+)*) # release segment + (?P
                                          # pre-release
+            [-_\.]?
+            (?Palpha|a|beta|b|preview|pre|c|rc)
+            [-_\.]?
+            (?P[0-9]+)?
+        )?
+        (?P                                         # post release
+            (?:-(?P[0-9]+))
+            |
+            (?:
+                [-_\.]?
+                (?Ppost|rev|r)
+                [-_\.]?
+                (?P[0-9]+)?
+            )
+        )?
+        (?P                                          # dev release
+            [-_\.]?
+            (?Pdev)
+            [-_\.]?
+            (?P[0-9]+)?
+        )?
+    )
+    (?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
+"""
+
+VERSION_PATTERN = _VERSION_PATTERN
+"""
+A string containing the regular expression used to match a valid version.
+
+The pattern is not anchored at either end, and is intended for embedding in larger
+expressions (for example, matching a version number as part of a file name). The
+regular expression should be compiled with the ``re.VERBOSE`` and ``re.IGNORECASE``
+flags set.
+
+:meta hide-value:
+"""
+
+
+class Version(_BaseVersion):
+    """This class abstracts handling of a project's versions.
+
+    A :class:`Version` instance is comparison aware and can be compared and
+    sorted using the standard Python interfaces.
+
+    >>> v1 = Version("1.0a5")
+    >>> v2 = Version("1.0")
+    >>> v1
+    
+    >>> v2
+    
+    >>> v1 < v2
+    True
+    >>> v1 == v2
+    False
+    >>> v1 > v2
+    False
+    >>> v1 >= v2
+    False
+    >>> v1 <= v2
+    True
+    """
+
+    _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE)
+    _key: CmpKey
+
+    def __init__(self, version: str) -> None:
+        """Initialize a Version object.
+
+        :param version:
+            The string representation of a version which will be parsed and normalized
+            before use.
+        :raises InvalidVersion:
+            If the ``version`` does not conform to PEP 440 in any way then this
+            exception will be raised.
+        """
+
+        # Validate the version and parse it into pieces
+        match = self._regex.search(version)
+        if not match:
+            raise InvalidVersion(f"Invalid version: '{version}'")
+
+        # Store the parsed out pieces of the version
+        self._version = _Version(
+            epoch=int(match.group("epoch")) if match.group("epoch") else 0,
+            release=tuple(int(i) for i in match.group("release").split(".")),
+            pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")),
+            post=_parse_letter_version(
+                match.group("post_l"), match.group("post_n1") or match.group("post_n2")
+            ),
+            dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")),
+            local=_parse_local_version(match.group("local")),
+        )
+
+        # Generate a key which will be used for sorting
+        self._key = _cmpkey(
+            self._version.epoch,
+            self._version.release,
+            self._version.pre,
+            self._version.post,
+            self._version.dev,
+            self._version.local,
+        )
+
+    def __repr__(self) -> str:
+        """A representation of the Version that shows all internal state.
+
+        >>> Version('1.0.0')
+        
+        """
+        return f""
+
+    def __str__(self) -> str:
+        """A string representation of the version that can be rounded-tripped.
+
+        >>> str(Version("1.0a5"))
+        '1.0a5'
+        """
+        parts = []
+
+        # Epoch
+        if self.epoch != 0:
+            parts.append(f"{self.epoch}!")
+
+        # Release segment
+        parts.append(".".join(str(x) for x in self.release))
+
+        # Pre-release
+        if self.pre is not None:
+            parts.append("".join(str(x) for x in self.pre))
+
+        # Post-release
+        if self.post is not None:
+            parts.append(f".post{self.post}")
+
+        # Development release
+        if self.dev is not None:
+            parts.append(f".dev{self.dev}")
+
+        # Local version segment
+        if self.local is not None:
+            parts.append(f"+{self.local}")
+
+        return "".join(parts)
+
+    @property
+    def epoch(self) -> int:
+        """The epoch of the version.
+
+        >>> Version("2.0.0").epoch
+        0
+        >>> Version("1!2.0.0").epoch
+        1
+        """
+        return self._version.epoch
+
+    @property
+    def release(self) -> Tuple[int, ...]:
+        """The components of the "release" segment of the version.
+
+        >>> Version("1.2.3").release
+        (1, 2, 3)
+        >>> Version("2.0.0").release
+        (2, 0, 0)
+        >>> Version("1!2.0.0.post0").release
+        (2, 0, 0)
+
+        Includes trailing zeroes but not the epoch or any pre-release / development /
+        post-release suffixes.
+        """
+        return self._version.release
+
+    @property
+    def pre(self) -> Optional[Tuple[str, int]]:
+        """The pre-release segment of the version.
+
+        >>> print(Version("1.2.3").pre)
+        None
+        >>> Version("1.2.3a1").pre
+        ('a', 1)
+        >>> Version("1.2.3b1").pre
+        ('b', 1)
+        >>> Version("1.2.3rc1").pre
+        ('rc', 1)
+        """
+        return self._version.pre
+
+    @property
+    def post(self) -> Optional[int]:
+        """The post-release number of the version.
+
+        >>> print(Version("1.2.3").post)
+        None
+        >>> Version("1.2.3.post1").post
+        1
+        """
+        return self._version.post[1] if self._version.post else None
+
+    @property
+    def dev(self) -> Optional[int]:
+        """The development number of the version.
+
+        >>> print(Version("1.2.3").dev)
+        None
+        >>> Version("1.2.3.dev1").dev
+        1
+        """
+        return self._version.dev[1] if self._version.dev else None
+
+    @property
+    def local(self) -> Optional[str]:
+        """The local version segment of the version.
+
+        >>> print(Version("1.2.3").local)
+        None
+        >>> Version("1.2.3+abc").local
+        'abc'
+        """
+        if self._version.local:
+            return ".".join(str(x) for x in self._version.local)
+        else:
+            return None
+
+    @property
+    def public(self) -> str:
+        """The public portion of the version.
+
+        >>> Version("1.2.3").public
+        '1.2.3'
+        >>> Version("1.2.3+abc").public
+        '1.2.3'
+        >>> Version("1.2.3+abc.dev1").public
+        '1.2.3'
+        """
+        return str(self).split("+", 1)[0]
+
+    @property
+    def base_version(self) -> str:
+        """The "base version" of the version.
+
+        >>> Version("1.2.3").base_version
+        '1.2.3'
+        >>> Version("1.2.3+abc").base_version
+        '1.2.3'
+        >>> Version("1!1.2.3+abc.dev1").base_version
+        '1!1.2.3'
+
+        The "base version" is the public version of the project without any pre or post
+        release markers.
+        """
+        parts = []
+
+        # Epoch
+        if self.epoch != 0:
+            parts.append(f"{self.epoch}!")
+
+        # Release segment
+        parts.append(".".join(str(x) for x in self.release))
+
+        return "".join(parts)
+
+    @property
+    def is_prerelease(self) -> bool:
+        """Whether this version is a pre-release.
+
+        >>> Version("1.2.3").is_prerelease
+        False
+        >>> Version("1.2.3a1").is_prerelease
+        True
+        >>> Version("1.2.3b1").is_prerelease
+        True
+        >>> Version("1.2.3rc1").is_prerelease
+        True
+        >>> Version("1.2.3dev1").is_prerelease
+        True
+        """
+        return self.dev is not None or self.pre is not None
+
+    @property
+    def is_postrelease(self) -> bool:
+        """Whether this version is a post-release.
+
+        >>> Version("1.2.3").is_postrelease
+        False
+        >>> Version("1.2.3.post1").is_postrelease
+        True
+        """
+        return self.post is not None
+
+    @property
+    def is_devrelease(self) -> bool:
+        """Whether this version is a development release.
+
+        >>> Version("1.2.3").is_devrelease
+        False
+        >>> Version("1.2.3.dev1").is_devrelease
+        True
+        """
+        return self.dev is not None
+
+    @property
+    def major(self) -> int:
+        """The first item of :attr:`release` or ``0`` if unavailable.
+
+        >>> Version("1.2.3").major
+        1
+        """
+        return self.release[0] if len(self.release) >= 1 else 0
+
+    @property
+    def minor(self) -> int:
+        """The second item of :attr:`release` or ``0`` if unavailable.
+
+        >>> Version("1.2.3").minor
+        2
+        >>> Version("1").minor
+        0
+        """
+        return self.release[1] if len(self.release) >= 2 else 0
+
+    @property
+    def micro(self) -> int:
+        """The third item of :attr:`release` or ``0`` if unavailable.
+
+        >>> Version("1.2.3").micro
+        3
+        >>> Version("1").micro
+        0
+        """
+        return self.release[2] if len(self.release) >= 3 else 0
+
+
+def _parse_letter_version(
+    letter: Optional[str], number: Union[str, bytes, SupportsInt, None]
+) -> Optional[Tuple[str, int]]:
+
+    if letter:
+        # We consider there to be an implicit 0 in a pre-release if there is
+        # not a numeral associated with it.
+        if number is None:
+            number = 0
+
+        # We normalize any letters to their lower case form
+        letter = letter.lower()
+
+        # We consider some words to be alternate spellings of other words and
+        # in those cases we want to normalize the spellings to our preferred
+        # spelling.
+        if letter == "alpha":
+            letter = "a"
+        elif letter == "beta":
+            letter = "b"
+        elif letter in ["c", "pre", "preview"]:
+            letter = "rc"
+        elif letter in ["rev", "r"]:
+            letter = "post"
+
+        return letter, int(number)
+    if not letter and number:
+        # We assume if we are given a number, but we are not given a letter
+        # then this is using the implicit post release syntax (e.g. 1.0-1)
+        letter = "post"
+
+        return letter, int(number)
+
+    return None
+
+
+_local_version_separators = re.compile(r"[\._-]")
+
+
+def _parse_local_version(local: Optional[str]) -> Optional[LocalType]:
+    """
+    Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
+    """
+    if local is not None:
+        return tuple(
+            part.lower() if not part.isdigit() else int(part)
+            for part in _local_version_separators.split(local)
+        )
+    return None
+
+
+def _cmpkey(
+    epoch: int,
+    release: Tuple[int, ...],
+    pre: Optional[Tuple[str, int]],
+    post: Optional[Tuple[str, int]],
+    dev: Optional[Tuple[str, int]],
+    local: Optional[LocalType],
+) -> CmpKey:
+
+    # When we compare a release version, we want to compare it with all of the
+    # trailing zeros removed. So we'll use a reverse the list, drop all the now
+    # leading zeros until we come to something non zero, then take the rest
+    # re-reverse it back into the correct order and make it a tuple and use
+    # that for our sorting key.
+    _release = tuple(
+        reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release))))
+    )
+
+    # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
+    # We'll do this by abusing the pre segment, but we _only_ want to do this
+    # if there is not a pre or a post segment. If we have one of those then
+    # the normal sorting rules will handle this case correctly.
+    if pre is None and post is None and dev is not None:
+        _pre: CmpPrePostDevType = NegativeInfinity
+    # Versions without a pre-release (except as noted above) should sort after
+    # those with one.
+    elif pre is None:
+        _pre = Infinity
+    else:
+        _pre = pre
+
+    # Versions without a post segment should sort before those with one.
+    if post is None:
+        _post: CmpPrePostDevType = NegativeInfinity
+
+    else:
+        _post = post
+
+    # Versions without a development segment should sort after those with one.
+    if dev is None:
+        _dev: CmpPrePostDevType = Infinity
+
+    else:
+        _dev = dev
+
+    if local is None:
+        # Versions without a local segment should sort before those with one.
+        _local: CmpLocalType = NegativeInfinity
+    else:
+        # Versions with a local segment need that segment parsed to implement
+        # the sorting rules in PEP440.
+        # - Alpha numeric segments sort before numeric segments
+        # - Alpha numeric segments sort lexicographically
+        # - Numeric segments sort numerically
+        # - Shorter versions sort before longer versions when the prefixes
+        #   match exactly
+        _local = tuple(
+            (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local
+        )
+
+    return epoch, _release, _pre, _post, _dev, _local
diff --git a/libs/common/pkg_resources/__init__.py b/libs/common/pkg_resources/__init__.py
index 2f5aa64a..10c6a9cd 100644
--- a/libs/common/pkg_resources/__init__.py
+++ b/libs/common/pkg_resources/__init__.py
@@ -1,4 +1,3 @@
-# coding: utf-8
 """
 Package resource API
 --------------------
@@ -13,16 +12,22 @@ The package resource API is designed to work with normal filesystem packages,
 .egg files, and unpacked .egg files.  It can also work in a limited way with
 .zip files and with custom PEP 302 loaders that support the ``get_data()``
 method.
+
+This module is deprecated. Users are directed to :mod:`importlib.resources`,
+:mod:`importlib.metadata` and :pypi:`packaging` instead.
 """
 
-from __future__ import absolute_import
-
 import sys
+
+if sys.version_info < (3, 8):
+    raise RuntimeError("Python 3.8 or later is required")
+
 import os
 import io
 import time
 import re
 import types
+from typing import Protocol
 import zipfile
 import zipimport
 import warnings
@@ -37,30 +42,21 @@ import email.parser
 import errno
 import tempfile
 import textwrap
-import itertools
 import inspect
 import ntpath
 import posixpath
+import importlib
+import importlib.machinery
 from pkgutil import get_importer
 
-try:
-    import _imp
-except ImportError:
-    # Python 3.2 compatibility
-    import imp as _imp
-
-try:
-    FileExistsError
-except NameError:
-    FileExistsError = OSError
-
-from pkg_resources.extern import six
-from pkg_resources.extern.six.moves import urllib, map, filter
+import _imp
 
 # capture these to bypass sandboxing
 from os import utime
+
 try:
     from os import mkdir, rename, unlink
+
     WRITE_SUPPORT = True
 except ImportError:
     # no write support, probably under GAE
@@ -69,32 +65,20 @@ except ImportError:
 from os import open as os_open
 from os.path import isdir, split
 
-try:
-    import importlib.machinery as importlib_machinery
-    # access attribute to force import under delayed import mechanisms.
-    importlib_machinery.__name__
-except ImportError:
-    importlib_machinery = None
+from pkg_resources.extern.jaraco.text import (
+    yield_lines,
+    drop_comment,
+    join_continuation,
+)
 
-from . import py31compat
-from pkg_resources.extern import appdirs
+from pkg_resources.extern import platformdirs
 from pkg_resources.extern import packaging
+
 __import__('pkg_resources.extern.packaging.version')
 __import__('pkg_resources.extern.packaging.specifiers')
 __import__('pkg_resources.extern.packaging.requirements')
 __import__('pkg_resources.extern.packaging.markers')
-
-
-__metaclass__ = type
-
-
-if (3, 0) < sys.version_info < (3, 5):
-    raise RuntimeError("Python 3.5 or later is required")
-
-if six.PY2:
-    # Those builtin exceptions are only defined in Python 3
-    PermissionError = None
-    NotADirectoryError = None
+__import__('pkg_resources.extern.packaging.utils')
 
 # declare some globals that will be defined later to
 # satisfy the linters.
@@ -117,6 +101,17 @@ _namespace_handlers = None
 _namespace_packages = None
 
 
+warnings.warn(
+    "pkg_resources is deprecated as an API. "
+    "See https://setuptools.pypa.io/en/latest/pkg_resources.html",
+    DeprecationWarning,
+    stacklevel=2,
+)
+
+
+_PEP440_FALLBACK = re.compile(r"^v?(?P(?:[0-9]+!)?[0-9]+(?:\.[0-9]+)*)", re.I)
+
+
 class PEP440Warning(RuntimeWarning):
     """
     Used when there is an issue with a version or specifier not complying with
@@ -124,11 +119,7 @@ class PEP440Warning(RuntimeWarning):
     """
 
 
-def parse_version(v):
-    try:
-        return packaging.version.Version(v)
-    except packaging.version.InvalidVersion:
-        return packaging.version.LegacyVersion(v)
+parse_version = packaging.version.Version
 
 
 _state_vars = {}
@@ -178,10 +169,10 @@ def get_supported_platform():
     """Return this platform's maximum compatible version.
 
     distutils.util.get_platform() normally reports the minimum version
-    of Mac OS X that would be required to *use* extensions produced by
+    of macOS that would be required to *use* extensions produced by
     distutils.  But what we want when checking compatibility is to know the
-    version of Mac OS X that we are *running*.  To allow usage of packages that
-    explicitly require a newer version of Mac OS X, we must also know the
+    version of macOS that we are *running*.  To allow usage of packages that
+    explicitly require a newer version of macOS, we must also know the
     current version of the OS.
 
     If this condition occurs for any other platform with a version in its
@@ -191,60 +182,96 @@ def get_supported_platform():
     m = macosVersionString.match(plat)
     if m is not None and sys.platform == "darwin":
         try:
-            plat = 'macosx-%s-%s' % ('.'.join(_macosx_vers()[:2]), m.group(3))
+            plat = 'macosx-%s-%s' % ('.'.join(_macos_vers()[:2]), m.group(3))
         except ValueError:
-            # not Mac OS X
+            # not macOS
             pass
     return plat
 
 
 __all__ = [
     # Basic resource access and distribution/entry point discovery
-    'require', 'run_script', 'get_provider', 'get_distribution',
-    'load_entry_point', 'get_entry_map', 'get_entry_info',
+    'require',
+    'run_script',
+    'get_provider',
+    'get_distribution',
+    'load_entry_point',
+    'get_entry_map',
+    'get_entry_info',
     'iter_entry_points',
-    'resource_string', 'resource_stream', 'resource_filename',
-    'resource_listdir', 'resource_exists', 'resource_isdir',
-
+    'resource_string',
+    'resource_stream',
+    'resource_filename',
+    'resource_listdir',
+    'resource_exists',
+    'resource_isdir',
     # Environmental control
-    'declare_namespace', 'working_set', 'add_activation_listener',
-    'find_distributions', 'set_extraction_path', 'cleanup_resources',
+    'declare_namespace',
+    'working_set',
+    'add_activation_listener',
+    'find_distributions',
+    'set_extraction_path',
+    'cleanup_resources',
     'get_default_cache',
-
     # Primary implementation classes
-    'Environment', 'WorkingSet', 'ResourceManager',
-    'Distribution', 'Requirement', 'EntryPoint',
-
+    'Environment',
+    'WorkingSet',
+    'ResourceManager',
+    'Distribution',
+    'Requirement',
+    'EntryPoint',
     # Exceptions
-    'ResolutionError', 'VersionConflict', 'DistributionNotFound',
-    'UnknownExtra', 'ExtractionError',
-
+    'ResolutionError',
+    'VersionConflict',
+    'DistributionNotFound',
+    'UnknownExtra',
+    'ExtractionError',
     # Warnings
     'PEP440Warning',
-
     # Parsing functions and string utilities
-    'parse_requirements', 'parse_version', 'safe_name', 'safe_version',
-    'get_platform', 'compatible_platforms', 'yield_lines', 'split_sections',
-    'safe_extra', 'to_filename', 'invalid_marker', 'evaluate_marker',
-
+    'parse_requirements',
+    'parse_version',
+    'safe_name',
+    'safe_version',
+    'get_platform',
+    'compatible_platforms',
+    'yield_lines',
+    'split_sections',
+    'safe_extra',
+    'to_filename',
+    'invalid_marker',
+    'evaluate_marker',
     # filesystem utilities
-    'ensure_directory', 'normalize_path',
-
+    'ensure_directory',
+    'normalize_path',
     # Distribution "precedence" constants
-    'EGG_DIST', 'BINARY_DIST', 'SOURCE_DIST', 'CHECKOUT_DIST', 'DEVELOP_DIST',
-
+    'EGG_DIST',
+    'BINARY_DIST',
+    'SOURCE_DIST',
+    'CHECKOUT_DIST',
+    'DEVELOP_DIST',
     # "Provider" interfaces, implementations, and registration/lookup APIs
-    'IMetadataProvider', 'IResourceProvider', 'FileMetadata',
-    'PathMetadata', 'EggMetadata', 'EmptyProvider', 'empty_provider',
-    'NullProvider', 'EggProvider', 'DefaultProvider', 'ZipProvider',
-    'register_finder', 'register_namespace_handler', 'register_loader_type',
-    'fixup_namespace_packages', 'get_importer',
-
+    'IMetadataProvider',
+    'IResourceProvider',
+    'FileMetadata',
+    'PathMetadata',
+    'EggMetadata',
+    'EmptyProvider',
+    'empty_provider',
+    'NullProvider',
+    'EggProvider',
+    'DefaultProvider',
+    'ZipProvider',
+    'register_finder',
+    'register_namespace_handler',
+    'register_loader_type',
+    'fixup_namespace_packages',
+    'get_importer',
     # Warnings
     'PkgResourcesDeprecationWarning',
-
     # Deprecated/backward compatibility only
-    'run_main', 'AvailableDistributions',
+    'run_main',
+    'AvailableDistributions',
 ]
 
 
@@ -303,8 +330,10 @@ class ContextualVersionConflict(VersionConflict):
 class DistributionNotFound(ResolutionError):
     """A requested distribution was not found"""
 
-    _template = ("The '{self.req}' distribution was not found "
-                 "and is required by {self.requirers_str}")
+    _template = (
+        "The '{self.req}' distribution was not found "
+        "and is required by {self.requirers_str}"
+    )
 
     @property
     def req(self):
@@ -364,23 +393,21 @@ def get_provider(moduleOrReq):
     return _find_adapter(_provider_factories, loader)(module)
 
 
-def _macosx_vers(_cache=[]):
-    if not _cache:
-        version = platform.mac_ver()[0]
-        # fallback for MacPorts
-        if version == '':
-            plist = '/System/Library/CoreServices/SystemVersion.plist'
-            if os.path.exists(plist):
-                if hasattr(plistlib, 'readPlist'):
-                    plist_content = plistlib.readPlist(plist)
-                    if 'ProductVersion' in plist_content:
-                        version = plist_content['ProductVersion']
-
-        _cache.append(version.split('.'))
-    return _cache[0]
+@functools.lru_cache(maxsize=None)
+def _macos_vers():
+    version = platform.mac_ver()[0]
+    # fallback for MacPorts
+    if version == '':
+        plist = '/System/Library/CoreServices/SystemVersion.plist'
+        if os.path.exists(plist):
+            with open(plist, 'rb') as fh:
+                plist_content = plistlib.load(fh)
+            if 'ProductVersion' in plist_content:
+                version = plist_content['ProductVersion']
+    return version.split('.')
 
 
-def _macosx_arch(machine):
+def _macos_arch(machine):
     return {'PowerPC': 'ppc', 'Power_Macintosh': 'ppc'}.get(machine, machine)
 
 
@@ -388,18 +415,19 @@ def get_build_platform():
     """Return this platform's string for platform-specific distributions
 
     XXX Currently this is the same as ``distutils.util.get_platform()``, but it
-    needs some hacks for Linux and Mac OS X.
+    needs some hacks for Linux and macOS.
     """
     from sysconfig import get_platform
 
     plat = get_platform()
     if sys.platform == "darwin" and not plat.startswith('macosx-'):
         try:
-            version = _macosx_vers()
+            version = _macos_vers()
             machine = os.uname()[4].replace(" ", "_")
             return "macosx-%d.%d-%s" % (
-                int(version[0]), int(version[1]),
-                _macosx_arch(machine),
+                int(version[0]),
+                int(version[1]),
+                _macos_arch(machine),
             )
         except ValueError:
             # if someone is running a non-Mac darwin system, this will fall
@@ -425,7 +453,7 @@ def compatible_platforms(provided, required):
         # easy case
         return True
 
-    # Mac OS X special cases
+    # macOS special cases
     reqMac = macosVersionString.match(required)
     if reqMac:
         provMac = macosVersionString.match(provided)
@@ -434,20 +462,23 @@ def compatible_platforms(provided, required):
         if not provMac:
             # this is backwards compatibility for packages built before
             # setuptools 0.6. All packages built after this point will
-            # use the new macosx designation.
+            # use the new macOS designation.
             provDarwin = darwinVersionString.match(provided)
             if provDarwin:
                 dversion = int(provDarwin.group(1))
                 macosversion = "%s.%s" % (reqMac.group(1), reqMac.group(2))
-                if dversion == 7 and macosversion >= "10.3" or \
-                        dversion == 8 and macosversion >= "10.4":
+                if (
+                    dversion == 7
+                    and macosversion >= "10.3"
+                    or dversion == 8
+                    and macosversion >= "10.4"
+                ):
                     return True
-            # egg isn't macosx or legacy darwin
+            # egg isn't macOS or legacy darwin
             return False
 
         # are they the same major version and machine type?
-        if provMac.group(1) != reqMac.group(1) or \
-                provMac.group(3) != reqMac.group(3):
+        if provMac.group(1) != reqMac.group(1) or provMac.group(3) != reqMac.group(3):
             return False
 
         # is the required OS major update >= the provided one?
@@ -475,7 +506,7 @@ run_main = run_script
 
 def get_distribution(dist):
     """Return a current distribution object for a Requirement or string"""
-    if isinstance(dist, six.string_types):
+    if isinstance(dist, str):
         dist = Requirement.parse(dist)
     if isinstance(dist, Requirement):
         dist = get_provider(dist)
@@ -499,54 +530,54 @@ def get_entry_info(dist, group, name):
     return get_distribution(dist).get_entry_info(group, name)
 
 
-class IMetadataProvider:
-    def has_metadata(name):
+class IMetadataProvider(Protocol):
+    def has_metadata(self, name):
         """Does the package's distribution contain the named metadata?"""
 
-    def get_metadata(name):
+    def get_metadata(self, name):
         """The named metadata resource as a string"""
 
-    def get_metadata_lines(name):
+    def get_metadata_lines(self, name):
         """Yield named metadata resource as list of non-blank non-comment lines
 
-       Leading and trailing whitespace is stripped from each line, and lines
-       with ``#`` as the first non-blank character are omitted."""
+        Leading and trailing whitespace is stripped from each line, and lines
+        with ``#`` as the first non-blank character are omitted."""
 
-    def metadata_isdir(name):
+    def metadata_isdir(self, name):
         """Is the named metadata a directory?  (like ``os.path.isdir()``)"""
 
-    def metadata_listdir(name):
+    def metadata_listdir(self, name):
         """List of metadata names in the directory (like ``os.listdir()``)"""
 
-    def run_script(script_name, namespace):
+    def run_script(self, script_name, namespace):
         """Execute the named script in the supplied namespace dictionary"""
 
 
-class IResourceProvider(IMetadataProvider):
+class IResourceProvider(IMetadataProvider, Protocol):
     """An object that provides access to package resources"""
 
-    def get_resource_filename(manager, resource_name):
+    def get_resource_filename(self, manager, resource_name):
         """Return a true filesystem path for `resource_name`
 
         `manager` must be an ``IResourceManager``"""
 
-    def get_resource_stream(manager, resource_name):
+    def get_resource_stream(self, manager, resource_name):
         """Return a readable file-like object for `resource_name`
 
         `manager` must be an ``IResourceManager``"""
 
-    def get_resource_string(manager, resource_name):
+    def get_resource_string(self, manager, resource_name):
         """Return a string containing the contents of `resource_name`
 
         `manager` must be an ``IResourceManager``"""
 
-    def has_resource(resource_name):
+    def has_resource(self, resource_name):
         """Does the package contain the named resource?"""
 
-    def resource_isdir(resource_name):
+    def resource_isdir(self, resource_name):
         """Is the named resource a directory?  (like ``os.path.isdir()``)"""
 
-    def resource_listdir(resource_name):
+    def resource_listdir(self, resource_name):
         """List of resource names in the directory (like ``os.listdir()``)"""
 
 
@@ -558,6 +589,7 @@ class WorkingSet:
         self.entries = []
         self.entry_keys = {}
         self.by_key = {}
+        self.normalized_to_canonical_keys = {}
         self.callbacks = []
 
         if entries is None:
@@ -638,6 +670,14 @@ class WorkingSet:
         is returned.
         """
         dist = self.by_key.get(req.key)
+
+        if dist is None:
+            canonical_key = self.normalized_to_canonical_keys.get(req.key)
+
+            if canonical_key is not None:
+                req.key = canonical_key
+                dist = self.by_key.get(canonical_key)
+
         if dist is not None and dist not in req:
             # XXX add more info
             raise VersionConflict(dist, req)
@@ -706,14 +746,22 @@ class WorkingSet:
             return
 
         self.by_key[dist.key] = dist
+        normalized_name = packaging.utils.canonicalize_name(dist.key)
+        self.normalized_to_canonical_keys[normalized_name] = dist.key
         if dist.key not in keys:
             keys.append(dist.key)
         if dist.key not in keys2:
             keys2.append(dist.key)
         self._added_new(dist)
 
-    def resolve(self, requirements, env=None, installer=None,
-                replace_conflicting=False, extras=None):
+    def resolve(
+        self,
+        requirements,
+        env=None,
+        installer=None,
+        replace_conflicting=False,
+        extras=None,
+    ):
         """List all distributions needed to (recursively) meet `requirements`
 
         `requirements` must be a sequence of ``Requirement`` objects.  `env`,
@@ -762,33 +810,9 @@ class WorkingSet:
             if not req_extras.markers_pass(req, extras):
                 continue
 
-            dist = best.get(req.key)
-            if dist is None:
-                # Find the best distribution and add it to the map
-                dist = self.by_key.get(req.key)
-                if dist is None or (dist not in req and replace_conflicting):
-                    ws = self
-                    if env is None:
-                        if dist is None:
-                            env = Environment(self.entries)
-                        else:
-                            # Use an empty environment and workingset to avoid
-                            # any further conflicts with the conflicting
-                            # distribution
-                            env = Environment([])
-                            ws = WorkingSet([])
-                    dist = best[req.key] = env.best_match(
-                        req, ws, installer,
-                        replace_conflicting=replace_conflicting
-                    )
-                    if dist is None:
-                        requirers = required_by.get(req, None)
-                        raise DistributionNotFound(req, requirers)
-                to_activate.append(dist)
-            if dist not in req:
-                # Oops, the "best" so far conflicts with a dependency
-                dependent_req = required_by[req]
-                raise VersionConflict(dist, req).with_context(dependent_req)
+            dist = self._resolve_dist(
+                req, best, replace_conflicting, env, installer, required_by, to_activate
+            )
 
             # push the new requirements onto the stack
             new_requirements = dist.requires(req.extras)[::-1]
@@ -804,8 +828,38 @@ class WorkingSet:
         # return list of distros to activate
         return to_activate
 
-    def find_plugins(
-            self, plugin_env, full_env=None, installer=None, fallback=True):
+    def _resolve_dist(
+        self, req, best, replace_conflicting, env, installer, required_by, to_activate
+    ):
+        dist = best.get(req.key)
+        if dist is None:
+            # Find the best distribution and add it to the map
+            dist = self.by_key.get(req.key)
+            if dist is None or (dist not in req and replace_conflicting):
+                ws = self
+                if env is None:
+                    if dist is None:
+                        env = Environment(self.entries)
+                    else:
+                        # Use an empty environment and workingset to avoid
+                        # any further conflicts with the conflicting
+                        # distribution
+                        env = Environment([])
+                        ws = WorkingSet([])
+                dist = best[req.key] = env.best_match(
+                    req, ws, installer, replace_conflicting=replace_conflicting
+                )
+                if dist is None:
+                    requirers = required_by.get(req, None)
+                    raise DistributionNotFound(req, requirers)
+            to_activate.append(dist)
+        if dist not in req:
+            # Oops, the "best" so far conflicts with a dependency
+            dependent_req = required_by[req]
+            raise VersionConflict(dist, req).with_context(dependent_req)
+        return dist
+
+    def find_plugins(self, plugin_env, full_env=None, installer=None, fallback=True):
         """Find all activatable distributions in `plugin_env`
 
         Example usage::
@@ -858,9 +912,7 @@ class WorkingSet:
         list(map(shadow_set.add, self))
 
         for project_name in plugin_projects:
-
             for dist in plugin_env[project_name]:
-
                 req = [dist.as_requirement()]
 
                 try:
@@ -924,15 +976,19 @@ class WorkingSet:
 
     def __getstate__(self):
         return (
-            self.entries[:], self.entry_keys.copy(), self.by_key.copy(),
-            self.callbacks[:]
+            self.entries[:],
+            self.entry_keys.copy(),
+            self.by_key.copy(),
+            self.normalized_to_canonical_keys.copy(),
+            self.callbacks[:],
         )
 
-    def __setstate__(self, e_k_b_c):
-        entries, keys, by_key, callbacks = e_k_b_c
+    def __setstate__(self, e_k_b_n_c):
+        entries, keys, by_key, normalized_to_canonical_keys, callbacks = e_k_b_n_c
         self.entries = entries[:]
         self.entry_keys = keys.copy()
         self.by_key = by_key.copy()
+        self.normalized_to_canonical_keys = normalized_to_canonical_keys.copy()
         self.callbacks = callbacks[:]
 
 
@@ -960,8 +1016,8 @@ class Environment:
     """Searchable snapshot of distributions on a search path"""
 
     def __init__(
-            self, search_path=None, platform=get_supported_platform(),
-            python=PY_MAJOR):
+        self, search_path=None, platform=get_supported_platform(), python=PY_MAJOR
+    ):
         """Snapshot distributions available on a search path
 
         Any distributions found on `search_path` are added to the environment.
@@ -1028,16 +1084,14 @@ class Environment:
         return self._distmap.get(distribution_key, [])
 
     def add(self, dist):
-        """Add `dist` if we ``can_add()`` it and it has not already been added
-        """
+        """Add `dist` if we ``can_add()`` it and it has not already been added"""
         if self.can_add(dist) and dist.has_version():
             dists = self._distmap.setdefault(dist.key, [])
             if dist not in dists:
                 dists.append(dist)
                 dists.sort(key=operator.attrgetter('hashcmp'), reverse=True)
 
-    def best_match(
-            self, req, working_set, installer=None, replace_conflicting=False):
+    def best_match(self, req, working_set, installer=None, replace_conflicting=False):
         """Find distribution best matching `req` and usable on `working_set`
 
         This calls the ``find(req)`` method of the `working_set` to see if a
@@ -1073,8 +1127,7 @@ class Environment:
         None is returned instead.  This method is a hook that allows subclasses
         to attempt other ways of obtaining a distribution before falling back
         to the `installer` argument."""
-        if installer is not None:
-            return installer(requirement)
+        return installer(requirement) if installer else None
 
     def __iter__(self):
         """Yield the unique project names of the available distributions"""
@@ -1124,6 +1177,7 @@ class ExtractionError(RuntimeError):
 
 class ResourceManager:
     """Manage resource extraction and packages"""
+
     extraction_path = None
 
     def __init__(self):
@@ -1135,9 +1189,7 @@ class ResourceManager:
 
     def resource_isdir(self, package_or_requirement, resource_name):
         """Is the named resource an existing directory?"""
-        return get_provider(package_or_requirement).resource_isdir(
-            resource_name
-        )
+        return get_provider(package_or_requirement).resource_isdir(resource_name)
 
     def resource_filename(self, package_or_requirement, resource_name):
         """Return a true filesystem path for specified resource"""
@@ -1159,9 +1211,7 @@ class ResourceManager:
 
     def resource_listdir(self, package_or_requirement, resource_name):
         """List the contents of the named resource directory"""
-        return get_provider(package_or_requirement).resource_listdir(
-            resource_name
-        )
+        return get_provider(package_or_requirement).resource_listdir(resource_name)
 
     def extraction_error(self):
         """Give an error message for problems extracting file(s)"""
@@ -1169,7 +1219,8 @@ class ResourceManager:
         old_exc = sys.exc_info()[1]
         cache_path = self.extraction_path or get_default_cache()
 
-        tmpl = textwrap.dedent("""
+        tmpl = textwrap.dedent(
+            """
             Can't extract file(s) to egg cache
 
             The following error occurred while trying to extract file(s)
@@ -1184,7 +1235,8 @@ class ResourceManager:
             Perhaps your account does not have write access to this directory?
             You can change the cache directory by setting the PYTHON_EGG_CACHE
             environment variable to point to an accessible directory.
-            """).lstrip()
+            """
+        ).lstrip()
         err = ExtractionError(tmpl.format(**locals()))
         err.manager = self
         err.cache_path = cache_path
@@ -1234,12 +1286,13 @@ class ResourceManager:
         mode = os.stat(path).st_mode
         if mode & stat.S_IWOTH or mode & stat.S_IWGRP:
             msg = (
-                "%s is writable by group/others and vulnerable to attack "
-                "when "
-                "used with get_resource_filename. Consider a more secure "
+                "Extraction path is writable by group/others "
+                "and vulnerable to attack when "
+                "used with get_resource_filename ({path}). "
+                "Consider a more secure "
                 "location (set with .set_extraction_path or the "
-                "PYTHON_EGG_CACHE environment variable)." % path
-            )
+                "PYTHON_EGG_CACHE environment variable)."
+            ).format(**locals())
             warnings.warn(msg, UserWarning)
 
     def postprocess(self, tempname, filename):
@@ -1282,9 +1335,7 @@ class ResourceManager:
         ``cleanup_resources()``.)
         """
         if self.cached_files:
-            raise ValueError(
-                "Can't change extraction path, files already extracted"
-            )
+            raise ValueError("Can't change extraction path, files already extracted")
 
         self.extraction_path = path
 
@@ -1308,9 +1359,8 @@ def get_default_cache():
     or a platform-relevant user cache dir for an app
     named "Python-Eggs".
     """
-    return (
-        os.environ.get('PYTHON_EGG_CACHE')
-        or appdirs.user_cache_dir(appname='Python-Eggs')
+    return os.environ.get('PYTHON_EGG_CACHE') or platformdirs.user_cache_dir(
+        appname='Python-Eggs'
     )
 
 
@@ -1334,6 +1384,38 @@ def safe_version(version):
         return re.sub('[^A-Za-z0-9.]+', '-', version)
 
 
+def _forgiving_version(version):
+    """Fallback when ``safe_version`` is not safe enough
+    >>> parse_version(_forgiving_version('0.23ubuntu1'))
+    
+    >>> parse_version(_forgiving_version('0.23-'))
+    
+    >>> parse_version(_forgiving_version('0.-_'))
+    
+    >>> parse_version(_forgiving_version('42.+?1'))
+    
+    >>> parse_version(_forgiving_version('hello world'))
+    
+    """
+    version = version.replace(' ', '.')
+    match = _PEP440_FALLBACK.search(version)
+    if match:
+        safe = match["safe"]
+        rest = version[len(safe) :]
+    else:
+        safe = "0"
+        rest = version
+    local = f"sanitized.{_safe_segment(rest)}".strip(".")
+    return f"{safe}.dev0+{local}"
+
+
+def _safe_segment(segment):
+    """Convert an arbitrary string into a safe segment"""
+    segment = re.sub('[^A-Za-z0-9.]+', '-', segment)
+    segment = re.sub('-[^A-Za-z0-9]+', '-', segment)
+    return re.sub(r'\.[^A-Za-z0-9]+', '.', segment).strip(".-")
+
+
 def safe_extra(extra):
     """Convert an arbitrary string to a standard 'extra' name
 
@@ -1377,7 +1459,7 @@ def evaluate_marker(text, extra=None):
         marker = packaging.markers.Marker(text)
         return marker.evaluate()
     except packaging.markers.InvalidMarker as e:
-        raise SyntaxError(e)
+        raise SyntaxError(e) from e
 
 
 class NullProvider:
@@ -1418,8 +1500,6 @@ class NullProvider:
             return ""
         path = self._get_metadata_path(name)
         value = self._get(path)
-        if six.PY2:
-            return value
         try:
             return value.decode('utf-8')
         except UnicodeDecodeError as exc:
@@ -1449,21 +1529,27 @@ class NullProvider:
         script = 'scripts/' + script_name
         if not self.has_metadata(script):
             raise ResolutionError(
-                "Script {script!r} not found in metadata at {self.egg_info!r}"
-                .format(**locals()),
+                "Script {script!r} not found in metadata at {self.egg_info!r}".format(
+                    **locals()
+                ),
             )
         script_text = self.get_metadata(script).replace('\r\n', '\n')
         script_text = script_text.replace('\r', '\n')
         script_filename = self._fn(self.egg_info, script)
         namespace['__file__'] = script_filename
         if os.path.exists(script_filename):
-            source = open(script_filename).read()
+            with open(script_filename) as fid:
+                source = fid.read()
             code = compile(source, script_filename, 'exec')
             exec(code, namespace, namespace)
         else:
             from linecache import cache
+
             cache[script_filename] = (
-                len(script_text), 0, script_text.split('\n'), script_filename
+                len(script_text),
+                0,
+                script_text.split('\n'),
+                script_filename,
             )
             script_code = compile(script_text, script_filename, 'exec')
             exec(script_code, namespace, namespace)
@@ -1493,7 +1579,7 @@ class NullProvider:
     def _validate_resource_path(path):
         """
         Validate the resource paths according to the docs.
-        https://setuptools.readthedocs.io/en/latest/pkg_resources.html#basic-resource-access
+        https://setuptools.pypa.io/en/latest/pkg_resources.html#basic-resource-access
 
         >>> warned = getfixture('recwarn')
         >>> warnings.simplefilter('always')
@@ -1543,9 +1629,9 @@ is not allowed.
         AttributeError: ...
         """
         invalid = (
-            os.path.pardir in path.split(posixpath.sep) or
-            posixpath.isabs(path) or
-            ntpath.isabs(path)
+            os.path.pardir in path.split(posixpath.sep)
+            or posixpath.isabs(path)
+            or ntpath.isabs(path)
         )
         if not invalid:
             return
@@ -1558,10 +1644,9 @@ is not allowed.
 
         # for compatibility, warn; in future
         # raise ValueError(msg)
-        warnings.warn(
+        issue_warning(
             msg[:-1] + " and will raise exceptions in a future release.",
             DeprecationWarning,
-            stacklevel=4,
         )
 
     def _get(self, path):
@@ -1575,26 +1660,35 @@ is not allowed.
 register_loader_type(object, NullProvider)
 
 
+def _parents(path):
+    """
+    yield all parents of path including path
+    """
+    last = None
+    while path != last:
+        yield path
+        last = path
+        path, _ = os.path.split(path)
+
+
 class EggProvider(NullProvider):
     """Provider based on a virtual filesystem"""
 
     def __init__(self, module):
-        NullProvider.__init__(self, module)
+        super().__init__(module)
         self._setup_prefix()
 
     def _setup_prefix(self):
-        # we assume here that our metadata may be nested inside a "basket"
-        # of multiple eggs; that's why we use module_path instead of .archive
-        path = self.module_path
-        old = None
-        while path != old:
-            if _is_egg_path(path):
-                self.egg_name = os.path.basename(path)
-                self.egg_info = os.path.join(path, 'EGG-INFO')
-                self.egg_root = path
-                break
-            old = path
-            path, base = os.path.split(path)
+        # Assume that metadata may be nested inside a "basket"
+        # of multiple eggs and use module_path instead of .archive.
+        eggs = filter(_is_egg_path, _parents(self.module_path))
+        egg = next(eggs, None)
+        egg and self._set_egg(egg)
+
+    def _set_egg(self, path):
+        self.egg_name = os.path.basename(path)
+        self.egg_info = os.path.join(path, 'EGG-INFO')
+        self.egg_root = path
 
 
 class DefaultProvider(EggProvider):
@@ -1618,9 +1712,12 @@ class DefaultProvider(EggProvider):
 
     @classmethod
     def _register(cls):
-        loader_names = 'SourceFileLoader', 'SourcelessFileLoader',
+        loader_names = (
+            'SourceFileLoader',
+            'SourcelessFileLoader',
+        )
         for name in loader_names:
-            loader_cls = getattr(importlib_machinery, name, type(None))
+            loader_cls = getattr(importlib.machinery, name, type(None))
             register_loader_type(loader_cls, cls)
 
 
@@ -1678,6 +1775,7 @@ class MemoizedZipManifests(ZipManifests):
     """
     Memoized zipfile manifests.
     """
+
     manifest_mod = collections.namedtuple('manifest_mod', 'manifest mtime')
 
     def load(self, path):
@@ -1701,7 +1799,7 @@ class ZipProvider(EggProvider):
     _zip_manifests = MemoizedZipManifests()
 
     def __init__(self, module):
-        EggProvider.__init__(self, module)
+        super().__init__(module)
         self.zip_pre = self.loader.archive + os.sep
 
     def _zipinfo_name(self, fspath):
@@ -1711,20 +1809,16 @@ class ZipProvider(EggProvider):
         if fspath == self.loader.archive:
             return ''
         if fspath.startswith(self.zip_pre):
-            return fspath[len(self.zip_pre):]
-        raise AssertionError(
-            "%s is not a subpath of %s" % (fspath, self.zip_pre)
-        )
+            return fspath[len(self.zip_pre) :]
+        raise AssertionError("%s is not a subpath of %s" % (fspath, self.zip_pre))
 
     def _parts(self, zip_path):
         # Convert a zipfile subpath into an egg-relative path part list.
         # pseudo-fs path
         fspath = self.zip_pre + zip_path
         if fspath.startswith(self.egg_root + os.sep):
-            return fspath[len(self.egg_root) + 1:].split(os.sep)
-        raise AssertionError(
-            "%s is not a subpath of %s" % (fspath, self.egg_root)
-        )
+            return fspath[len(self.egg_root) + 1 :].split(os.sep)
+        raise AssertionError("%s is not a subpath of %s" % (fspath, self.egg_root))
 
     @property
     def zipinfo(self):
@@ -1752,26 +1846,22 @@ class ZipProvider(EggProvider):
         timestamp = time.mktime(date_time)
         return timestamp, size
 
-    def _extract_resource(self, manager, zip_path):
-
+    # FIXME: 'ZipProvider._extract_resource' is too complex (12)
+    def _extract_resource(self, manager, zip_path):  # noqa: C901
         if zip_path in self._index():
             for name in self._index()[zip_path]:
-                last = self._extract_resource(
-                    manager, os.path.join(zip_path, name)
-                )
+                last = self._extract_resource(manager, os.path.join(zip_path, name))
             # return the extracted directory name
             return os.path.dirname(last)
 
         timestamp, size = self._get_date_and_size(self.zipinfo[zip_path])
 
         if not WRITE_SUPPORT:
-            raise IOError('"os.rename" and "os.unlink" are not supported '
-                          'on this platform')
-        try:
-
-            real_path = manager.get_cache_path(
-                self.egg_name, self._parts(zip_path)
+            raise OSError(
+                '"os.rename" and "os.unlink" are not supported ' 'on this platform'
             )
+        try:
+            real_path = manager.get_cache_path(self.egg_name, self._parts(zip_path))
 
             if self._is_current(real_path, zip_path):
                 return real_path
@@ -1788,7 +1878,7 @@ class ZipProvider(EggProvider):
             try:
                 rename(tmpnam, real_path)
 
-            except os.error:
+            except OSError:
                 if os.path.isfile(real_path):
                     if self._is_current(real_path, zip_path):
                         # the file became current since it was checked above,
@@ -1801,7 +1891,7 @@ class ZipProvider(EggProvider):
                         return real_path
                 raise
 
-        except os.error:
+        except OSError:
             # report a user-friendly error
             manager.extraction_error()
 
@@ -1894,14 +1984,13 @@ class FileMetadata(EmptyProvider):
         if name != 'PKG-INFO':
             raise KeyError("No metadata except PKG-INFO is available")
 
-        with io.open(self.path, encoding='utf-8', errors="replace") as f:
+        with open(self.path, encoding='utf-8', errors="replace") as f:
             metadata = f.read()
         self._warn_on_replacement(metadata)
         return metadata
 
     def _warn_on_replacement(self, metadata):
-        # Python 2.7 compat for: replacement_char = '�'
-        replacement_char = b'\xef\xbf\xbd'.decode('utf-8')
+        replacement_char = '�'
         if replacement_char in metadata:
             tmpl = "{self.path} could not be properly decoded in UTF-8"
             msg = tmpl.format(**locals())
@@ -1989,9 +2078,8 @@ def find_eggs_in_zip(importer, path_item, only=False):
         if _is_egg_path(subitem):
             subpath = os.path.join(path_item, subitem)
             dists = find_eggs_in_zip(zipimport.zipimporter(subpath), subpath)
-            for dist in dists:
-                yield dist
-        elif subitem.lower().endswith('.dist-info'):
+            yield from dists
+        elif subitem.lower().endswith(('.dist-info', '.egg-info')):
             subpath = os.path.join(path_item, subitem)
             submeta = EggMetadata(zipimport.zipimporter(subpath))
             submeta.egg_info = subpath
@@ -2008,78 +2096,42 @@ def find_nothing(importer, path_item, only=False):
 register_finder(object, find_nothing)
 
 
-def _by_version_descending(names):
-    """
-    Given a list of filenames, return them in descending order
-    by version number.
-
-    >>> names = 'bar', 'foo', 'Python-2.7.10.egg', 'Python-2.7.2.egg'
-    >>> _by_version_descending(names)
-    ['Python-2.7.10.egg', 'Python-2.7.2.egg', 'foo', 'bar']
-    >>> names = 'Setuptools-1.2.3b1.egg', 'Setuptools-1.2.3.egg'
-    >>> _by_version_descending(names)
-    ['Setuptools-1.2.3.egg', 'Setuptools-1.2.3b1.egg']
-    >>> names = 'Setuptools-1.2.3b1.egg', 'Setuptools-1.2.3.post1.egg'
-    >>> _by_version_descending(names)
-    ['Setuptools-1.2.3.post1.egg', 'Setuptools-1.2.3b1.egg']
-    """
-    def _by_version(name):
-        """
-        Parse each component of the filename
-        """
-        name, ext = os.path.splitext(name)
-        parts = itertools.chain(name.split('-'), [ext])
-        return [packaging.version.parse(part) for part in parts]
-
-    return sorted(names, key=_by_version, reverse=True)
-
-
 def find_on_path(importer, path_item, only=False):
     """Yield distributions accessible on a sys.path directory"""
     path_item = _normalize_cached(path_item)
 
     if _is_unpacked_egg(path_item):
         yield Distribution.from_filename(
-            path_item, metadata=PathMetadata(
-                path_item, os.path.join(path_item, 'EGG-INFO')
-            )
+            path_item,
+            metadata=PathMetadata(path_item, os.path.join(path_item, 'EGG-INFO')),
         )
         return
 
-    entries = safe_listdir(path_item)
-
-    # for performance, before sorting by version,
-    # screen entries for only those that will yield
-    # distributions
-    filtered = (
-        entry
-        for entry in entries
-        if dist_factory(path_item, entry, only)
-    )
+    entries = (os.path.join(path_item, child) for child in safe_listdir(path_item))
 
     # scan for .egg and .egg-info in directory
-    path_item_entries = _by_version_descending(filtered)
-    for entry in path_item_entries:
+    for entry in sorted(entries):
         fullpath = os.path.join(path_item, entry)
         factory = dist_factory(path_item, entry, only)
-        for dist in factory(fullpath):
-            yield dist
+        yield from factory(fullpath)
 
 
 def dist_factory(path_item, entry, only):
-    """
-    Return a dist_factory for a path_item and entry
-    """
+    """Return a dist_factory for the given entry."""
     lower = entry.lower()
-    is_meta = any(map(lower.endswith, ('.egg-info', '.dist-info')))
+    is_egg_info = lower.endswith('.egg-info')
+    is_dist_info = lower.endswith('.dist-info') and os.path.isdir(
+        os.path.join(path_item, entry)
+    )
+    is_meta = is_egg_info or is_dist_info
     return (
         distributions_from_metadata
-        if is_meta else
-        find_distributions
-        if not only and _is_egg_path(entry) else
-        resolve_egg_link
-        if not only and lower.endswith('.egg-link') else
-        NoDists()
+        if is_meta
+        else find_distributions
+        if not only and _is_egg_path(entry)
+        else resolve_egg_link
+        if not only and lower.endswith('.egg-link')
+        else NoDists()
     )
 
 
@@ -2091,10 +2143,9 @@ class NoDists:
     >>> list(NoDists()('anything'))
     []
     """
+
     def __bool__(self):
         return False
-    if six.PY2:
-        __nonzero__ = __bool__
 
     def __call__(self, fullpath):
         return iter(())
@@ -2111,12 +2162,7 @@ def safe_listdir(path):
     except OSError as e:
         # Ignore the directory if does not exist, not a directory or
         # permission denied
-        ignorable = (
-            e.errno in (errno.ENOTDIR, errno.EACCES, errno.ENOENT)
-            # Python 2 on Windows needs to be handled this way :(
-            or getattr(e, "winerror", None) == 267
-        )
-        if not ignorable:
+        if e.errno not in (errno.ENOTDIR, errno.EACCES, errno.ENOENT):
             raise
     return ()
 
@@ -2132,7 +2178,10 @@ def distributions_from_metadata(path):
         metadata = FileMetadata(path)
     entry = os.path.basename(path)
     yield Distribution.from_location(
-        root, entry, metadata, precedence=DEVELOP_DIST,
+        root,
+        entry,
+        metadata,
+        precedence=DEVELOP_DIST,
     )
 
 
@@ -2154,17 +2203,16 @@ def resolve_egg_link(path):
     """
     referenced_paths = non_empty_lines(path)
     resolved_paths = (
-        os.path.join(os.path.dirname(path), ref)
-        for ref in referenced_paths
+        os.path.join(os.path.dirname(path), ref) for ref in referenced_paths
     )
     dist_groups = map(find_distributions, resolved_paths)
     return next(dist_groups, ())
 
 
-register_finder(pkgutil.ImpImporter, find_on_path)
+if hasattr(pkgutil, 'ImpImporter'):
+    register_finder(pkgutil.ImpImporter, find_on_path)
 
-if hasattr(importlib_machinery, 'FileFinder'):
-    register_finder(importlib_machinery.FileFinder, find_on_path)
+register_finder(importlib.machinery.FileFinder, find_on_path)
 
 _declare_state('dict', _namespace_handlers={})
 _declare_state('dict', _namespace_packages={})
@@ -2195,10 +2243,16 @@ def _handle_ns(packageName, path_item):
     if importer is None:
         return None
 
-    # capture warnings due to #1111
-    with warnings.catch_warnings():
-        warnings.simplefilter("ignore")
-        loader = importer.find_module(packageName)
+    # use find_spec (PEP 451) and fall-back to find_module (PEP 302)
+    try:
+        spec = importer.find_spec(packageName)
+    except AttributeError:
+        # capture warnings due to #1111
+        with warnings.catch_warnings():
+            warnings.simplefilter("ignore")
+            loader = importer.find_module(packageName)
+    else:
+        loader = spec.loader if spec else None
 
     if loader is None:
         return None
@@ -2214,7 +2268,7 @@ def _handle_ns(packageName, path_item):
     if subpath is not None:
         path = module.__path__
         path.append(subpath)
-        loader.load_module(packageName)
+        importlib.import_module(packageName)
         _rebuild_mod_path(path, packageName, module)
     return subpath
 
@@ -2256,6 +2310,15 @@ def _rebuild_mod_path(orig_path, package_name, module):
 def declare_namespace(packageName):
     """Declare that package 'packageName' is a namespace package"""
 
+    msg = (
+        f"Deprecated call to `pkg_resources.declare_namespace({packageName!r})`.\n"
+        "Implementing implicit namespace packages (as specified in PEP 420) "
+        "is preferred to `pkg_resources.declare_namespace`. "
+        "See https://setuptools.pypa.io/en/latest/references/"
+        "keywords.html#keyword-namespace-packages"
+    )
+    warnings.warn(msg, DeprecationWarning, stacklevel=2)
+
     _imp.acquire_lock()
     try:
         if packageName in _namespace_packages:
@@ -2270,8 +2333,8 @@ def declare_namespace(packageName):
                 __import__(parent)
             try:
                 path = sys.modules[parent].__path__
-            except AttributeError:
-                raise TypeError("Not a package:", parent)
+            except AttributeError as e:
+                raise TypeError("Not a package:", parent) from e
 
         # Track what packages are namespaces, so when new path items are added,
         # they can be updated
@@ -2312,11 +2375,11 @@ def file_ns_handler(importer, path_item, packageName, module):
         return subpath
 
 
-register_namespace_handler(pkgutil.ImpImporter, file_ns_handler)
-register_namespace_handler(zipimport.zipimporter, file_ns_handler)
+if hasattr(pkgutil, 'ImpImporter'):
+    register_namespace_handler(pkgutil.ImpImporter, file_ns_handler)
 
-if hasattr(importlib_machinery, 'FileFinder'):
-    register_namespace_handler(importlib_machinery.FileFinder, file_ns_handler)
+register_namespace_handler(zipimport.zipimporter, file_ns_handler)
+register_namespace_handler(importlib.machinery.FileFinder, file_ns_handler)
 
 
 def null_ns_handler(importer, path_item, packageName, module):
@@ -2342,28 +2405,32 @@ def _cygwin_patch(filename):  # pragma: nocover
     return os.path.abspath(filename) if sys.platform == 'cygwin' else filename
 
 
-def _normalize_cached(filename, _cache={}):
-    try:
-        return _cache[filename]
-    except KeyError:
-        _cache[filename] = result = normalize_path(filename)
-        return result
+@functools.lru_cache(maxsize=None)
+def _normalize_cached(filename):
+    return normalize_path(filename)
 
 
 def _is_egg_path(path):
     """
     Determine if given path appears to be an egg.
     """
-    return path.lower().endswith('.egg')
+    return _is_zip_egg(path) or _is_unpacked_egg(path)
+
+
+def _is_zip_egg(path):
+    return (
+        path.lower().endswith('.egg')
+        and os.path.isfile(path)
+        and zipfile.is_zipfile(path)
+    )
 
 
 def _is_unpacked_egg(path):
     """
     Determine if given path appears to be an unpacked egg.
     """
-    return (
-        _is_egg_path(path) and
-        os.path.isfile(os.path.join(path, 'EGG-INFO', 'PKG-INFO'))
+    return path.lower().endswith('.egg') and os.path.isfile(
+        os.path.join(path, 'EGG-INFO', 'PKG-INFO')
     )
 
 
@@ -2375,20 +2442,6 @@ def _set_parent_ns(packageName):
         setattr(sys.modules[parent], name, sys.modules[packageName])
 
 
-def yield_lines(strs):
-    """Yield non-empty/non-comment lines of a string or sequence"""
-    if isinstance(strs, six.string_types):
-        for s in strs.splitlines():
-            s = s.strip()
-            # skip blank lines/comments
-            if s and not s.startswith('#'):
-                yield s
-    else:
-        for ss in strs:
-            for s in yield_lines(ss):
-                yield s
-
-
 MODULE = re.compile(r"\w+(\.\w+)*$").match
 EGG_NAME = re.compile(
     r"""
@@ -2450,7 +2503,7 @@ class EntryPoint:
         try:
             return functools.reduce(getattr, self.attrs, module)
         except AttributeError as exc:
-            raise ImportError(str(exc))
+            raise ImportError(str(exc)) from exc
 
     def require(self, env=None, installer=None):
         if self.extras and not self.dist:
@@ -2536,22 +2589,15 @@ class EntryPoint:
         return maps
 
 
-def _remove_md5_fragment(location):
-    if not location:
-        return ''
-    parsed = urllib.parse.urlparse(location)
-    if parsed[-1].startswith('md5='):
-        return urllib.parse.urlunparse(parsed[:-1] + ('',))
-    return location
-
-
 def _version_from_file(lines):
     """
     Given an iterable of lines from a Metadata file, return
     the value of the Version field, if present, or None otherwise.
     """
+
     def is_version_line(line):
         return line.lower().startswith('version:')
+
     version_lines = filter(is_version_line, lines)
     line = next(iter(version_lines), '')
     _, _, value = line.partition(':')
@@ -2560,12 +2606,19 @@ def _version_from_file(lines):
 
 class Distribution:
     """Wrap an actual or potential sys.path entry w/metadata"""
+
     PKG_INFO = 'PKG-INFO'
 
     def __init__(
-            self, location=None, metadata=None, project_name=None,
-            version=None, py_version=PY_MAJOR, platform=None,
-            precedence=EGG_DIST):
+        self,
+        location=None,
+        metadata=None,
+        project_name=None,
+        version=None,
+        py_version=PY_MAJOR,
+        platform=None,
+        precedence=EGG_DIST,
+    ):
         self.project_name = safe_name(project_name or 'Unknown')
         if version is not None:
             self._version = safe_version(version)
@@ -2588,8 +2641,13 @@ class Distribution:
                     'name', 'ver', 'pyver', 'plat'
                 )
         return cls(
-            location, metadata, project_name=project_name, version=version,
-            py_version=py_version, platform=platform, **kw
+            location,
+            metadata,
+            project_name=project_name,
+            version=version,
+            py_version=py_version,
+            platform=platform,
+            **kw,
         )._reload_version()
 
     def _reload_version(self):
@@ -2598,10 +2656,10 @@ class Distribution:
     @property
     def hashcmp(self):
         return (
-            self.parsed_version,
+            self._forgiving_parsed_version,
             self.precedence,
             self.key,
-            _remove_md5_fragment(self.location),
+            self.location,
             self.py_version or '',
             self.platform or '',
         )
@@ -2645,48 +2703,55 @@ class Distribution:
     @property
     def parsed_version(self):
         if not hasattr(self, "_parsed_version"):
-            self._parsed_version = parse_version(self.version)
+            try:
+                self._parsed_version = parse_version(self.version)
+            except packaging.version.InvalidVersion as ex:
+                info = f"(package: {self.project_name})"
+                if hasattr(ex, "add_note"):
+                    ex.add_note(info)  # PEP 678
+                    raise
+                raise packaging.version.InvalidVersion(f"{str(ex)} {info}") from None
 
         return self._parsed_version
 
-    def _warn_legacy_version(self):
-        LV = packaging.version.LegacyVersion
-        is_legacy = isinstance(self._parsed_version, LV)
-        if not is_legacy:
-            return
+    @property
+    def _forgiving_parsed_version(self):
+        try:
+            return self.parsed_version
+        except packaging.version.InvalidVersion as ex:
+            self._parsed_version = parse_version(_forgiving_version(self.version))
 
-        # While an empty version is technically a legacy version and
-        # is not a valid PEP 440 version, it's also unlikely to
-        # actually come from someone and instead it is more likely that
-        # it comes from setuptools attempting to parse a filename and
-        # including it in the list. So for that we'll gate this warning
-        # on if the version is anything at all or not.
-        if not self.version:
-            return
+            notes = "\n".join(getattr(ex, "__notes__", []))  # PEP 678
+            msg = f"""!!\n\n
+            *************************************************************************
+            {str(ex)}\n{notes}
 
-        tmpl = textwrap.dedent("""
-            '{project_name} ({version})' is being parsed as a legacy,
-            non PEP 440,
-            version. You may find odd behavior and sort order.
-            In particular it will be sorted as less than 0.0. It
-            is recommended to migrate to PEP 440 compatible
-            versions.
-            """).strip().replace('\n', ' ')
+            This is a long overdue deprecation.
+            For the time being, `pkg_resources` will use `{self._parsed_version}`
+            as a replacement to avoid breaking existing environments,
+            but no future compatibility is guaranteed.
 
-        warnings.warn(tmpl.format(**vars(self)), PEP440Warning)
+            If you maintain package {self.project_name} you should implement
+            the relevant changes to adequate the project to PEP 440 immediately.
+            *************************************************************************
+            \n\n!!
+            """
+            warnings.warn(msg, DeprecationWarning)
+
+            return self._parsed_version
 
     @property
     def version(self):
         try:
             return self._version
-        except AttributeError:
+        except AttributeError as e:
             version = self._get_version()
             if version is None:
                 path = self._get_metadata_path_for_display(self.PKG_INFO)
-                msg = (
-                    "Missing 'Version:' header and/or {} file at path: {}"
-                ).format(self.PKG_INFO, path)
-                raise ValueError(msg, self)
+                msg = ("Missing 'Version:' header and/or {} file at path: {}").format(
+                    self.PKG_INFO, path
+                )
+                raise ValueError(msg, self) from e
 
             return version
 
@@ -2714,8 +2779,7 @@ class Distribution:
             reqs = dm.pop(extra)
             new_extra, _, marker = extra.partition(':')
             fails_marker = marker and (
-                invalid_marker(marker)
-                or not evaluate_marker(marker)
+                invalid_marker(marker) or not evaluate_marker(marker)
             )
             if fails_marker:
                 reqs = []
@@ -2739,10 +2803,10 @@ class Distribution:
         for ext in extras:
             try:
                 deps.extend(dm[safe_extra(ext)])
-            except KeyError:
+            except KeyError as e:
                 raise UnknownExtra(
                     "%s has no such extra feature %r" % (self, ext)
-                )
+                ) from e
         return deps
 
     def _get_metadata_path_for_display(self, name):
@@ -2764,14 +2828,11 @@ class Distribution:
 
     def _get_metadata(self, name):
         if self.has_metadata(name):
-            for line in self.get_metadata_lines(name):
-                yield line
+            yield from self.get_metadata_lines(name)
 
     def _get_version(self):
         lines = self._get_metadata(self.PKG_INFO)
-        version = _version_from_file(lines)
-
-        return version
+        return _version_from_file(lines)
 
     def activate(self, path=None, replace=False):
         """Ensure distribution is importable on `path` (default=sys.path)"""
@@ -2787,8 +2848,9 @@ class Distribution:
     def egg_name(self):
         """Return what this distribution's standard .egg filename should be"""
         filename = "%s-%s-py%s" % (
-            to_filename(self.project_name), to_filename(self.version),
-            self.py_version or PY_MAJOR
+            to_filename(self.project_name),
+            to_filename(self.version),
+            self.py_version or PY_MAJOR,
         )
 
         if self.platform:
@@ -2817,22 +2879,14 @@ class Distribution:
 
     def __dir__(self):
         return list(
-            set(super(Distribution, self).__dir__())
-            | set(
-                attr for attr in self._provider.__dir__()
-                if not attr.startswith('_')
-            )
+            set(super().__dir__())
+            | set(attr for attr in self._provider.__dir__() if not attr.startswith('_'))
         )
 
-    if not hasattr(object, '__dir__'):
-        # python 2.7 not supported
-        del __dir__
-
     @classmethod
     def from_filename(cls, filename, metadata=None, **kw):
         return cls.from_location(
-            _normalize_cached(filename), os.path.basename(filename), metadata,
-            **kw
+            _normalize_cached(filename), os.path.basename(filename), metadata, **kw
         )
 
     def as_requirement(self):
@@ -2867,7 +2921,8 @@ class Distribution:
         """Return the EntryPoint object for `group`+`name`, or ``None``"""
         return self.get_entry_map(group).get(name)
 
-    def insert_on(self, path, loc=None, replace=False):
+    # FIXME: 'Distribution.insert_on' is too complex (13)
+    def insert_on(self, path, loc=None, replace=False):  # noqa: C901
         """Ensure self.location is on path
 
         If replace=False (default):
@@ -2943,14 +2998,18 @@ class Distribution:
         nsp = dict.fromkeys(self._get_metadata('namespace_packages.txt'))
         loc = normalize_path(self.location)
         for modname in self._get_metadata('top_level.txt'):
-            if (modname not in sys.modules or modname in nsp
-                    or modname in _namespace_packages):
+            if (
+                modname not in sys.modules
+                or modname in nsp
+                or modname in _namespace_packages
+            ):
                 continue
             if modname in ('pkg_resources', 'setuptools', 'site'):
                 continue
             fn = getattr(sys.modules[modname], '__file__', None)
-            if fn and (normalize_path(fn).startswith(loc) or
-                       fn.startswith(self.location)):
+            if fn and (
+                normalize_path(fn).startswith(loc) or fn.startswith(self.location)
+            ):
                 continue
             issue_warning(
                 "Module %s was already imported from %s, but %s is being added"
@@ -2963,6 +3022,9 @@ class Distribution:
         except ValueError:
             issue_warning("Unbuilt egg for " + repr(self))
             return False
+        except SystemError:
+            # TODO: remove this except clause when python/cpython#103632 is fixed.
+            return False
         return True
 
     def clone(self, **kw):
@@ -3002,6 +3064,7 @@ class DistInfoDistribution(Distribution):
     Wrap an actual or potential sys.path entry
     w/metadata, .dist-info style.
     """
+
     PKG_INFO = 'METADATA'
     EQEQ = re.compile(r"([\(,])\s*(\d.*?)\s*([,\)])")
 
@@ -3037,12 +3100,12 @@ class DistInfoDistribution(Distribution):
                 if not req.marker or req.marker.evaluate({'extra': extra}):
                     yield req
 
-        common = frozenset(reqs_for_extra(None))
+        common = types.MappingProxyType(dict.fromkeys(reqs_for_extra(None)))
         dm[None].extend(common)
 
         for extra in self._parsed_pkg_info.get_all('Provides-Extra') or []:
             s_extra = safe_extra(extra.strip())
-            dm[s_extra] = list(frozenset(reqs_for_extra(extra)) - common)
+            dm[s_extra] = [r for r in reqs_for_extra(extra) if r not in common]
 
         return dm
 
@@ -3067,45 +3130,27 @@ def issue_warning(*args, **kw):
     warnings.warn(stacklevel=level + 1, *args, **kw)
 
 
-class RequirementParseError(ValueError):
-    def __str__(self):
-        return ' '.join(self.args)
-
-
 def parse_requirements(strs):
-    """Yield ``Requirement`` objects for each specification in `strs`
+    """
+    Yield ``Requirement`` objects for each specification in `strs`.
 
     `strs` must be a string, or a (possibly-nested) iterable thereof.
     """
-    # create a steppable iterator, so we can handle \-continuations
-    lines = iter(yield_lines(strs))
+    return map(Requirement, join_continuation(map(drop_comment, yield_lines(strs))))
 
-    for line in lines:
-        # Drop comments -- a hash without a space may be in a URL.
-        if ' #' in line:
-            line = line[:line.find(' #')]
-        # If there is a line continuation, drop it, and append the next line.
-        if line.endswith('\\'):
-            line = line[:-2].strip()
-            try:
-                line += next(lines)
-            except StopIteration:
-                return
-        yield Requirement(line)
+
+class RequirementParseError(packaging.requirements.InvalidRequirement):
+    "Compatibility wrapper for InvalidRequirement"
 
 
 class Requirement(packaging.requirements.Requirement):
     def __init__(self, requirement_string):
         """DO NOT CALL THIS UNDOCUMENTED METHOD; use Requirement.parse()!"""
-        try:
-            super(Requirement, self).__init__(requirement_string)
-        except packaging.requirements.InvalidRequirement as e:
-            raise RequirementParseError(str(e))
+        super().__init__(requirement_string)
         self.unsafe_name = self.name
         project_name = safe_name(self.name)
         self.project_name, self.key = project_name, project_name.lower()
-        self.specs = [
-            (spec.operator, spec.version) for spec in self.specifier]
+        self.specs = [(spec.operator, spec.version) for spec in self.specifier]
         self.extras = tuple(map(safe_extra, self.extras))
         self.hashCmp = (
             self.key,
@@ -3117,10 +3162,7 @@ class Requirement(packaging.requirements.Requirement):
         self.__hash = hash(self.hashCmp)
 
     def __eq__(self, other):
-        return (
-            isinstance(other, Requirement) and
-            self.hashCmp == other.hashCmp
-        )
+        return isinstance(other, Requirement) and self.hashCmp == other.hashCmp
 
     def __ne__(self, other):
         return not self == other
@@ -3145,7 +3187,7 @@ class Requirement(packaging.requirements.Requirement):
 
     @staticmethod
     def parse(s):
-        req, = parse_requirements(s)
+        (req,) = parse_requirements(s)
         return req
 
 
@@ -3165,18 +3207,19 @@ def _find_adapter(registry, ob):
     for t in types:
         if t in registry:
             return registry[t]
+    return None
 
 
 def ensure_directory(path):
     """Ensure that the parent directory of `path` exists"""
     dirname = os.path.dirname(path)
-    py31compat.makedirs(dirname, exist_ok=True)
+    os.makedirs(dirname, exist_ok=True)
 
 
 def _bypass_ensure_directory(path):
     """Sandbox-bypassing version of ensure_directory()"""
     if not WRITE_SUPPORT:
-        raise IOError('"os.mkdir" not supported on this platform.')
+        raise OSError('"os.mkdir" not supported on this platform.')
     dirname, filename = split(path)
     if dirname and filename and not isdir(dirname):
         _bypass_ensure_directory(dirname)
@@ -3248,6 +3291,15 @@ def _initialize(g=globals()):
     )
 
 
+class PkgResourcesDeprecationWarning(Warning):
+    """
+    Base class for warning about deprecations in ``pkg_resources``
+
+    This class is not derived from ``DeprecationWarning``, and as such is
+    visible by default.
+    """
+
+
 @_call_aside
 def _initialize_master_working_set():
     """
@@ -3274,10 +3326,7 @@ def _initialize_master_working_set():
     # ensure that all distributions added to the working set in the future
     # (e.g. by calling ``require()``) will get activated as well,
     # with higher priority (replace=True).
-    tuple(
-        dist.activate(replace=False)
-        for dist in working_set
-    )
+    tuple(dist.activate(replace=False) for dist in working_set)
     add_activation_listener(
         lambda dist: dist.activate(replace=True),
         existing=False,
@@ -3286,11 +3335,3 @@ def _initialize_master_working_set():
     # match order
     list(map(working_set.add_entry, sys.path))
     globals().update(locals())
-
-class PkgResourcesDeprecationWarning(Warning):
-    """
-    Base class for warning about deprecations in ``pkg_resources``
-
-    This class is not derived from ``DeprecationWarning``, and as such is
-    visible by default.
-    """
diff --git a/libs/common/pkg_resources/_vendor/appdirs.py b/libs/common/pkg_resources/_vendor/appdirs.py
deleted file mode 100644
index ae67001a..00000000
--- a/libs/common/pkg_resources/_vendor/appdirs.py
+++ /dev/null
@@ -1,608 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-# Copyright (c) 2005-2010 ActiveState Software Inc.
-# Copyright (c) 2013 Eddy Petrișor
-
-"""Utilities for determining application-specific dirs.
-
-See  for details and usage.
-"""
-# Dev Notes:
-# - MSDN on where to store app data files:
-#   http://support.microsoft.com/default.aspx?scid=kb;en-us;310294#XSLTH3194121123120121120120
-# - Mac OS X: http://developer.apple.com/documentation/MacOSX/Conceptual/BPFileSystem/index.html
-# - XDG spec for Un*x: http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
-
-__version_info__ = (1, 4, 3)
-__version__ = '.'.join(map(str, __version_info__))
-
-
-import sys
-import os
-
-PY3 = sys.version_info[0] == 3
-
-if PY3:
-    unicode = str
-
-if sys.platform.startswith('java'):
-    import platform
-    os_name = platform.java_ver()[3][0]
-    if os_name.startswith('Windows'): # "Windows XP", "Windows 7", etc.
-        system = 'win32'
-    elif os_name.startswith('Mac'): # "Mac OS X", etc.
-        system = 'darwin'
-    else: # "Linux", "SunOS", "FreeBSD", etc.
-        # Setting this to "linux2" is not ideal, but only Windows or Mac
-        # are actually checked for and the rest of the module expects
-        # *sys.platform* style strings.
-        system = 'linux2'
-else:
-    system = sys.platform
-
-
-
-def user_data_dir(appname=None, appauthor=None, version=None, roaming=False):
-    r"""Return full path to the user-specific data dir for this application.
-
-        "appname" is the name of application.
-            If None, just the system directory is returned.
-        "appauthor" (only used on Windows) is the name of the
-            appauthor or distributing body for this application. Typically
-            it is the owning company name. This falls back to appname. You may
-            pass False to disable it.
-        "version" is an optional version path element to append to the
-            path. You might want to use this if you want multiple versions
-            of your app to be able to run independently. If used, this
-            would typically be ".".
-            Only applied when appname is present.
-        "roaming" (boolean, default False) can be set True to use the Windows
-            roaming appdata directory. That means that for users on a Windows
-            network setup for roaming profiles, this user data will be
-            sync'd on login. See
-            
-            for a discussion of issues.
-
-    Typical user data directories are:
-        Mac OS X:               ~/Library/Application Support/
-        Unix:                   ~/.local/share/    # or in $XDG_DATA_HOME, if defined
-        Win XP (not roaming):   C:\Documents and Settings\\Application Data\\
-        Win XP (roaming):       C:\Documents and Settings\\Local Settings\Application Data\\
-        Win 7  (not roaming):   C:\Users\\AppData\Local\\
-        Win 7  (roaming):       C:\Users\\AppData\Roaming\\
-
-    For Unix, we follow the XDG spec and support $XDG_DATA_HOME.
-    That means, by default "~/.local/share/".
-    """
-    if system == "win32":
-        if appauthor is None:
-            appauthor = appname
-        const = roaming and "CSIDL_APPDATA" or "CSIDL_LOCAL_APPDATA"
-        path = os.path.normpath(_get_win_folder(const))
-        if appname:
-            if appauthor is not False:
-                path = os.path.join(path, appauthor, appname)
-            else:
-                path = os.path.join(path, appname)
-    elif system == 'darwin':
-        path = os.path.expanduser('~/Library/Application Support/')
-        if appname:
-            path = os.path.join(path, appname)
-    else:
-        path = os.getenv('XDG_DATA_HOME', os.path.expanduser("~/.local/share"))
-        if appname:
-            path = os.path.join(path, appname)
-    if appname and version:
-        path = os.path.join(path, version)
-    return path
-
-
-def site_data_dir(appname=None, appauthor=None, version=None, multipath=False):
-    r"""Return full path to the user-shared data dir for this application.
-
-        "appname" is the name of application.
-            If None, just the system directory is returned.
-        "appauthor" (only used on Windows) is the name of the
-            appauthor or distributing body for this application. Typically
-            it is the owning company name. This falls back to appname. You may
-            pass False to disable it.
-        "version" is an optional version path element to append to the
-            path. You might want to use this if you want multiple versions
-            of your app to be able to run independently. If used, this
-            would typically be ".".
-            Only applied when appname is present.
-        "multipath" is an optional parameter only applicable to *nix
-            which indicates that the entire list of data dirs should be
-            returned. By default, the first item from XDG_DATA_DIRS is
-            returned, or '/usr/local/share/',
-            if XDG_DATA_DIRS is not set
-
-    Typical site data directories are:
-        Mac OS X:   /Library/Application Support/
-        Unix:       /usr/local/share/ or /usr/share/
-        Win XP:     C:\Documents and Settings\All Users\Application Data\\
-        Vista:      (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.)
-        Win 7:      C:\ProgramData\\   # Hidden, but writeable on Win 7.
-
-    For Unix, this is using the $XDG_DATA_DIRS[0] default.
-
-    WARNING: Do not use this on Windows. See the Vista-Fail note above for why.
-    """
-    if system == "win32":
-        if appauthor is None:
-            appauthor = appname
-        path = os.path.normpath(_get_win_folder("CSIDL_COMMON_APPDATA"))
-        if appname:
-            if appauthor is not False:
-                path = os.path.join(path, appauthor, appname)
-            else:
-                path = os.path.join(path, appname)
-    elif system == 'darwin':
-        path = os.path.expanduser('/Library/Application Support')
-        if appname:
-            path = os.path.join(path, appname)
-    else:
-        # XDG default for $XDG_DATA_DIRS
-        # only first, if multipath is False
-        path = os.getenv('XDG_DATA_DIRS',
-                         os.pathsep.join(['/usr/local/share', '/usr/share']))
-        pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)]
-        if appname:
-            if version:
-                appname = os.path.join(appname, version)
-            pathlist = [os.sep.join([x, appname]) for x in pathlist]
-
-        if multipath:
-            path = os.pathsep.join(pathlist)
-        else:
-            path = pathlist[0]
-        return path
-
-    if appname and version:
-        path = os.path.join(path, version)
-    return path
-
-
-def user_config_dir(appname=None, appauthor=None, version=None, roaming=False):
-    r"""Return full path to the user-specific config dir for this application.
-
-        "appname" is the name of application.
-            If None, just the system directory is returned.
-        "appauthor" (only used on Windows) is the name of the
-            appauthor or distributing body for this application. Typically
-            it is the owning company name. This falls back to appname. You may
-            pass False to disable it.
-        "version" is an optional version path element to append to the
-            path. You might want to use this if you want multiple versions
-            of your app to be able to run independently. If used, this
-            would typically be ".".
-            Only applied when appname is present.
-        "roaming" (boolean, default False) can be set True to use the Windows
-            roaming appdata directory. That means that for users on a Windows
-            network setup for roaming profiles, this user data will be
-            sync'd on login. See
-            
-            for a discussion of issues.
-
-    Typical user config directories are:
-        Mac OS X:               same as user_data_dir
-        Unix:                   ~/.config/     # or in $XDG_CONFIG_HOME, if defined
-        Win *:                  same as user_data_dir
-
-    For Unix, we follow the XDG spec and support $XDG_CONFIG_HOME.
-    That means, by default "~/.config/".
-    """
-    if system in ["win32", "darwin"]:
-        path = user_data_dir(appname, appauthor, None, roaming)
-    else:
-        path = os.getenv('XDG_CONFIG_HOME', os.path.expanduser("~/.config"))
-        if appname:
-            path = os.path.join(path, appname)
-    if appname and version:
-        path = os.path.join(path, version)
-    return path
-
-
-def site_config_dir(appname=None, appauthor=None, version=None, multipath=False):
-    r"""Return full path to the user-shared data dir for this application.
-
-        "appname" is the name of application.
-            If None, just the system directory is returned.
-        "appauthor" (only used on Windows) is the name of the
-            appauthor or distributing body for this application. Typically
-            it is the owning company name. This falls back to appname. You may
-            pass False to disable it.
-        "version" is an optional version path element to append to the
-            path. You might want to use this if you want multiple versions
-            of your app to be able to run independently. If used, this
-            would typically be ".".
-            Only applied when appname is present.
-        "multipath" is an optional parameter only applicable to *nix
-            which indicates that the entire list of config dirs should be
-            returned. By default, the first item from XDG_CONFIG_DIRS is
-            returned, or '/etc/xdg/', if XDG_CONFIG_DIRS is not set
-
-    Typical site config directories are:
-        Mac OS X:   same as site_data_dir
-        Unix:       /etc/xdg/ or $XDG_CONFIG_DIRS[i]/ for each value in
-                    $XDG_CONFIG_DIRS
-        Win *:      same as site_data_dir
-        Vista:      (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.)
-
-    For Unix, this is using the $XDG_CONFIG_DIRS[0] default, if multipath=False
-
-    WARNING: Do not use this on Windows. See the Vista-Fail note above for why.
-    """
-    if system in ["win32", "darwin"]:
-        path = site_data_dir(appname, appauthor)
-        if appname and version:
-            path = os.path.join(path, version)
-    else:
-        # XDG default for $XDG_CONFIG_DIRS
-        # only first, if multipath is False
-        path = os.getenv('XDG_CONFIG_DIRS', '/etc/xdg')
-        pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)]
-        if appname:
-            if version:
-                appname = os.path.join(appname, version)
-            pathlist = [os.sep.join([x, appname]) for x in pathlist]
-
-        if multipath:
-            path = os.pathsep.join(pathlist)
-        else:
-            path = pathlist[0]
-    return path
-
-
-def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True):
-    r"""Return full path to the user-specific cache dir for this application.
-
-        "appname" is the name of application.
-            If None, just the system directory is returned.
-        "appauthor" (only used on Windows) is the name of the
-            appauthor or distributing body for this application. Typically
-            it is the owning company name. This falls back to appname. You may
-            pass False to disable it.
-        "version" is an optional version path element to append to the
-            path. You might want to use this if you want multiple versions
-            of your app to be able to run independently. If used, this
-            would typically be ".".
-            Only applied when appname is present.
-        "opinion" (boolean) can be False to disable the appending of
-            "Cache" to the base app data dir for Windows. See
-            discussion below.
-
-    Typical user cache directories are:
-        Mac OS X:   ~/Library/Caches/
-        Unix:       ~/.cache/ (XDG default)
-        Win XP:     C:\Documents and Settings\\Local Settings\Application Data\\\Cache
-        Vista:      C:\Users\\AppData\Local\\\Cache
-
-    On Windows the only suggestion in the MSDN docs is that local settings go in
-    the `CSIDL_LOCAL_APPDATA` directory. This is identical to the non-roaming
-    app data dir (the default returned by `user_data_dir` above). Apps typically
-    put cache data somewhere *under* the given dir here. Some examples:
-        ...\Mozilla\Firefox\Profiles\\Cache
-        ...\Acme\SuperApp\Cache\1.0
-    OPINION: This function appends "Cache" to the `CSIDL_LOCAL_APPDATA` value.
-    This can be disabled with the `opinion=False` option.
-    """
-    if system == "win32":
-        if appauthor is None:
-            appauthor = appname
-        path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA"))
-        if appname:
-            if appauthor is not False:
-                path = os.path.join(path, appauthor, appname)
-            else:
-                path = os.path.join(path, appname)
-            if opinion:
-                path = os.path.join(path, "Cache")
-    elif system == 'darwin':
-        path = os.path.expanduser('~/Library/Caches')
-        if appname:
-            path = os.path.join(path, appname)
-    else:
-        path = os.getenv('XDG_CACHE_HOME', os.path.expanduser('~/.cache'))
-        if appname:
-            path = os.path.join(path, appname)
-    if appname and version:
-        path = os.path.join(path, version)
-    return path
-
-
-def user_state_dir(appname=None, appauthor=None, version=None, roaming=False):
-    r"""Return full path to the user-specific state dir for this application.
-
-        "appname" is the name of application.
-            If None, just the system directory is returned.
-        "appauthor" (only used on Windows) is the name of the
-            appauthor or distributing body for this application. Typically
-            it is the owning company name. This falls back to appname. You may
-            pass False to disable it.
-        "version" is an optional version path element to append to the
-            path. You might want to use this if you want multiple versions
-            of your app to be able to run independently. If used, this
-            would typically be ".".
-            Only applied when appname is present.
-        "roaming" (boolean, default False) can be set True to use the Windows
-            roaming appdata directory. That means that for users on a Windows
-            network setup for roaming profiles, this user data will be
-            sync'd on login. See
-            
-            for a discussion of issues.
-
-    Typical user state directories are:
-        Mac OS X:  same as user_data_dir
-        Unix:      ~/.local/state/   # or in $XDG_STATE_HOME, if defined
-        Win *:     same as user_data_dir
-
-    For Unix, we follow this Debian proposal 
-    to extend the XDG spec and support $XDG_STATE_HOME.
-
-    That means, by default "~/.local/state/".
-    """
-    if system in ["win32", "darwin"]:
-        path = user_data_dir(appname, appauthor, None, roaming)
-    else:
-        path = os.getenv('XDG_STATE_HOME', os.path.expanduser("~/.local/state"))
-        if appname:
-            path = os.path.join(path, appname)
-    if appname and version:
-        path = os.path.join(path, version)
-    return path
-
-
-def user_log_dir(appname=None, appauthor=None, version=None, opinion=True):
-    r"""Return full path to the user-specific log dir for this application.
-
-        "appname" is the name of application.
-            If None, just the system directory is returned.
-        "appauthor" (only used on Windows) is the name of the
-            appauthor or distributing body for this application. Typically
-            it is the owning company name. This falls back to appname. You may
-            pass False to disable it.
-        "version" is an optional version path element to append to the
-            path. You might want to use this if you want multiple versions
-            of your app to be able to run independently. If used, this
-            would typically be ".".
-            Only applied when appname is present.
-        "opinion" (boolean) can be False to disable the appending of
-            "Logs" to the base app data dir for Windows, and "log" to the
-            base cache dir for Unix. See discussion below.
-
-    Typical user log directories are:
-        Mac OS X:   ~/Library/Logs/
-        Unix:       ~/.cache//log  # or under $XDG_CACHE_HOME if defined
-        Win XP:     C:\Documents and Settings\\Local Settings\Application Data\\\Logs
-        Vista:      C:\Users\\AppData\Local\\\Logs
-
-    On Windows the only suggestion in the MSDN docs is that local settings
-    go in the `CSIDL_LOCAL_APPDATA` directory. (Note: I'm interested in
-    examples of what some windows apps use for a logs dir.)
-
-    OPINION: This function appends "Logs" to the `CSIDL_LOCAL_APPDATA`
-    value for Windows and appends "log" to the user cache dir for Unix.
-    This can be disabled with the `opinion=False` option.
-    """
-    if system == "darwin":
-        path = os.path.join(
-            os.path.expanduser('~/Library/Logs'),
-            appname)
-    elif system == "win32":
-        path = user_data_dir(appname, appauthor, version)
-        version = False
-        if opinion:
-            path = os.path.join(path, "Logs")
-    else:
-        path = user_cache_dir(appname, appauthor, version)
-        version = False
-        if opinion:
-            path = os.path.join(path, "log")
-    if appname and version:
-        path = os.path.join(path, version)
-    return path
-
-
-class AppDirs(object):
-    """Convenience wrapper for getting application dirs."""
-    def __init__(self, appname=None, appauthor=None, version=None,
-            roaming=False, multipath=False):
-        self.appname = appname
-        self.appauthor = appauthor
-        self.version = version
-        self.roaming = roaming
-        self.multipath = multipath
-
-    @property
-    def user_data_dir(self):
-        return user_data_dir(self.appname, self.appauthor,
-                             version=self.version, roaming=self.roaming)
-
-    @property
-    def site_data_dir(self):
-        return site_data_dir(self.appname, self.appauthor,
-                             version=self.version, multipath=self.multipath)
-
-    @property
-    def user_config_dir(self):
-        return user_config_dir(self.appname, self.appauthor,
-                               version=self.version, roaming=self.roaming)
-
-    @property
-    def site_config_dir(self):
-        return site_config_dir(self.appname, self.appauthor,
-                             version=self.version, multipath=self.multipath)
-
-    @property
-    def user_cache_dir(self):
-        return user_cache_dir(self.appname, self.appauthor,
-                              version=self.version)
-
-    @property
-    def user_state_dir(self):
-        return user_state_dir(self.appname, self.appauthor,
-                              version=self.version)
-
-    @property
-    def user_log_dir(self):
-        return user_log_dir(self.appname, self.appauthor,
-                            version=self.version)
-
-
-#---- internal support stuff
-
-def _get_win_folder_from_registry(csidl_name):
-    """This is a fallback technique at best. I'm not sure if using the
-    registry for this guarantees us the correct answer for all CSIDL_*
-    names.
-    """
-    if PY3:
-      import winreg as _winreg
-    else:
-      import _winreg
-
-    shell_folder_name = {
-        "CSIDL_APPDATA": "AppData",
-        "CSIDL_COMMON_APPDATA": "Common AppData",
-        "CSIDL_LOCAL_APPDATA": "Local AppData",
-    }[csidl_name]
-
-    key = _winreg.OpenKey(
-        _winreg.HKEY_CURRENT_USER,
-        r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
-    )
-    dir, type = _winreg.QueryValueEx(key, shell_folder_name)
-    return dir
-
-
-def _get_win_folder_with_pywin32(csidl_name):
-    from win32com.shell import shellcon, shell
-    dir = shell.SHGetFolderPath(0, getattr(shellcon, csidl_name), 0, 0)
-    # Try to make this a unicode path because SHGetFolderPath does
-    # not return unicode strings when there is unicode data in the
-    # path.
-    try:
-        dir = unicode(dir)
-
-        # Downgrade to short path name if have highbit chars. See
-        # .
-        has_high_char = False
-        for c in dir:
-            if ord(c) > 255:
-                has_high_char = True
-                break
-        if has_high_char:
-            try:
-                import win32api
-                dir = win32api.GetShortPathName(dir)
-            except ImportError:
-                pass
-    except UnicodeError:
-        pass
-    return dir
-
-
-def _get_win_folder_with_ctypes(csidl_name):
-    import ctypes
-
-    csidl_const = {
-        "CSIDL_APPDATA": 26,
-        "CSIDL_COMMON_APPDATA": 35,
-        "CSIDL_LOCAL_APPDATA": 28,
-    }[csidl_name]
-
-    buf = ctypes.create_unicode_buffer(1024)
-    ctypes.windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf)
-
-    # Downgrade to short path name if have highbit chars. See
-    # .
-    has_high_char = False
-    for c in buf:
-        if ord(c) > 255:
-            has_high_char = True
-            break
-    if has_high_char:
-        buf2 = ctypes.create_unicode_buffer(1024)
-        if ctypes.windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024):
-            buf = buf2
-
-    return buf.value
-
-def _get_win_folder_with_jna(csidl_name):
-    import array
-    from com.sun import jna
-    from com.sun.jna.platform import win32
-
-    buf_size = win32.WinDef.MAX_PATH * 2
-    buf = array.zeros('c', buf_size)
-    shell = win32.Shell32.INSTANCE
-    shell.SHGetFolderPath(None, getattr(win32.ShlObj, csidl_name), None, win32.ShlObj.SHGFP_TYPE_CURRENT, buf)
-    dir = jna.Native.toString(buf.tostring()).rstrip("\0")
-
-    # Downgrade to short path name if have highbit chars. See
-    # .
-    has_high_char = False
-    for c in dir:
-        if ord(c) > 255:
-            has_high_char = True
-            break
-    if has_high_char:
-        buf = array.zeros('c', buf_size)
-        kernel = win32.Kernel32.INSTANCE
-        if kernel.GetShortPathName(dir, buf, buf_size):
-            dir = jna.Native.toString(buf.tostring()).rstrip("\0")
-
-    return dir
-
-if system == "win32":
-    try:
-        import win32com.shell
-        _get_win_folder = _get_win_folder_with_pywin32
-    except ImportError:
-        try:
-            from ctypes import windll
-            _get_win_folder = _get_win_folder_with_ctypes
-        except ImportError:
-            try:
-                import com.sun.jna
-                _get_win_folder = _get_win_folder_with_jna
-            except ImportError:
-                _get_win_folder = _get_win_folder_from_registry
-
-
-#---- self test code
-
-if __name__ == "__main__":
-    appname = "MyApp"
-    appauthor = "MyCompany"
-
-    props = ("user_data_dir",
-             "user_config_dir",
-             "user_cache_dir",
-             "user_state_dir",
-             "user_log_dir",
-             "site_data_dir",
-             "site_config_dir")
-
-    print("-- app dirs %s --" % __version__)
-
-    print("-- app dirs (with optional 'version')")
-    dirs = AppDirs(appname, appauthor, version="1.0")
-    for prop in props:
-        print("%s: %s" % (prop, getattr(dirs, prop)))
-
-    print("\n-- app dirs (without optional 'version')")
-    dirs = AppDirs(appname, appauthor)
-    for prop in props:
-        print("%s: %s" % (prop, getattr(dirs, prop)))
-
-    print("\n-- app dirs (without optional 'appauthor')")
-    dirs = AppDirs(appname)
-    for prop in props:
-        print("%s: %s" % (prop, getattr(dirs, prop)))
-
-    print("\n-- app dirs (with disabled 'appauthor')")
-    dirs = AppDirs(appname, appauthor=False)
-    for prop in props:
-        print("%s: %s" % (prop, getattr(dirs, prop)))
diff --git a/libs/common/pkg_resources/_vendor/packaging/__init__.py b/libs/common/pkg_resources/_vendor/packaging/__init__.py
index 5ee62202..13cadc7f 100644
--- a/libs/common/pkg_resources/_vendor/packaging/__init__.py
+++ b/libs/common/pkg_resources/_vendor/packaging/__init__.py
@@ -1,14 +1,15 @@
 # This file is dual licensed under the terms of the Apache License, Version
 # 2.0, and the BSD License. See the LICENSE file in the root of this repository
 # for complete details.
-from __future__ import absolute_import, division, print_function
 
-from .__about__ import (
-    __author__, __copyright__, __email__, __license__, __summary__, __title__,
-    __uri__, __version__
-)
+__title__ = "packaging"
+__summary__ = "Core utilities for Python packages"
+__uri__ = "https://github.com/pypa/packaging"
 
-__all__ = [
-    "__title__", "__summary__", "__uri__", "__version__", "__author__",
-    "__email__", "__license__", "__copyright__",
-]
+__version__ = "23.1"
+
+__author__ = "Donald Stufft and individual contributors"
+__email__ = "donald@stufft.io"
+
+__license__ = "BSD-2-Clause or Apache-2.0"
+__copyright__ = "2014-2019 %s" % __author__
diff --git a/libs/common/pkg_resources/_vendor/packaging/_compat.py b/libs/common/pkg_resources/_vendor/packaging/_compat.py
deleted file mode 100644
index 210bb80b..00000000
--- a/libs/common/pkg_resources/_vendor/packaging/_compat.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# This file is dual licensed under the terms of the Apache License, Version
-# 2.0, and the BSD License. See the LICENSE file in the root of this repository
-# for complete details.
-from __future__ import absolute_import, division, print_function
-
-import sys
-
-
-PY2 = sys.version_info[0] == 2
-PY3 = sys.version_info[0] == 3
-
-# flake8: noqa
-
-if PY3:
-    string_types = str,
-else:
-    string_types = basestring,
-
-
-def with_metaclass(meta, *bases):
-    """
-    Create a base class with a metaclass.
-    """
-    # This requires a bit of explanation: the basic idea is to make a dummy
-    # metaclass for one level of class instantiation that replaces itself with
-    # the actual metaclass.
-    class metaclass(meta):
-        def __new__(cls, name, this_bases, d):
-            return meta(name, bases, d)
-    return type.__new__(metaclass, 'temporary_class', (), {})
diff --git a/libs/common/pkg_resources/_vendor/packaging/_structures.py b/libs/common/pkg_resources/_vendor/packaging/_structures.py
index ccc27861..90a6465f 100644
--- a/libs/common/pkg_resources/_vendor/packaging/_structures.py
+++ b/libs/common/pkg_resources/_vendor/packaging/_structures.py
@@ -1,68 +1,61 @@
 # This file is dual licensed under the terms of the Apache License, Version
 # 2.0, and the BSD License. See the LICENSE file in the root of this repository
 # for complete details.
-from __future__ import absolute_import, division, print_function
 
 
-class Infinity(object):
-
-    def __repr__(self):
+class InfinityType:
+    def __repr__(self) -> str:
         return "Infinity"
 
-    def __hash__(self):
+    def __hash__(self) -> int:
         return hash(repr(self))
 
-    def __lt__(self, other):
+    def __lt__(self, other: object) -> bool:
         return False
 
-    def __le__(self, other):
+    def __le__(self, other: object) -> bool:
         return False
 
-    def __eq__(self, other):
+    def __eq__(self, other: object) -> bool:
         return isinstance(other, self.__class__)
 
-    def __ne__(self, other):
-        return not isinstance(other, self.__class__)
-
-    def __gt__(self, other):
+    def __gt__(self, other: object) -> bool:
         return True
 
-    def __ge__(self, other):
+    def __ge__(self, other: object) -> bool:
         return True
 
-    def __neg__(self):
+    def __neg__(self: object) -> "NegativeInfinityType":
         return NegativeInfinity
 
-Infinity = Infinity()
+
+Infinity = InfinityType()
 
 
-class NegativeInfinity(object):
-
-    def __repr__(self):
+class NegativeInfinityType:
+    def __repr__(self) -> str:
         return "-Infinity"
 
-    def __hash__(self):
+    def __hash__(self) -> int:
         return hash(repr(self))
 
-    def __lt__(self, other):
+    def __lt__(self, other: object) -> bool:
         return True
 
-    def __le__(self, other):
+    def __le__(self, other: object) -> bool:
         return True
 
-    def __eq__(self, other):
+    def __eq__(self, other: object) -> bool:
         return isinstance(other, self.__class__)
 
-    def __ne__(self, other):
-        return not isinstance(other, self.__class__)
-
-    def __gt__(self, other):
+    def __gt__(self, other: object) -> bool:
         return False
 
-    def __ge__(self, other):
+    def __ge__(self, other: object) -> bool:
         return False
 
-    def __neg__(self):
+    def __neg__(self: object) -> InfinityType:
         return Infinity
 
-NegativeInfinity = NegativeInfinity()
+
+NegativeInfinity = NegativeInfinityType()
diff --git a/libs/common/pkg_resources/_vendor/packaging/markers.py b/libs/common/pkg_resources/_vendor/packaging/markers.py
index 892e578e..8b98fca7 100644
--- a/libs/common/pkg_resources/_vendor/packaging/markers.py
+++ b/libs/common/pkg_resources/_vendor/packaging/markers.py
@@ -1,26 +1,35 @@
 # This file is dual licensed under the terms of the Apache License, Version
 # 2.0, and the BSD License. See the LICENSE file in the root of this repository
 # for complete details.
-from __future__ import absolute_import, division, print_function
 
 import operator
 import os
 import platform
 import sys
+from typing import Any, Callable, Dict, List, Optional, Tuple, Union
 
-from pkg_resources.extern.pyparsing import ParseException, ParseResults, stringStart, stringEnd
-from pkg_resources.extern.pyparsing import ZeroOrMore, Group, Forward, QuotedString
-from pkg_resources.extern.pyparsing import Literal as L  # noqa
-
-from ._compat import string_types
-from .specifiers import Specifier, InvalidSpecifier
-
+from ._parser import (
+    MarkerAtom,
+    MarkerList,
+    Op,
+    Value,
+    Variable,
+    parse_marker as _parse_marker,
+)
+from ._tokenizer import ParserSyntaxError
+from .specifiers import InvalidSpecifier, Specifier
+from .utils import canonicalize_name
 
 __all__ = [
-    "InvalidMarker", "UndefinedComparison", "UndefinedEnvironmentName",
-    "Marker", "default_environment",
+    "InvalidMarker",
+    "UndefinedComparison",
+    "UndefinedEnvironmentName",
+    "Marker",
+    "default_environment",
 ]
 
+Operator = Callable[[str, str], bool]
+
 
 class InvalidMarker(ValueError):
     """
@@ -41,119 +50,37 @@ class UndefinedEnvironmentName(ValueError):
     """
 
 
-class Node(object):
-
-    def __init__(self, value):
-        self.value = value
-
-    def __str__(self):
-        return str(self.value)
-
-    def __repr__(self):
-        return "<{0}({1!r})>".format(self.__class__.__name__, str(self))
-
-    def serialize(self):
-        raise NotImplementedError
+def _normalize_extra_values(results: Any) -> Any:
+    """
+    Normalize extra values.
+    """
+    if isinstance(results[0], tuple):
+        lhs, op, rhs = results[0]
+        if isinstance(lhs, Variable) and lhs.value == "extra":
+            normalized_extra = canonicalize_name(rhs.value)
+            rhs = Value(normalized_extra)
+        elif isinstance(rhs, Variable) and rhs.value == "extra":
+            normalized_extra = canonicalize_name(lhs.value)
+            lhs = Value(normalized_extra)
+        results[0] = lhs, op, rhs
+    return results
 
 
-class Variable(Node):
+def _format_marker(
+    marker: Union[List[str], MarkerAtom, str], first: Optional[bool] = True
+) -> str:
 
-    def serialize(self):
-        return str(self)
-
-
-class Value(Node):
-
-    def serialize(self):
-        return '"{0}"'.format(self)
-
-
-class Op(Node):
-
-    def serialize(self):
-        return str(self)
-
-
-VARIABLE = (
-    L("implementation_version") |
-    L("platform_python_implementation") |
-    L("implementation_name") |
-    L("python_full_version") |
-    L("platform_release") |
-    L("platform_version") |
-    L("platform_machine") |
-    L("platform_system") |
-    L("python_version") |
-    L("sys_platform") |
-    L("os_name") |
-    L("os.name") |  # PEP-345
-    L("sys.platform") |  # PEP-345
-    L("platform.version") |  # PEP-345
-    L("platform.machine") |  # PEP-345
-    L("platform.python_implementation") |  # PEP-345
-    L("python_implementation") |  # undocumented setuptools legacy
-    L("extra")
-)
-ALIASES = {
-    'os.name': 'os_name',
-    'sys.platform': 'sys_platform',
-    'platform.version': 'platform_version',
-    'platform.machine': 'platform_machine',
-    'platform.python_implementation': 'platform_python_implementation',
-    'python_implementation': 'platform_python_implementation'
-}
-VARIABLE.setParseAction(lambda s, l, t: Variable(ALIASES.get(t[0], t[0])))
-
-VERSION_CMP = (
-    L("===") |
-    L("==") |
-    L(">=") |
-    L("<=") |
-    L("!=") |
-    L("~=") |
-    L(">") |
-    L("<")
-)
-
-MARKER_OP = VERSION_CMP | L("not in") | L("in")
-MARKER_OP.setParseAction(lambda s, l, t: Op(t[0]))
-
-MARKER_VALUE = QuotedString("'") | QuotedString('"')
-MARKER_VALUE.setParseAction(lambda s, l, t: Value(t[0]))
-
-BOOLOP = L("and") | L("or")
-
-MARKER_VAR = VARIABLE | MARKER_VALUE
-
-MARKER_ITEM = Group(MARKER_VAR + MARKER_OP + MARKER_VAR)
-MARKER_ITEM.setParseAction(lambda s, l, t: tuple(t[0]))
-
-LPAREN = L("(").suppress()
-RPAREN = L(")").suppress()
-
-MARKER_EXPR = Forward()
-MARKER_ATOM = MARKER_ITEM | Group(LPAREN + MARKER_EXPR + RPAREN)
-MARKER_EXPR << MARKER_ATOM + ZeroOrMore(BOOLOP + MARKER_EXPR)
-
-MARKER = stringStart + MARKER_EXPR + stringEnd
-
-
-def _coerce_parse_result(results):
-    if isinstance(results, ParseResults):
-        return [_coerce_parse_result(i) for i in results]
-    else:
-        return results
-
-
-def _format_marker(marker, first=True):
-    assert isinstance(marker, (list, tuple, string_types))
+    assert isinstance(marker, (list, tuple, str))
 
     # Sometimes we have a structure like [[...]] which is a single item list
     # where the single item is itself it's own list. In that case we want skip
     # the rest of this function so that we don't get extraneous () on the
     # outside.
-    if (isinstance(marker, list) and len(marker) == 1 and
-            isinstance(marker[0], (list, tuple))):
+    if (
+        isinstance(marker, list)
+        and len(marker) == 1
+        and isinstance(marker[0], (list, tuple))
+    ):
         return _format_marker(marker[0])
 
     if isinstance(marker, list):
@@ -168,7 +95,7 @@ def _format_marker(marker, first=True):
         return marker
 
 
-_operators = {
+_operators: Dict[str, Operator] = {
     "in": lambda lhs, rhs: lhs in rhs,
     "not in": lambda lhs, rhs: lhs not in rhs,
     "<": operator.lt,
@@ -180,42 +107,38 @@ _operators = {
 }
 
 
-def _eval_op(lhs, op, rhs):
+def _eval_op(lhs: str, op: Op, rhs: str) -> bool:
     try:
         spec = Specifier("".join([op.serialize(), rhs]))
     except InvalidSpecifier:
         pass
     else:
-        return spec.contains(lhs)
+        return spec.contains(lhs, prereleases=True)
 
-    oper = _operators.get(op.serialize())
+    oper: Optional[Operator] = _operators.get(op.serialize())
     if oper is None:
-        raise UndefinedComparison(
-            "Undefined {0!r} on {1!r} and {2!r}.".format(op, lhs, rhs)
-        )
+        raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.")
 
     return oper(lhs, rhs)
 
 
-_undefined = object()
+def _normalize(*values: str, key: str) -> Tuple[str, ...]:
+    # PEP 685 – Comparison of extra names for optional distribution dependencies
+    # https://peps.python.org/pep-0685/
+    # > When comparing extra names, tools MUST normalize the names being
+    # > compared using the semantics outlined in PEP 503 for names
+    if key == "extra":
+        return tuple(canonicalize_name(v) for v in values)
+
+    # other environment markers don't have such standards
+    return values
 
 
-def _get_env(environment, name):
-    value = environment.get(name, _undefined)
-
-    if value is _undefined:
-        raise UndefinedEnvironmentName(
-            "{0!r} does not exist in evaluation environment.".format(name)
-        )
-
-    return value
-
-
-def _evaluate_markers(markers, environment):
-    groups = [[]]
+def _evaluate_markers(markers: MarkerList, environment: Dict[str, str]) -> bool:
+    groups: List[List[bool]] = [[]]
 
     for marker in markers:
-        assert isinstance(marker, (list, tuple, string_types))
+        assert isinstance(marker, (list, tuple, str))
 
         if isinstance(marker, list):
             groups[-1].append(_evaluate_markers(marker, environment))
@@ -223,12 +146,15 @@ def _evaluate_markers(markers, environment):
             lhs, op, rhs = marker
 
             if isinstance(lhs, Variable):
-                lhs_value = _get_env(environment, lhs.value)
+                environment_key = lhs.value
+                lhs_value = environment[environment_key]
                 rhs_value = rhs.value
             else:
                 lhs_value = lhs.value
-                rhs_value = _get_env(environment, rhs.value)
+                environment_key = rhs.value
+                rhs_value = environment[environment_key]
 
+            lhs_value, rhs_value = _normalize(lhs_value, rhs_value, key=environment_key)
             groups[-1].append(_eval_op(lhs_value, op, rhs_value))
         else:
             assert marker in ["and", "or"]
@@ -238,22 +164,17 @@ def _evaluate_markers(markers, environment):
     return any(all(item) for item in groups)
 
 
-def format_full_version(info):
-    version = '{0.major}.{0.minor}.{0.micro}'.format(info)
+def format_full_version(info: "sys._version_info") -> str:
+    version = "{0.major}.{0.minor}.{0.micro}".format(info)
     kind = info.releaselevel
-    if kind != 'final':
+    if kind != "final":
         version += kind[0] + str(info.serial)
     return version
 
 
-def default_environment():
-    if hasattr(sys, 'implementation'):
-        iver = format_full_version(sys.implementation.version)
-        implementation_name = sys.implementation.name
-    else:
-        iver = '0'
-        implementation_name = ''
-
+def default_environment() -> Dict[str, str]:
+    iver = format_full_version(sys.implementation.version)
+    implementation_name = sys.implementation.name
     return {
         "implementation_name": implementation_name,
         "implementation_version": iver,
@@ -264,28 +185,53 @@ def default_environment():
         "platform_version": platform.version(),
         "python_full_version": platform.python_version(),
         "platform_python_implementation": platform.python_implementation(),
-        "python_version": platform.python_version()[:3],
+        "python_version": ".".join(platform.python_version_tuple()[:2]),
         "sys_platform": sys.platform,
     }
 
 
-class Marker(object):
-
-    def __init__(self, marker):
+class Marker:
+    def __init__(self, marker: str) -> None:
+        # Note: We create a Marker object without calling this constructor in
+        #       packaging.requirements.Requirement. If any additional logic is
+        #       added here, make sure to mirror/adapt Requirement.
         try:
-            self._markers = _coerce_parse_result(MARKER.parseString(marker))
-        except ParseException as e:
-            err_str = "Invalid marker: {0!r}, parse error at {1!r}".format(
-                marker, marker[e.loc:e.loc + 8])
-            raise InvalidMarker(err_str)
+            self._markers = _normalize_extra_values(_parse_marker(marker))
+            # The attribute `_markers` can be described in terms of a recursive type:
+            # MarkerList = List[Union[Tuple[Node, ...], str, MarkerList]]
+            #
+            # For example, the following expression:
+            # python_version > "3.6" or (python_version == "3.6" and os_name == "unix")
+            #
+            # is parsed into:
+            # [
+            #     (, ')>, ),
+            #     'and',
+            #     [
+            #         (, , ),
+            #         'or',
+            #         (, , )
+            #     ]
+            # ]
+        except ParserSyntaxError as e:
+            raise InvalidMarker(str(e)) from e
 
-    def __str__(self):
+    def __str__(self) -> str:
         return _format_marker(self._markers)
 
-    def __repr__(self):
-        return "".format(str(self))
+    def __repr__(self) -> str:
+        return f""
 
-    def evaluate(self, environment=None):
+    def __hash__(self) -> int:
+        return hash((self.__class__.__name__, str(self)))
+
+    def __eq__(self, other: Any) -> bool:
+        if not isinstance(other, Marker):
+            return NotImplemented
+
+        return str(self) == str(other)
+
+    def evaluate(self, environment: Optional[Dict[str, str]] = None) -> bool:
         """Evaluate a marker.
 
         Return the boolean from evaluating the given marker against the
@@ -295,7 +241,12 @@ class Marker(object):
         The environment is determined from the current Python process.
         """
         current_environment = default_environment()
+        current_environment["extra"] = ""
         if environment is not None:
             current_environment.update(environment)
+            # The API used to allow setting extra to None. We need to handle this
+            # case for backwards compatibility.
+            if current_environment["extra"] is None:
+                current_environment["extra"] = ""
 
         return _evaluate_markers(self._markers, current_environment)
diff --git a/libs/common/pkg_resources/_vendor/packaging/requirements.py b/libs/common/pkg_resources/_vendor/packaging/requirements.py
index 0c8c4a38..f34bfa85 100644
--- a/libs/common/pkg_resources/_vendor/packaging/requirements.py
+++ b/libs/common/pkg_resources/_vendor/packaging/requirements.py
@@ -1,18 +1,14 @@
 # This file is dual licensed under the terms of the Apache License, Version
 # 2.0, and the BSD License. See the LICENSE file in the root of this repository
 # for complete details.
-from __future__ import absolute_import, division, print_function
 
-import string
-import re
+import urllib.parse
+from typing import Any, List, Optional, Set
 
-from pkg_resources.extern.pyparsing import stringStart, stringEnd, originalTextFor, ParseException
-from pkg_resources.extern.pyparsing import ZeroOrMore, Word, Optional, Regex, Combine
-from pkg_resources.extern.pyparsing import Literal as L  # noqa
-from pkg_resources.extern.six.moves.urllib import parse as urlparse
-
-from .markers import MARKER_EXPR, Marker
-from .specifiers import LegacySpecifier, Specifier, SpecifierSet
+from ._parser import parse_requirement as _parse_requirement
+from ._tokenizer import ParserSyntaxError
+from .markers import Marker, _normalize_extra_values
+from .specifiers import SpecifierSet
 
 
 class InvalidRequirement(ValueError):
@@ -21,58 +17,7 @@ class InvalidRequirement(ValueError):
     """
 
 
-ALPHANUM = Word(string.ascii_letters + string.digits)
-
-LBRACKET = L("[").suppress()
-RBRACKET = L("]").suppress()
-LPAREN = L("(").suppress()
-RPAREN = L(")").suppress()
-COMMA = L(",").suppress()
-SEMICOLON = L(";").suppress()
-AT = L("@").suppress()
-
-PUNCTUATION = Word("-_.")
-IDENTIFIER_END = ALPHANUM | (ZeroOrMore(PUNCTUATION) + ALPHANUM)
-IDENTIFIER = Combine(ALPHANUM + ZeroOrMore(IDENTIFIER_END))
-
-NAME = IDENTIFIER("name")
-EXTRA = IDENTIFIER
-
-URI = Regex(r'[^ ]+')("url")
-URL = (AT + URI)
-
-EXTRAS_LIST = EXTRA + ZeroOrMore(COMMA + EXTRA)
-EXTRAS = (LBRACKET + Optional(EXTRAS_LIST) + RBRACKET)("extras")
-
-VERSION_PEP440 = Regex(Specifier._regex_str, re.VERBOSE | re.IGNORECASE)
-VERSION_LEGACY = Regex(LegacySpecifier._regex_str, re.VERBOSE | re.IGNORECASE)
-
-VERSION_ONE = VERSION_PEP440 ^ VERSION_LEGACY
-VERSION_MANY = Combine(VERSION_ONE + ZeroOrMore(COMMA + VERSION_ONE),
-                       joinString=",", adjacent=False)("_raw_spec")
-_VERSION_SPEC = Optional(((LPAREN + VERSION_MANY + RPAREN) | VERSION_MANY))
-_VERSION_SPEC.setParseAction(lambda s, l, t: t._raw_spec or '')
-
-VERSION_SPEC = originalTextFor(_VERSION_SPEC)("specifier")
-VERSION_SPEC.setParseAction(lambda s, l, t: t[1])
-
-MARKER_EXPR = originalTextFor(MARKER_EXPR())("marker")
-MARKER_EXPR.setParseAction(
-    lambda s, l, t: Marker(s[t._original_start:t._original_end])
-)
-MARKER_SEPERATOR = SEMICOLON
-MARKER = MARKER_SEPERATOR + MARKER_EXPR
-
-VERSION_AND_MARKER = VERSION_SPEC + Optional(MARKER)
-URL_AND_MARKER = URL + Optional(MARKER)
-
-NAMED_REQUIREMENT = \
-    NAME + Optional(EXTRAS) + (URL_AND_MARKER | VERSION_AND_MARKER)
-
-REQUIREMENT = stringStart + NAMED_REQUIREMENT + stringEnd
-
-
-class Requirement(object):
+class Requirement:
     """Parse a requirement.
 
     Parse a given requirement string into its parts, such as name, specifier,
@@ -85,43 +30,66 @@ class Requirement(object):
     #       the thing as well as the version? What about the markers?
     # TODO: Can we normalize the name and extra name?
 
-    def __init__(self, requirement_string):
+    def __init__(self, requirement_string: str) -> None:
         try:
-            req = REQUIREMENT.parseString(requirement_string)
-        except ParseException as e:
-            raise InvalidRequirement(
-                "Invalid requirement, parse error at \"{0!r}\"".format(
-                    requirement_string[e.loc:e.loc + 8]))
+            parsed = _parse_requirement(requirement_string)
+        except ParserSyntaxError as e:
+            raise InvalidRequirement(str(e)) from e
 
-        self.name = req.name
-        if req.url:
-            parsed_url = urlparse.urlparse(req.url)
-            if not (parsed_url.scheme and parsed_url.netloc) or (
-                    not parsed_url.scheme and not parsed_url.netloc):
-                raise InvalidRequirement("Invalid URL given")
-            self.url = req.url
+        self.name: str = parsed.name
+        if parsed.url:
+            parsed_url = urllib.parse.urlparse(parsed.url)
+            if parsed_url.scheme == "file":
+                if urllib.parse.urlunparse(parsed_url) != parsed.url:
+                    raise InvalidRequirement("Invalid URL given")
+            elif not (parsed_url.scheme and parsed_url.netloc) or (
+                not parsed_url.scheme and not parsed_url.netloc
+            ):
+                raise InvalidRequirement(f"Invalid URL: {parsed.url}")
+            self.url: Optional[str] = parsed.url
         else:
             self.url = None
-        self.extras = set(req.extras.asList() if req.extras else [])
-        self.specifier = SpecifierSet(req.specifier)
-        self.marker = req.marker if req.marker else None
+        self.extras: Set[str] = set(parsed.extras if parsed.extras else [])
+        self.specifier: SpecifierSet = SpecifierSet(parsed.specifier)
+        self.marker: Optional[Marker] = None
+        if parsed.marker is not None:
+            self.marker = Marker.__new__(Marker)
+            self.marker._markers = _normalize_extra_values(parsed.marker)
 
-    def __str__(self):
-        parts = [self.name]
+    def __str__(self) -> str:
+        parts: List[str] = [self.name]
 
         if self.extras:
-            parts.append("[{0}]".format(",".join(sorted(self.extras))))
+            formatted_extras = ",".join(sorted(self.extras))
+            parts.append(f"[{formatted_extras}]")
 
         if self.specifier:
             parts.append(str(self.specifier))
 
         if self.url:
-            parts.append("@ {0}".format(self.url))
+            parts.append(f"@ {self.url}")
+            if self.marker:
+                parts.append(" ")
 
         if self.marker:
-            parts.append("; {0}".format(self.marker))
+            parts.append(f"; {self.marker}")
 
         return "".join(parts)
 
-    def __repr__(self):
-        return "".format(str(self))
+    def __repr__(self) -> str:
+        return f""
+
+    def __hash__(self) -> int:
+        return hash((self.__class__.__name__, str(self)))
+
+    def __eq__(self, other: Any) -> bool:
+        if not isinstance(other, Requirement):
+            return NotImplemented
+
+        return (
+            self.name == other.name
+            and self.extras == other.extras
+            and self.specifier == other.specifier
+            and self.url == other.url
+            and self.marker == other.marker
+        )
diff --git a/libs/common/pkg_resources/_vendor/packaging/specifiers.py b/libs/common/pkg_resources/_vendor/packaging/specifiers.py
index 7f5a76cf..ba8fe37b 100644
--- a/libs/common/pkg_resources/_vendor/packaging/specifiers.py
+++ b/libs/common/pkg_resources/_vendor/packaging/specifiers.py
@@ -1,282 +1,123 @@
 # This file is dual licensed under the terms of the Apache License, Version
 # 2.0, and the BSD License. See the LICENSE file in the root of this repository
 # for complete details.
-from __future__ import absolute_import, division, print_function
+"""
+.. testsetup::
+
+    from packaging.specifiers import Specifier, SpecifierSet, InvalidSpecifier
+    from packaging.version import Version
+"""
 
 import abc
-import functools
 import itertools
 import re
+from typing import (
+    Callable,
+    Iterable,
+    Iterator,
+    List,
+    Optional,
+    Set,
+    Tuple,
+    TypeVar,
+    Union,
+)
 
-from ._compat import string_types, with_metaclass
-from .version import Version, LegacyVersion, parse
+from .utils import canonicalize_version
+from .version import Version
+
+UnparsedVersion = Union[Version, str]
+UnparsedVersionVar = TypeVar("UnparsedVersionVar", bound=UnparsedVersion)
+CallableOperator = Callable[[Version, str], bool]
+
+
+def _coerce_version(version: UnparsedVersion) -> Version:
+    if not isinstance(version, Version):
+        version = Version(version)
+    return version
 
 
 class InvalidSpecifier(ValueError):
     """
-    An invalid specifier was found, users should refer to PEP 440.
+    Raised when attempting to create a :class:`Specifier` with a specifier
+    string that is invalid.
+
+    >>> Specifier("lolwat")
+    Traceback (most recent call last):
+        ...
+    packaging.specifiers.InvalidSpecifier: Invalid specifier: 'lolwat'
     """
 
 
-class BaseSpecifier(with_metaclass(abc.ABCMeta, object)):
-
+class BaseSpecifier(metaclass=abc.ABCMeta):
     @abc.abstractmethod
-    def __str__(self):
+    def __str__(self) -> str:
         """
-        Returns the str representation of this Specifier like object. This
+        Returns the str representation of this Specifier-like object. This
         should be representative of the Specifier itself.
         """
 
     @abc.abstractmethod
-    def __hash__(self):
+    def __hash__(self) -> int:
         """
-        Returns a hash value for this Specifier like object.
+        Returns a hash value for this Specifier-like object.
         """
 
     @abc.abstractmethod
-    def __eq__(self, other):
+    def __eq__(self, other: object) -> bool:
         """
-        Returns a boolean representing whether or not the two Specifier like
+        Returns a boolean representing whether or not the two Specifier-like
         objects are equal.
+
+        :param other: The other object to check against.
         """
 
+    @property
     @abc.abstractmethod
-    def __ne__(self, other):
-        """
-        Returns a boolean representing whether or not the two Specifier like
-        objects are not equal.
-        """
+    def prereleases(self) -> Optional[bool]:
+        """Whether or not pre-releases as a whole are allowed.
 
-    @abc.abstractproperty
-    def prereleases(self):
-        """
-        Returns whether or not pre-releases as a whole are allowed by this
-        specifier.
+        This can be set to either ``True`` or ``False`` to explicitly enable or disable
+        prereleases or it can be set to ``None`` (the default) to use default semantics.
         """
 
     @prereleases.setter
-    def prereleases(self, value):
-        """
-        Sets whether or not pre-releases as a whole are allowed by this
-        specifier.
+    def prereleases(self, value: bool) -> None:
+        """Setter for :attr:`prereleases`.
+
+        :param value: The value to set.
         """
 
     @abc.abstractmethod
-    def contains(self, item, prereleases=None):
+    def contains(self, item: str, prereleases: Optional[bool] = None) -> bool:
         """
         Determines if the given item is contained within this specifier.
         """
 
     @abc.abstractmethod
-    def filter(self, iterable, prereleases=None):
+    def filter(
+        self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None
+    ) -> Iterator[UnparsedVersionVar]:
         """
         Takes an iterable of items and filters them so that only items which
         are contained within this specifier are allowed in it.
         """
 
 
-class _IndividualSpecifier(BaseSpecifier):
+class Specifier(BaseSpecifier):
+    """This class abstracts handling of version specifiers.
 
-    _operators = {}
+    .. tip::
 
-    def __init__(self, spec="", prereleases=None):
-        match = self._regex.search(spec)
-        if not match:
-            raise InvalidSpecifier("Invalid specifier: '{0}'".format(spec))
+        It is generally not required to instantiate this manually. You should instead
+        prefer to work with :class:`SpecifierSet` instead, which can parse
+        comma-separated version specifiers (which is what package metadata contains).
+    """
 
-        self._spec = (
-            match.group("operator").strip(),
-            match.group("version").strip(),
-        )
-
-        # Store whether or not this Specifier should accept prereleases
-        self._prereleases = prereleases
-
-    def __repr__(self):
-        pre = (
-            ", prereleases={0!r}".format(self.prereleases)
-            if self._prereleases is not None
-            else ""
-        )
-
-        return "<{0}({1!r}{2})>".format(
-            self.__class__.__name__,
-            str(self),
-            pre,
-        )
-
-    def __str__(self):
-        return "{0}{1}".format(*self._spec)
-
-    def __hash__(self):
-        return hash(self._spec)
-
-    def __eq__(self, other):
-        if isinstance(other, string_types):
-            try:
-                other = self.__class__(other)
-            except InvalidSpecifier:
-                return NotImplemented
-        elif not isinstance(other, self.__class__):
-            return NotImplemented
-
-        return self._spec == other._spec
-
-    def __ne__(self, other):
-        if isinstance(other, string_types):
-            try:
-                other = self.__class__(other)
-            except InvalidSpecifier:
-                return NotImplemented
-        elif not isinstance(other, self.__class__):
-            return NotImplemented
-
-        return self._spec != other._spec
-
-    def _get_operator(self, op):
-        return getattr(self, "_compare_{0}".format(self._operators[op]))
-
-    def _coerce_version(self, version):
-        if not isinstance(version, (LegacyVersion, Version)):
-            version = parse(version)
-        return version
-
-    @property
-    def operator(self):
-        return self._spec[0]
-
-    @property
-    def version(self):
-        return self._spec[1]
-
-    @property
-    def prereleases(self):
-        return self._prereleases
-
-    @prereleases.setter
-    def prereleases(self, value):
-        self._prereleases = value
-
-    def __contains__(self, item):
-        return self.contains(item)
-
-    def contains(self, item, prereleases=None):
-        # Determine if prereleases are to be allowed or not.
-        if prereleases is None:
-            prereleases = self.prereleases
-
-        # Normalize item to a Version or LegacyVersion, this allows us to have
-        # a shortcut for ``"2.0" in Specifier(">=2")
-        item = self._coerce_version(item)
-
-        # Determine if we should be supporting prereleases in this specifier
-        # or not, if we do not support prereleases than we can short circuit
-        # logic if this version is a prereleases.
-        if item.is_prerelease and not prereleases:
-            return False
-
-        # Actually do the comparison to determine if this item is contained
-        # within this Specifier or not.
-        return self._get_operator(self.operator)(item, self.version)
-
-    def filter(self, iterable, prereleases=None):
-        yielded = False
-        found_prereleases = []
-
-        kw = {"prereleases": prereleases if prereleases is not None else True}
-
-        # Attempt to iterate over all the values in the iterable and if any of
-        # them match, yield them.
-        for version in iterable:
-            parsed_version = self._coerce_version(version)
-
-            if self.contains(parsed_version, **kw):
-                # If our version is a prerelease, and we were not set to allow
-                # prereleases, then we'll store it for later incase nothing
-                # else matches this specifier.
-                if (parsed_version.is_prerelease and not
-                        (prereleases or self.prereleases)):
-                    found_prereleases.append(version)
-                # Either this is not a prerelease, or we should have been
-                # accepting prereleases from the begining.
-                else:
-                    yielded = True
-                    yield version
-
-        # Now that we've iterated over everything, determine if we've yielded
-        # any values, and if we have not and we have any prereleases stored up
-        # then we will go ahead and yield the prereleases.
-        if not yielded and found_prereleases:
-            for version in found_prereleases:
-                yield version
-
-
-class LegacySpecifier(_IndividualSpecifier):
-
-    _regex_str = (
-        r"""
-        (?P(==|!=|<=|>=|<|>))
-        \s*
-        (?P
-            [^,;\s)]* # Since this is a "legacy" specifier, and the version
-                      # string can be just about anything, we match everything
-                      # except for whitespace, a semi-colon for marker support,
-                      # a closing paren since versions can be enclosed in
-                      # them, and a comma since it's a version separator.
-        )
-        """
-    )
-
-    _regex = re.compile(
-        r"^\s*" + _regex_str + r"\s*$", re.VERBOSE | re.IGNORECASE)
-
-    _operators = {
-        "==": "equal",
-        "!=": "not_equal",
-        "<=": "less_than_equal",
-        ">=": "greater_than_equal",
-        "<": "less_than",
-        ">": "greater_than",
-    }
-
-    def _coerce_version(self, version):
-        if not isinstance(version, LegacyVersion):
-            version = LegacyVersion(str(version))
-        return version
-
-    def _compare_equal(self, prospective, spec):
-        return prospective == self._coerce_version(spec)
-
-    def _compare_not_equal(self, prospective, spec):
-        return prospective != self._coerce_version(spec)
-
-    def _compare_less_than_equal(self, prospective, spec):
-        return prospective <= self._coerce_version(spec)
-
-    def _compare_greater_than_equal(self, prospective, spec):
-        return prospective >= self._coerce_version(spec)
-
-    def _compare_less_than(self, prospective, spec):
-        return prospective < self._coerce_version(spec)
-
-    def _compare_greater_than(self, prospective, spec):
-        return prospective > self._coerce_version(spec)
-
-
-def _require_version_compare(fn):
-    @functools.wraps(fn)
-    def wrapped(self, prospective, spec):
-        if not isinstance(prospective, Version):
-            return False
-        return fn(self, prospective, spec)
-    return wrapped
-
-
-class Specifier(_IndividualSpecifier):
-
-    _regex_str = (
-        r"""
+    _operator_regex_str = r"""
         (?P(~=|==|!=|<=|>=|<|>|===))
+        """
+    _version_regex_str = r"""
         (?P
             (?:
                 # The identity operators allow for an escape hatch that will
@@ -286,8 +127,10 @@ class Specifier(_IndividualSpecifier):
                 # but included entirely as an escape hatch.
                 (?<====)  # Only match for the identity operator
                 \s*
-                [^\s]*    # We just match everything, except for whitespace
-                          # since we are only testing for strict identity.
+                [^\s;)]*  # The arbitrary version can be just about anything,
+                          # we match everything except for whitespace, a
+                          # semi-colon for marker support, and a closing paren
+                          # since versions can be enclosed in them.
             )
             |
             (?:
@@ -300,23 +143,23 @@ class Specifier(_IndividualSpecifier):
                 v?
                 (?:[0-9]+!)?          # epoch
                 [0-9]+(?:\.[0-9]+)*   # release
-                (?:                   # pre release
-                    [-_\.]?
-                    (a|b|c|rc|alpha|beta|pre|preview)
-                    [-_\.]?
-                    [0-9]*
-                )?
-                (?:                   # post release
-                    (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
-                )?
 
-                # You cannot use a wild card and a dev or local version
-                # together so group them with a | and make them optional.
+                # You cannot use a wild card and a pre-release, post-release, a dev or
+                # local version together so group them with a | and make them optional.
                 (?:
+                    \.\*  # Wild card syntax of .*
+                    |
+                    (?:                                  # pre release
+                        [-_\.]?
+                        (alpha|beta|preview|pre|a|b|c|rc)
+                        [-_\.]?
+                        [0-9]*
+                    )?
+                    (?:                                  # post release
+                        (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*)
+                    )?
                     (?:[-_\.]?dev[-_\.]?[0-9]*)?         # dev release
                     (?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)? # local
-                    |
-                    \.\*  # Wild card syntax of .*
                 )?
             )
             |
@@ -331,7 +174,7 @@ class Specifier(_IndividualSpecifier):
                 [0-9]+(?:\.[0-9]+)+   # release  (We have a + instead of a *)
                 (?:                   # pre release
                     [-_\.]?
-                    (a|b|c|rc|alpha|beta|pre|preview)
+                    (alpha|beta|preview|pre|a|b|c|rc)
                     [-_\.]?
                     [0-9]*
                 )?
@@ -356,7 +199,7 @@ class Specifier(_IndividualSpecifier):
                 [0-9]+(?:\.[0-9]+)*   # release
                 (?:                   # pre release
                     [-_\.]?
-                    (a|b|c|rc|alpha|beta|pre|preview)
+                    (alpha|beta|preview|pre|a|b|c|rc)
                     [-_\.]?
                     [0-9]*
                 )?
@@ -367,10 +210,11 @@ class Specifier(_IndividualSpecifier):
             )
         )
         """
-    )
 
     _regex = re.compile(
-        r"^\s*" + _regex_str + r"\s*$", re.VERBOSE | re.IGNORECASE)
+        r"^\s*" + _operator_regex_str + _version_regex_str + r"\s*$",
+        re.VERBOSE | re.IGNORECASE,
+    )
 
     _operators = {
         "~=": "compatible",
@@ -383,8 +227,154 @@ class Specifier(_IndividualSpecifier):
         "===": "arbitrary",
     }
 
-    @_require_version_compare
-    def _compare_compatible(self, prospective, spec):
+    def __init__(self, spec: str = "", prereleases: Optional[bool] = None) -> None:
+        """Initialize a Specifier instance.
+
+        :param spec:
+            The string representation of a specifier which will be parsed and
+            normalized before use.
+        :param prereleases:
+            This tells the specifier if it should accept prerelease versions if
+            applicable or not. The default of ``None`` will autodetect it from the
+            given specifiers.
+        :raises InvalidSpecifier:
+            If the given specifier is invalid (i.e. bad syntax).
+        """
+        match = self._regex.search(spec)
+        if not match:
+            raise InvalidSpecifier(f"Invalid specifier: '{spec}'")
+
+        self._spec: Tuple[str, str] = (
+            match.group("operator").strip(),
+            match.group("version").strip(),
+        )
+
+        # Store whether or not this Specifier should accept prereleases
+        self._prereleases = prereleases
+
+    # https://github.com/python/mypy/pull/13475#pullrequestreview-1079784515
+    @property  # type: ignore[override]
+    def prereleases(self) -> bool:
+        # If there is an explicit prereleases set for this, then we'll just
+        # blindly use that.
+        if self._prereleases is not None:
+            return self._prereleases
+
+        # Look at all of our specifiers and determine if they are inclusive
+        # operators, and if they are if they are including an explicit
+        # prerelease.
+        operator, version = self._spec
+        if operator in ["==", ">=", "<=", "~=", "==="]:
+            # The == specifier can include a trailing .*, if it does we
+            # want to remove before parsing.
+            if operator == "==" and version.endswith(".*"):
+                version = version[:-2]
+
+            # Parse the version, and if it is a pre-release than this
+            # specifier allows pre-releases.
+            if Version(version).is_prerelease:
+                return True
+
+        return False
+
+    @prereleases.setter
+    def prereleases(self, value: bool) -> None:
+        self._prereleases = value
+
+    @property
+    def operator(self) -> str:
+        """The operator of this specifier.
+
+        >>> Specifier("==1.2.3").operator
+        '=='
+        """
+        return self._spec[0]
+
+    @property
+    def version(self) -> str:
+        """The version of this specifier.
+
+        >>> Specifier("==1.2.3").version
+        '1.2.3'
+        """
+        return self._spec[1]
+
+    def __repr__(self) -> str:
+        """A representation of the Specifier that shows all internal state.
+
+        >>> Specifier('>=1.0.0')
+        =1.0.0')>
+        >>> Specifier('>=1.0.0', prereleases=False)
+        =1.0.0', prereleases=False)>
+        >>> Specifier('>=1.0.0', prereleases=True)
+        =1.0.0', prereleases=True)>
+        """
+        pre = (
+            f", prereleases={self.prereleases!r}"
+            if self._prereleases is not None
+            else ""
+        )
+
+        return f"<{self.__class__.__name__}({str(self)!r}{pre})>"
+
+    def __str__(self) -> str:
+        """A string representation of the Specifier that can be round-tripped.
+
+        >>> str(Specifier('>=1.0.0'))
+        '>=1.0.0'
+        >>> str(Specifier('>=1.0.0', prereleases=False))
+        '>=1.0.0'
+        """
+        return "{}{}".format(*self._spec)
+
+    @property
+    def _canonical_spec(self) -> Tuple[str, str]:
+        canonical_version = canonicalize_version(
+            self._spec[1],
+            strip_trailing_zero=(self._spec[0] != "~="),
+        )
+        return self._spec[0], canonical_version
+
+    def __hash__(self) -> int:
+        return hash(self._canonical_spec)
+
+    def __eq__(self, other: object) -> bool:
+        """Whether or not the two Specifier-like objects are equal.
+
+        :param other: The other object to check against.
+
+        The value of :attr:`prereleases` is ignored.
+
+        >>> Specifier("==1.2.3") == Specifier("== 1.2.3.0")
+        True
+        >>> (Specifier("==1.2.3", prereleases=False) ==
+        ...  Specifier("==1.2.3", prereleases=True))
+        True
+        >>> Specifier("==1.2.3") == "==1.2.3"
+        True
+        >>> Specifier("==1.2.3") == Specifier("==1.2.4")
+        False
+        >>> Specifier("==1.2.3") == Specifier("~=1.2.3")
+        False
+        """
+        if isinstance(other, str):
+            try:
+                other = self.__class__(str(other))
+            except InvalidSpecifier:
+                return NotImplemented
+        elif not isinstance(other, self.__class__):
+            return NotImplemented
+
+        return self._canonical_spec == other._canonical_spec
+
+    def _get_operator(self, op: str) -> CallableOperator:
+        operator_callable: CallableOperator = getattr(
+            self, f"_compare_{self._operators[op]}"
+        )
+        return operator_callable
+
+    def _compare_compatible(self, prospective: Version, spec: str) -> bool:
+
         # Compatible releases have an equivalent combination of >= and ==. That
         # is that ~=2.2 is equivalent to >=2.2,==2.*. This allows us to
         # implement this in terms of the other specifiers instead of
@@ -392,76 +382,81 @@ class Specifier(_IndividualSpecifier):
         # the other specifiers.
 
         # We want everything but the last item in the version, but we want to
-        # ignore post and dev releases and we want to treat the pre-release as
-        # it's own separate segment.
+        # ignore suffix segments.
         prefix = ".".join(
-            list(
-                itertools.takewhile(
-                    lambda x: (not x.startswith("post") and not
-                               x.startswith("dev")),
-                    _version_split(spec),
-                )
-            )[:-1]
+            list(itertools.takewhile(_is_not_suffix, _version_split(spec)))[:-1]
         )
 
         # Add the prefix notation to the end of our string
         prefix += ".*"
 
-        return (self._get_operator(">=")(prospective, spec) and
-                self._get_operator("==")(prospective, prefix))
+        return self._get_operator(">=")(prospective, spec) and self._get_operator("==")(
+            prospective, prefix
+        )
+
+    def _compare_equal(self, prospective: Version, spec: str) -> bool:
 
-    @_require_version_compare
-    def _compare_equal(self, prospective, spec):
         # We need special logic to handle prefix matching
         if spec.endswith(".*"):
             # In the case of prefix matching we want to ignore local segment.
-            prospective = Version(prospective.public)
+            normalized_prospective = canonicalize_version(
+                prospective.public, strip_trailing_zero=False
+            )
+            # Get the normalized version string ignoring the trailing .*
+            normalized_spec = canonicalize_version(spec[:-2], strip_trailing_zero=False)
             # Split the spec out by dots, and pretend that there is an implicit
             # dot in between a release segment and a pre-release segment.
-            spec = _version_split(spec[:-2])  # Remove the trailing .*
+            split_spec = _version_split(normalized_spec)
 
             # Split the prospective version out by dots, and pretend that there
             # is an implicit dot in between a release segment and a pre-release
             # segment.
-            prospective = _version_split(str(prospective))
+            split_prospective = _version_split(normalized_prospective)
+
+            # 0-pad the prospective version before shortening it to get the correct
+            # shortened version.
+            padded_prospective, _ = _pad_version(split_prospective, split_spec)
 
             # Shorten the prospective version to be the same length as the spec
             # so that we can determine if the specifier is a prefix of the
             # prospective version or not.
-            prospective = prospective[:len(spec)]
+            shortened_prospective = padded_prospective[: len(split_spec)]
 
-            # Pad out our two sides with zeros so that they both equal the same
-            # length.
-            spec, prospective = _pad_version(spec, prospective)
+            return shortened_prospective == split_spec
         else:
             # Convert our spec string into a Version
-            spec = Version(spec)
+            spec_version = Version(spec)
 
             # If the specifier does not have a local segment, then we want to
             # act as if the prospective version also does not have a local
             # segment.
-            if not spec.local:
+            if not spec_version.local:
                 prospective = Version(prospective.public)
 
-        return prospective == spec
+            return prospective == spec_version
 
-    @_require_version_compare
-    def _compare_not_equal(self, prospective, spec):
+    def _compare_not_equal(self, prospective: Version, spec: str) -> bool:
         return not self._compare_equal(prospective, spec)
 
-    @_require_version_compare
-    def _compare_less_than_equal(self, prospective, spec):
-        return prospective <= Version(spec)
+    def _compare_less_than_equal(self, prospective: Version, spec: str) -> bool:
 
-    @_require_version_compare
-    def _compare_greater_than_equal(self, prospective, spec):
-        return prospective >= Version(spec)
+        # NB: Local version identifiers are NOT permitted in the version
+        # specifier, so local version labels can be universally removed from
+        # the prospective version.
+        return Version(prospective.public) <= Version(spec)
+
+    def _compare_greater_than_equal(self, prospective: Version, spec: str) -> bool:
+
+        # NB: Local version identifiers are NOT permitted in the version
+        # specifier, so local version labels can be universally removed from
+        # the prospective version.
+        return Version(prospective.public) >= Version(spec)
+
+    def _compare_less_than(self, prospective: Version, spec_str: str) -> bool:
 
-    @_require_version_compare
-    def _compare_less_than(self, prospective, spec):
         # Convert our spec to a Version instance, since we'll want to work with
         # it as a version.
-        spec = Version(spec)
+        spec = Version(spec_str)
 
         # Check to see if the prospective version is less than the spec
         # version. If it's not we can short circuit and just return False now
@@ -482,11 +477,11 @@ class Specifier(_IndividualSpecifier):
         # version in the spec.
         return True
 
-    @_require_version_compare
-    def _compare_greater_than(self, prospective, spec):
+    def _compare_greater_than(self, prospective: Version, spec_str: str) -> bool:
+
         # Convert our spec to a Version instance, since we'll want to work with
         # it as a version.
-        spec = Version(spec)
+        spec = Version(spec_str)
 
         # Check to see if the prospective version is greater than the spec
         # version. If it's not we can short circuit and just return False now
@@ -503,7 +498,7 @@ class Specifier(_IndividualSpecifier):
                 return False
 
         # Ensure that we do not allow a local version of the version mentioned
-        # in the specifier, which is techincally greater than, to match.
+        # in the specifier, which is technically greater than, to match.
         if prospective.local is not None:
             if Version(prospective.base_version) == Version(spec.base_version):
                 return False
@@ -513,43 +508,143 @@ class Specifier(_IndividualSpecifier):
         # same version in the spec.
         return True
 
-    def _compare_arbitrary(self, prospective, spec):
+    def _compare_arbitrary(self, prospective: Version, spec: str) -> bool:
         return str(prospective).lower() == str(spec).lower()
 
-    @property
-    def prereleases(self):
-        # If there is an explicit prereleases set for this, then we'll just
-        # blindly use that.
-        if self._prereleases is not None:
-            return self._prereleases
+    def __contains__(self, item: Union[str, Version]) -> bool:
+        """Return whether or not the item is contained in this specifier.
 
-        # Look at all of our specifiers and determine if they are inclusive
-        # operators, and if they are if they are including an explicit
-        # prerelease.
-        operator, version = self._spec
-        if operator in ["==", ">=", "<=", "~=", "==="]:
-            # The == specifier can include a trailing .*, if it does we
-            # want to remove before parsing.
-            if operator == "==" and version.endswith(".*"):
-                version = version[:-2]
+        :param item: The item to check for.
 
-            # Parse the version, and if it is a pre-release than this
-            # specifier allows pre-releases.
-            if parse(version).is_prerelease:
-                return True
+        This is used for the ``in`` operator and behaves the same as
+        :meth:`contains` with no ``prereleases`` argument passed.
 
-        return False
+        >>> "1.2.3" in Specifier(">=1.2.3")
+        True
+        >>> Version("1.2.3") in Specifier(">=1.2.3")
+        True
+        >>> "1.0.0" in Specifier(">=1.2.3")
+        False
+        >>> "1.3.0a1" in Specifier(">=1.2.3")
+        False
+        >>> "1.3.0a1" in Specifier(">=1.2.3", prereleases=True)
+        True
+        """
+        return self.contains(item)
 
-    @prereleases.setter
-    def prereleases(self, value):
-        self._prereleases = value
+    def contains(
+        self, item: UnparsedVersion, prereleases: Optional[bool] = None
+    ) -> bool:
+        """Return whether or not the item is contained in this specifier.
+
+        :param item:
+            The item to check for, which can be a version string or a
+            :class:`Version` instance.
+        :param prereleases:
+            Whether or not to match prereleases with this Specifier. If set to
+            ``None`` (the default), it uses :attr:`prereleases` to determine
+            whether or not prereleases are allowed.
+
+        >>> Specifier(">=1.2.3").contains("1.2.3")
+        True
+        >>> Specifier(">=1.2.3").contains(Version("1.2.3"))
+        True
+        >>> Specifier(">=1.2.3").contains("1.0.0")
+        False
+        >>> Specifier(">=1.2.3").contains("1.3.0a1")
+        False
+        >>> Specifier(">=1.2.3", prereleases=True).contains("1.3.0a1")
+        True
+        >>> Specifier(">=1.2.3").contains("1.3.0a1", prereleases=True)
+        True
+        """
+
+        # Determine if prereleases are to be allowed or not.
+        if prereleases is None:
+            prereleases = self.prereleases
+
+        # Normalize item to a Version, this allows us to have a shortcut for
+        # "2.0" in Specifier(">=2")
+        normalized_item = _coerce_version(item)
+
+        # Determine if we should be supporting prereleases in this specifier
+        # or not, if we do not support prereleases than we can short circuit
+        # logic if this version is a prereleases.
+        if normalized_item.is_prerelease and not prereleases:
+            return False
+
+        # Actually do the comparison to determine if this item is contained
+        # within this Specifier or not.
+        operator_callable: CallableOperator = self._get_operator(self.operator)
+        return operator_callable(normalized_item, self.version)
+
+    def filter(
+        self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None
+    ) -> Iterator[UnparsedVersionVar]:
+        """Filter items in the given iterable, that match the specifier.
+
+        :param iterable:
+            An iterable that can contain version strings and :class:`Version` instances.
+            The items in the iterable will be filtered according to the specifier.
+        :param prereleases:
+            Whether or not to allow prereleases in the returned iterator. If set to
+            ``None`` (the default), it will be intelligently decide whether to allow
+            prereleases or not (based on the :attr:`prereleases` attribute, and
+            whether the only versions matching are prereleases).
+
+        This method is smarter than just ``filter(Specifier().contains, [...])``
+        because it implements the rule from :pep:`440` that a prerelease item
+        SHOULD be accepted if no other versions match the given specifier.
+
+        >>> list(Specifier(">=1.2.3").filter(["1.2", "1.3", "1.5a1"]))
+        ['1.3']
+        >>> list(Specifier(">=1.2.3").filter(["1.2", "1.2.3", "1.3", Version("1.4")]))
+        ['1.2.3', '1.3', ]
+        >>> list(Specifier(">=1.2.3").filter(["1.2", "1.5a1"]))
+        ['1.5a1']
+        >>> list(Specifier(">=1.2.3").filter(["1.3", "1.5a1"], prereleases=True))
+        ['1.3', '1.5a1']
+        >>> list(Specifier(">=1.2.3", prereleases=True).filter(["1.3", "1.5a1"]))
+        ['1.3', '1.5a1']
+        """
+
+        yielded = False
+        found_prereleases = []
+
+        kw = {"prereleases": prereleases if prereleases is not None else True}
+
+        # Attempt to iterate over all the values in the iterable and if any of
+        # them match, yield them.
+        for version in iterable:
+            parsed_version = _coerce_version(version)
+
+            if self.contains(parsed_version, **kw):
+                # If our version is a prerelease, and we were not set to allow
+                # prereleases, then we'll store it for later in case nothing
+                # else matches this specifier.
+                if parsed_version.is_prerelease and not (
+                    prereleases or self.prereleases
+                ):
+                    found_prereleases.append(version)
+                # Either this is not a prerelease, or we should have been
+                # accepting prereleases from the beginning.
+                else:
+                    yielded = True
+                    yield version
+
+        # Now that we've iterated over everything, determine if we've yielded
+        # any values, and if we have not and we have any prereleases stored up
+        # then we will go ahead and yield the prereleases.
+        if not yielded and found_prereleases:
+            for version in found_prereleases:
+                yield version
 
 
 _prefix_regex = re.compile(r"^([0-9]+)((?:a|b|c|rc)[0-9]+)$")
 
 
-def _version_split(version):
-    result = []
+def _version_split(version: str) -> List[str]:
+    result: List[str] = []
     for item in version.split("."):
         match = _prefix_regex.search(item)
         if match:
@@ -559,7 +654,13 @@ def _version_split(version):
     return result
 
 
-def _pad_version(left, right):
+def _is_not_suffix(segment: str) -> bool:
+    return not any(
+        segment.startswith(prefix) for prefix in ("dev", "a", "b", "rc", "post")
+    )
+
+
+def _pad_version(left: List[str], right: List[str]) -> Tuple[List[str], List[str]]:
     left_split, right_split = [], []
 
     # Get the release segment of our versions
@@ -567,40 +668,50 @@ def _pad_version(left, right):
     right_split.append(list(itertools.takewhile(lambda x: x.isdigit(), right)))
 
     # Get the rest of our versions
-    left_split.append(left[len(left_split[0]):])
-    right_split.append(right[len(right_split[0]):])
+    left_split.append(left[len(left_split[0]) :])
+    right_split.append(right[len(right_split[0]) :])
 
     # Insert our padding
-    left_split.insert(
-        1,
-        ["0"] * max(0, len(right_split[0]) - len(left_split[0])),
-    )
-    right_split.insert(
-        1,
-        ["0"] * max(0, len(left_split[0]) - len(right_split[0])),
-    )
+    left_split.insert(1, ["0"] * max(0, len(right_split[0]) - len(left_split[0])))
+    right_split.insert(1, ["0"] * max(0, len(left_split[0]) - len(right_split[0])))
 
-    return (
-        list(itertools.chain(*left_split)),
-        list(itertools.chain(*right_split)),
-    )
+    return (list(itertools.chain(*left_split)), list(itertools.chain(*right_split)))
 
 
 class SpecifierSet(BaseSpecifier):
+    """This class abstracts handling of a set of version specifiers.
 
-    def __init__(self, specifiers="", prereleases=None):
-        # Split on , to break each indidivual specifier into it's own item, and
+    It can be passed a single specifier (``>=3.0``), a comma-separated list of
+    specifiers (``>=3.0,!=3.1``), or no specifier at all.
+    """
+
+    def __init__(
+        self, specifiers: str = "", prereleases: Optional[bool] = None
+    ) -> None:
+        """Initialize a SpecifierSet instance.
+
+        :param specifiers:
+            The string representation of a specifier or a comma-separated list of
+            specifiers which will be parsed and normalized before use.
+        :param prereleases:
+            This tells the SpecifierSet if it should accept prerelease versions if
+            applicable or not. The default of ``None`` will autodetect it from the
+            given specifiers.
+
+        :raises InvalidSpecifier:
+            If the given ``specifiers`` are not parseable than this exception will be
+            raised.
+        """
+
+        # Split on `,` to break each individual specifier into it's own item, and
         # strip each item to remove leading/trailing whitespace.
-        specifiers = [s.strip() for s in specifiers.split(",") if s.strip()]
+        split_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()]
 
         # Parsed each individual specifier, attempting first to make it a
-        # Specifier and falling back to a LegacySpecifier.
-        parsed = set()
-        for specifier in specifiers:
-            try:
-                parsed.add(Specifier(specifier))
-            except InvalidSpecifier:
-                parsed.add(LegacySpecifier(specifier))
+        # Specifier.
+        parsed: Set[Specifier] = set()
+        for specifier in split_specifiers:
+            parsed.add(Specifier(specifier))
 
         # Turn our parsed specifiers into a frozen set and save them for later.
         self._specs = frozenset(parsed)
@@ -609,23 +720,75 @@ class SpecifierSet(BaseSpecifier):
         # we accept prereleases or not.
         self._prereleases = prereleases
 
-    def __repr__(self):
+    @property
+    def prereleases(self) -> Optional[bool]:
+        # If we have been given an explicit prerelease modifier, then we'll
+        # pass that through here.
+        if self._prereleases is not None:
+            return self._prereleases
+
+        # If we don't have any specifiers, and we don't have a forced value,
+        # then we'll just return None since we don't know if this should have
+        # pre-releases or not.
+        if not self._specs:
+            return None
+
+        # Otherwise we'll see if any of the given specifiers accept
+        # prereleases, if any of them do we'll return True, otherwise False.
+        return any(s.prereleases for s in self._specs)
+
+    @prereleases.setter
+    def prereleases(self, value: bool) -> None:
+        self._prereleases = value
+
+    def __repr__(self) -> str:
+        """A representation of the specifier set that shows all internal state.
+
+        Note that the ordering of the individual specifiers within the set may not
+        match the input string.
+
+        >>> SpecifierSet('>=1.0.0,!=2.0.0')
+        =1.0.0')>
+        >>> SpecifierSet('>=1.0.0,!=2.0.0', prereleases=False)
+        =1.0.0', prereleases=False)>
+        >>> SpecifierSet('>=1.0.0,!=2.0.0', prereleases=True)
+        =1.0.0', prereleases=True)>
+        """
         pre = (
-            ", prereleases={0!r}".format(self.prereleases)
+            f", prereleases={self.prereleases!r}"
             if self._prereleases is not None
             else ""
         )
 
-        return "".format(str(self), pre)
+        return f""
 
-    def __str__(self):
+    def __str__(self) -> str:
+        """A string representation of the specifier set that can be round-tripped.
+
+        Note that the ordering of the individual specifiers within the set may not
+        match the input string.
+
+        >>> str(SpecifierSet(">=1.0.0,!=1.0.1"))
+        '!=1.0.1,>=1.0.0'
+        >>> str(SpecifierSet(">=1.0.0,!=1.0.1", prereleases=False))
+        '!=1.0.1,>=1.0.0'
+        """
         return ",".join(sorted(str(s) for s in self._specs))
 
-    def __hash__(self):
+    def __hash__(self) -> int:
         return hash(self._specs)
 
-    def __and__(self, other):
-        if isinstance(other, string_types):
+    def __and__(self, other: Union["SpecifierSet", str]) -> "SpecifierSet":
+        """Return a SpecifierSet which is a combination of the two sets.
+
+        :param other: The other object to combine with.
+
+        >>> SpecifierSet(">=1.0.0,!=1.0.1") & '<=2.0.0,!=2.0.1'
+        =1.0.0')>
+        >>> SpecifierSet(">=1.0.0,!=1.0.1") & SpecifierSet('<=2.0.0,!=2.0.1')
+        =1.0.0')>
+        """
+        if isinstance(other, str):
             other = SpecifierSet(other)
         elif not isinstance(other, SpecifierSet):
             return NotImplemented
@@ -647,60 +810,99 @@ class SpecifierSet(BaseSpecifier):
 
         return specifier
 
-    def __eq__(self, other):
-        if isinstance(other, string_types):
-            other = SpecifierSet(other)
-        elif isinstance(other, _IndividualSpecifier):
+    def __eq__(self, other: object) -> bool:
+        """Whether or not the two SpecifierSet-like objects are equal.
+
+        :param other: The other object to check against.
+
+        The value of :attr:`prereleases` is ignored.
+
+        >>> SpecifierSet(">=1.0.0,!=1.0.1") == SpecifierSet(">=1.0.0,!=1.0.1")
+        True
+        >>> (SpecifierSet(">=1.0.0,!=1.0.1", prereleases=False) ==
+        ...  SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True))
+        True
+        >>> SpecifierSet(">=1.0.0,!=1.0.1") == ">=1.0.0,!=1.0.1"
+        True
+        >>> SpecifierSet(">=1.0.0,!=1.0.1") == SpecifierSet(">=1.0.0")
+        False
+        >>> SpecifierSet(">=1.0.0,!=1.0.1") == SpecifierSet(">=1.0.0,!=1.0.2")
+        False
+        """
+        if isinstance(other, (str, Specifier)):
             other = SpecifierSet(str(other))
         elif not isinstance(other, SpecifierSet):
             return NotImplemented
 
         return self._specs == other._specs
 
-    def __ne__(self, other):
-        if isinstance(other, string_types):
-            other = SpecifierSet(other)
-        elif isinstance(other, _IndividualSpecifier):
-            other = SpecifierSet(str(other))
-        elif not isinstance(other, SpecifierSet):
-            return NotImplemented
-
-        return self._specs != other._specs
-
-    def __len__(self):
+    def __len__(self) -> int:
+        """Returns the number of specifiers in this specifier set."""
         return len(self._specs)
 
-    def __iter__(self):
+    def __iter__(self) -> Iterator[Specifier]:
+        """
+        Returns an iterator over all the underlying :class:`Specifier` instances
+        in this specifier set.
+
+        >>> sorted(SpecifierSet(">=1.0.0,!=1.0.1"), key=str)
+        [, =1.0.0')>]
+        """
         return iter(self._specs)
 
-    @property
-    def prereleases(self):
-        # If we have been given an explicit prerelease modifier, then we'll
-        # pass that through here.
-        if self._prereleases is not None:
-            return self._prereleases
+    def __contains__(self, item: UnparsedVersion) -> bool:
+        """Return whether or not the item is contained in this specifier.
 
-        # If we don't have any specifiers, and we don't have a forced value,
-        # then we'll just return None since we don't know if this should have
-        # pre-releases or not.
-        if not self._specs:
-            return None
+        :param item: The item to check for.
 
-        # Otherwise we'll see if any of the given specifiers accept
-        # prereleases, if any of them do we'll return True, otherwise False.
-        return any(s.prereleases for s in self._specs)
+        This is used for the ``in`` operator and behaves the same as
+        :meth:`contains` with no ``prereleases`` argument passed.
 
-    @prereleases.setter
-    def prereleases(self, value):
-        self._prereleases = value
-
-    def __contains__(self, item):
+        >>> "1.2.3" in SpecifierSet(">=1.0.0,!=1.0.1")
+        True
+        >>> Version("1.2.3") in SpecifierSet(">=1.0.0,!=1.0.1")
+        True
+        >>> "1.0.1" in SpecifierSet(">=1.0.0,!=1.0.1")
+        False
+        >>> "1.3.0a1" in SpecifierSet(">=1.0.0,!=1.0.1")
+        False
+        >>> "1.3.0a1" in SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True)
+        True
+        """
         return self.contains(item)
 
-    def contains(self, item, prereleases=None):
-        # Ensure that our item is a Version or LegacyVersion instance.
-        if not isinstance(item, (LegacyVersion, Version)):
-            item = parse(item)
+    def contains(
+        self,
+        item: UnparsedVersion,
+        prereleases: Optional[bool] = None,
+        installed: Optional[bool] = None,
+    ) -> bool:
+        """Return whether or not the item is contained in this SpecifierSet.
+
+        :param item:
+            The item to check for, which can be a version string or a
+            :class:`Version` instance.
+        :param prereleases:
+            Whether or not to match prereleases with this SpecifierSet. If set to
+            ``None`` (the default), it uses :attr:`prereleases` to determine
+            whether or not prereleases are allowed.
+
+        >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.2.3")
+        True
+        >>> SpecifierSet(">=1.0.0,!=1.0.1").contains(Version("1.2.3"))
+        True
+        >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.0.1")
+        False
+        >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.3.0a1")
+        False
+        >>> SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True).contains("1.3.0a1")
+        True
+        >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.3.0a1", prereleases=True)
+        True
+        """
+        # Ensure that our item is a Version instance.
+        if not isinstance(item, Version):
+            item = Version(item)
 
         # Determine if we're forcing a prerelease or not, if we're not forcing
         # one for this particular filter call, then we'll use whatever the
@@ -717,16 +919,56 @@ class SpecifierSet(BaseSpecifier):
         if not prereleases and item.is_prerelease:
             return False
 
+        if installed and item.is_prerelease:
+            item = Version(item.base_version)
+
         # We simply dispatch to the underlying specs here to make sure that the
         # given version is contained within all of them.
         # Note: This use of all() here means that an empty set of specifiers
         #       will always return True, this is an explicit design decision.
-        return all(
-            s.contains(item, prereleases=prereleases)
-            for s in self._specs
-        )
+        return all(s.contains(item, prereleases=prereleases) for s in self._specs)
 
-    def filter(self, iterable, prereleases=None):
+    def filter(
+        self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None
+    ) -> Iterator[UnparsedVersionVar]:
+        """Filter items in the given iterable, that match the specifiers in this set.
+
+        :param iterable:
+            An iterable that can contain version strings and :class:`Version` instances.
+            The items in the iterable will be filtered according to the specifier.
+        :param prereleases:
+            Whether or not to allow prereleases in the returned iterator. If set to
+            ``None`` (the default), it will be intelligently decide whether to allow
+            prereleases or not (based on the :attr:`prereleases` attribute, and
+            whether the only versions matching are prereleases).
+
+        This method is smarter than just ``filter(SpecifierSet(...).contains, [...])``
+        because it implements the rule from :pep:`440` that a prerelease item
+        SHOULD be accepted if no other versions match the given specifier.
+
+        >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.3", "1.5a1"]))
+        ['1.3']
+        >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.3", Version("1.4")]))
+        ['1.3', ]
+        >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.5a1"]))
+        []
+        >>> list(SpecifierSet(">=1.2.3").filter(["1.3", "1.5a1"], prereleases=True))
+        ['1.3', '1.5a1']
+        >>> list(SpecifierSet(">=1.2.3", prereleases=True).filter(["1.3", "1.5a1"]))
+        ['1.3', '1.5a1']
+
+        An "empty" SpecifierSet will filter items based on the presence of prerelease
+        versions in the set.
+
+        >>> list(SpecifierSet("").filter(["1.3", "1.5a1"]))
+        ['1.3']
+        >>> list(SpecifierSet("").filter(["1.5a1"]))
+        ['1.5a1']
+        >>> list(SpecifierSet("", prereleases=True).filter(["1.3", "1.5a1"]))
+        ['1.3', '1.5a1']
+        >>> list(SpecifierSet("").filter(["1.3", "1.5a1"], prereleases=True))
+        ['1.3', '1.5a1']
+        """
         # Determine if we're forcing a prerelease or not, if we're not forcing
         # one for this particular filter call, then we'll use whatever the
         # SpecifierSet thinks for whether or not we should support prereleases.
@@ -739,24 +981,16 @@ class SpecifierSet(BaseSpecifier):
         if self._specs:
             for spec in self._specs:
                 iterable = spec.filter(iterable, prereleases=bool(prereleases))
-            return iterable
+            return iter(iterable)
         # If we do not have any specifiers, then we need to have a rough filter
         # which will filter out any pre-releases, unless there are no final
-        # releases, and which will filter out LegacyVersion in general.
+        # releases.
         else:
-            filtered = []
-            found_prereleases = []
+            filtered: List[UnparsedVersionVar] = []
+            found_prereleases: List[UnparsedVersionVar] = []
 
             for item in iterable:
-                # Ensure that we some kind of Version class for this item.
-                if not isinstance(item, (LegacyVersion, Version)):
-                    parsed_version = parse(item)
-                else:
-                    parsed_version = item
-
-                # Filter out any item which is parsed as a LegacyVersion
-                if isinstance(parsed_version, LegacyVersion):
-                    continue
+                parsed_version = _coerce_version(item)
 
                 # Store any item which is a pre-release for later unless we've
                 # already found a final version or we are accepting prereleases
@@ -769,6 +1003,6 @@ class SpecifierSet(BaseSpecifier):
             # If we've found no items except for pre-releases, then we'll go
             # ahead and use the pre-releases
             if not filtered and found_prereleases and prereleases is None:
-                return found_prereleases
+                return iter(found_prereleases)
 
-            return filtered
+            return iter(filtered)
diff --git a/libs/common/pkg_resources/_vendor/packaging/utils.py b/libs/common/pkg_resources/_vendor/packaging/utils.py
index 942387ce..33c613b7 100644
--- a/libs/common/pkg_resources/_vendor/packaging/utils.py
+++ b/libs/common/pkg_resources/_vendor/packaging/utils.py
@@ -1,14 +1,141 @@
 # This file is dual licensed under the terms of the Apache License, Version
 # 2.0, and the BSD License. See the LICENSE file in the root of this repository
 # for complete details.
-from __future__ import absolute_import, division, print_function
 
 import re
+from typing import FrozenSet, NewType, Tuple, Union, cast
+
+from .tags import Tag, parse_tag
+from .version import InvalidVersion, Version
+
+BuildTag = Union[Tuple[()], Tuple[int, str]]
+NormalizedName = NewType("NormalizedName", str)
+
+
+class InvalidWheelFilename(ValueError):
+    """
+    An invalid wheel filename was found, users should refer to PEP 427.
+    """
+
+
+class InvalidSdistFilename(ValueError):
+    """
+    An invalid sdist filename was found, users should refer to the packaging user guide.
+    """
 
 
 _canonicalize_regex = re.compile(r"[-_.]+")
+# PEP 427: The build number must start with a digit.
+_build_tag_regex = re.compile(r"(\d+)(.*)")
 
 
-def canonicalize_name(name):
+def canonicalize_name(name: str) -> NormalizedName:
     # This is taken from PEP 503.
-    return _canonicalize_regex.sub("-", name).lower()
+    value = _canonicalize_regex.sub("-", name).lower()
+    return cast(NormalizedName, value)
+
+
+def canonicalize_version(
+    version: Union[Version, str], *, strip_trailing_zero: bool = True
+) -> str:
+    """
+    This is very similar to Version.__str__, but has one subtle difference
+    with the way it handles the release segment.
+    """
+    if isinstance(version, str):
+        try:
+            parsed = Version(version)
+        except InvalidVersion:
+            # Legacy versions cannot be normalized
+            return version
+    else:
+        parsed = version
+
+    parts = []
+
+    # Epoch
+    if parsed.epoch != 0:
+        parts.append(f"{parsed.epoch}!")
+
+    # Release segment
+    release_segment = ".".join(str(x) for x in parsed.release)
+    if strip_trailing_zero:
+        # NB: This strips trailing '.0's to normalize
+        release_segment = re.sub(r"(\.0)+$", "", release_segment)
+    parts.append(release_segment)
+
+    # Pre-release
+    if parsed.pre is not None:
+        parts.append("".join(str(x) for x in parsed.pre))
+
+    # Post-release
+    if parsed.post is not None:
+        parts.append(f".post{parsed.post}")
+
+    # Development release
+    if parsed.dev is not None:
+        parts.append(f".dev{parsed.dev}")
+
+    # Local version segment
+    if parsed.local is not None:
+        parts.append(f"+{parsed.local}")
+
+    return "".join(parts)
+
+
+def parse_wheel_filename(
+    filename: str,
+) -> Tuple[NormalizedName, Version, BuildTag, FrozenSet[Tag]]:
+    if not filename.endswith(".whl"):
+        raise InvalidWheelFilename(
+            f"Invalid wheel filename (extension must be '.whl'): {filename}"
+        )
+
+    filename = filename[:-4]
+    dashes = filename.count("-")
+    if dashes not in (4, 5):
+        raise InvalidWheelFilename(
+            f"Invalid wheel filename (wrong number of parts): {filename}"
+        )
+
+    parts = filename.split("-", dashes - 2)
+    name_part = parts[0]
+    # See PEP 427 for the rules on escaping the project name
+    if "__" in name_part or re.match(r"^[\w\d._]*$", name_part, re.UNICODE) is None:
+        raise InvalidWheelFilename(f"Invalid project name: {filename}")
+    name = canonicalize_name(name_part)
+    version = Version(parts[1])
+    if dashes == 5:
+        build_part = parts[2]
+        build_match = _build_tag_regex.match(build_part)
+        if build_match is None:
+            raise InvalidWheelFilename(
+                f"Invalid build number: {build_part} in '{filename}'"
+            )
+        build = cast(BuildTag, (int(build_match.group(1)), build_match.group(2)))
+    else:
+        build = ()
+    tags = parse_tag(parts[-1])
+    return (name, version, build, tags)
+
+
+def parse_sdist_filename(filename: str) -> Tuple[NormalizedName, Version]:
+    if filename.endswith(".tar.gz"):
+        file_stem = filename[: -len(".tar.gz")]
+    elif filename.endswith(".zip"):
+        file_stem = filename[: -len(".zip")]
+    else:
+        raise InvalidSdistFilename(
+            f"Invalid sdist filename (extension must be '.tar.gz' or '.zip'):"
+            f" {filename}"
+        )
+
+    # We are requiring a PEP 440 version, which cannot contain dashes,
+    # so we split on the last dash.
+    name_part, sep, version_part = file_stem.rpartition("-")
+    if not sep:
+        raise InvalidSdistFilename(f"Invalid sdist filename: {filename}")
+
+    name = canonicalize_name(name_part)
+    version = Version(version_part)
+    return (name, version)
diff --git a/libs/common/pkg_resources/_vendor/packaging/version.py b/libs/common/pkg_resources/_vendor/packaging/version.py
index 83b5ee8c..b30e8cbf 100644
--- a/libs/common/pkg_resources/_vendor/packaging/version.py
+++ b/libs/common/pkg_resources/_vendor/packaging/version.py
@@ -1,162 +1,116 @@
 # This file is dual licensed under the terms of the Apache License, Version
 # 2.0, and the BSD License. See the LICENSE file in the root of this repository
 # for complete details.
-from __future__ import absolute_import, division, print_function
+"""
+.. testsetup::
+
+    from packaging.version import parse, Version
+"""
 
 import collections
 import itertools
 import re
+from typing import Any, Callable, Optional, SupportsInt, Tuple, Union
 
-from ._structures import Infinity
+from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType
 
+__all__ = ["VERSION_PATTERN", "parse", "Version", "InvalidVersion"]
 
-__all__ = [
-    "parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN"
+InfiniteTypes = Union[InfinityType, NegativeInfinityType]
+PrePostDevType = Union[InfiniteTypes, Tuple[str, int]]
+SubLocalType = Union[InfiniteTypes, int, str]
+LocalType = Union[
+    NegativeInfinityType,
+    Tuple[
+        Union[
+            SubLocalType,
+            Tuple[SubLocalType, str],
+            Tuple[NegativeInfinityType, SubLocalType],
+        ],
+        ...,
+    ],
 ]
-
+CmpKey = Tuple[
+    int, Tuple[int, ...], PrePostDevType, PrePostDevType, PrePostDevType, LocalType
+]
+VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool]
 
 _Version = collections.namedtuple(
-    "_Version",
-    ["epoch", "release", "dev", "pre", "post", "local"],
+    "_Version", ["epoch", "release", "dev", "pre", "post", "local"]
 )
 
 
-def parse(version):
+def parse(version: str) -> "Version":
+    """Parse the given version string.
+
+    >>> parse('1.0.dev1')
+    
+
+    :param version: The version string to parse.
+    :raises InvalidVersion: When the version string is not a valid version.
     """
-    Parse the given version string and return either a :class:`Version` object
-    or a :class:`LegacyVersion` object depending on if the given version is
-    a valid PEP 440 version or a legacy version.
-    """
-    try:
-        return Version(version)
-    except InvalidVersion:
-        return LegacyVersion(version)
+    return Version(version)
 
 
 class InvalidVersion(ValueError):
-    """
-    An invalid version was found, users should refer to PEP 440.
+    """Raised when a version string is not a valid version.
+
+    >>> Version("invalid")
+    Traceback (most recent call last):
+        ...
+    packaging.version.InvalidVersion: Invalid version: 'invalid'
     """
 
 
-class _BaseVersion(object):
+class _BaseVersion:
+    _key: Tuple[Any, ...]
 
-    def __hash__(self):
+    def __hash__(self) -> int:
         return hash(self._key)
 
-    def __lt__(self, other):
-        return self._compare(other, lambda s, o: s < o)
-
-    def __le__(self, other):
-        return self._compare(other, lambda s, o: s <= o)
-
-    def __eq__(self, other):
-        return self._compare(other, lambda s, o: s == o)
-
-    def __ge__(self, other):
-        return self._compare(other, lambda s, o: s >= o)
-
-    def __gt__(self, other):
-        return self._compare(other, lambda s, o: s > o)
-
-    def __ne__(self, other):
-        return self._compare(other, lambda s, o: s != o)
-
-    def _compare(self, other, method):
+    # Please keep the duplicated `isinstance` check
+    # in the six comparisons hereunder
+    # unless you find a way to avoid adding overhead function calls.
+    def __lt__(self, other: "_BaseVersion") -> bool:
         if not isinstance(other, _BaseVersion):
             return NotImplemented
 
-        return method(self._key, other._key)
+        return self._key < other._key
 
+    def __le__(self, other: "_BaseVersion") -> bool:
+        if not isinstance(other, _BaseVersion):
+            return NotImplemented
 
-class LegacyVersion(_BaseVersion):
+        return self._key <= other._key
 
-    def __init__(self, version):
-        self._version = str(version)
-        self._key = _legacy_cmpkey(self._version)
+    def __eq__(self, other: object) -> bool:
+        if not isinstance(other, _BaseVersion):
+            return NotImplemented
 
-    def __str__(self):
-        return self._version
+        return self._key == other._key
 
-    def __repr__(self):
-        return "".format(repr(str(self)))
+    def __ge__(self, other: "_BaseVersion") -> bool:
+        if not isinstance(other, _BaseVersion):
+            return NotImplemented
 
-    @property
-    def public(self):
-        return self._version
+        return self._key >= other._key
 
-    @property
-    def base_version(self):
-        return self._version
+    def __gt__(self, other: "_BaseVersion") -> bool:
+        if not isinstance(other, _BaseVersion):
+            return NotImplemented
 
-    @property
-    def local(self):
-        return None
+        return self._key > other._key
 
-    @property
-    def is_prerelease(self):
-        return False
+    def __ne__(self, other: object) -> bool:
+        if not isinstance(other, _BaseVersion):
+            return NotImplemented
 
-    @property
-    def is_postrelease(self):
-        return False
+        return self._key != other._key
 
 
-_legacy_version_component_re = re.compile(
-    r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE,
-)
-
-_legacy_version_replacement_map = {
-    "pre": "c", "preview": "c", "-": "final-", "rc": "c", "dev": "@",
-}
-
-
-def _parse_version_parts(s):
-    for part in _legacy_version_component_re.split(s):
-        part = _legacy_version_replacement_map.get(part, part)
-
-        if not part or part == ".":
-            continue
-
-        if part[:1] in "0123456789":
-            # pad for numeric comparison
-            yield part.zfill(8)
-        else:
-            yield "*" + part
-
-    # ensure that alpha/beta/candidate are before final
-    yield "*final"
-
-
-def _legacy_cmpkey(version):
-    # We hardcode an epoch of -1 here. A PEP 440 version can only have a epoch
-    # greater than or equal to 0. This will effectively put the LegacyVersion,
-    # which uses the defacto standard originally implemented by setuptools,
-    # as before all PEP 440 versions.
-    epoch = -1
-
-    # This scheme is taken from pkg_resources.parse_version setuptools prior to
-    # it's adoption of the packaging library.
-    parts = []
-    for part in _parse_version_parts(version.lower()):
-        if part.startswith("*"):
-            # remove "-" before a prerelease tag
-            if part < "*final":
-                while parts and parts[-1] == "*final-":
-                    parts.pop()
-
-            # remove trailing zeros from each series of numeric parts
-            while parts and parts[-1] == "00000000":
-                parts.pop()
-
-        parts.append(part)
-    parts = tuple(parts)
-
-    return epoch, parts
-
 # Deliberately not anchored to the start and end of the string, to make it
 # easier for 3rd party code to reuse
-VERSION_PATTERN = r"""
+_VERSION_PATTERN = r"""
     v?
     (?:
         (?:(?P[0-9]+)!)?                           # epoch
@@ -187,36 +141,71 @@ VERSION_PATTERN = r"""
     (?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))?       # local version
 """
 
+VERSION_PATTERN = _VERSION_PATTERN
+"""
+A string containing the regular expression used to match a valid version.
+
+The pattern is not anchored at either end, and is intended for embedding in larger
+expressions (for example, matching a version number as part of a file name). The
+regular expression should be compiled with the ``re.VERBOSE`` and ``re.IGNORECASE``
+flags set.
+
+:meta hide-value:
+"""
+
 
 class Version(_BaseVersion):
+    """This class abstracts handling of a project's versions.
 
-    _regex = re.compile(
-        r"^\s*" + VERSION_PATTERN + r"\s*$",
-        re.VERBOSE | re.IGNORECASE,
-    )
+    A :class:`Version` instance is comparison aware and can be compared and
+    sorted using the standard Python interfaces.
+
+    >>> v1 = Version("1.0a5")
+    >>> v2 = Version("1.0")
+    >>> v1
+    
+    >>> v2
+    
+    >>> v1 < v2
+    True
+    >>> v1 == v2
+    False
+    >>> v1 > v2
+    False
+    >>> v1 >= v2
+    False
+    >>> v1 <= v2
+    True
+    """
+
+    _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE)
+    _key: CmpKey
+
+    def __init__(self, version: str) -> None:
+        """Initialize a Version object.
+
+        :param version:
+            The string representation of a version which will be parsed and normalized
+            before use.
+        :raises InvalidVersion:
+            If the ``version`` does not conform to PEP 440 in any way then this
+            exception will be raised.
+        """
 
-    def __init__(self, version):
         # Validate the version and parse it into pieces
         match = self._regex.search(version)
         if not match:
-            raise InvalidVersion("Invalid version: '{0}'".format(version))
+            raise InvalidVersion(f"Invalid version: '{version}'")
 
         # Store the parsed out pieces of the version
         self._version = _Version(
             epoch=int(match.group("epoch")) if match.group("epoch") else 0,
             release=tuple(int(i) for i in match.group("release").split(".")),
-            pre=_parse_letter_version(
-                match.group("pre_l"),
-                match.group("pre_n"),
-            ),
+            pre=_parse_letter_version(match.group("pre_l"), match.group("pre_n")),
             post=_parse_letter_version(
-                match.group("post_l"),
-                match.group("post_n1") or match.group("post_n2"),
-            ),
-            dev=_parse_letter_version(
-                match.group("dev_l"),
-                match.group("dev_n"),
+                match.group("post_l"), match.group("post_n1") or match.group("post_n2")
             ),
+            dev=_parse_letter_version(match.group("dev_l"), match.group("dev_n")),
             local=_parse_local_version(match.group("local")),
         )
 
@@ -230,72 +219,241 @@ class Version(_BaseVersion):
             self._version.local,
         )
 
-    def __repr__(self):
-        return "".format(repr(str(self)))
+    def __repr__(self) -> str:
+        """A representation of the Version that shows all internal state.
 
-    def __str__(self):
+        >>> Version('1.0.0')
+        
+        """
+        return f""
+
+    def __str__(self) -> str:
+        """A string representation of the version that can be rounded-tripped.
+
+        >>> str(Version("1.0a5"))
+        '1.0a5'
+        """
         parts = []
 
         # Epoch
-        if self._version.epoch != 0:
-            parts.append("{0}!".format(self._version.epoch))
+        if self.epoch != 0:
+            parts.append(f"{self.epoch}!")
 
         # Release segment
-        parts.append(".".join(str(x) for x in self._version.release))
+        parts.append(".".join(str(x) for x in self.release))
 
         # Pre-release
-        if self._version.pre is not None:
-            parts.append("".join(str(x) for x in self._version.pre))
+        if self.pre is not None:
+            parts.append("".join(str(x) for x in self.pre))
 
         # Post-release
-        if self._version.post is not None:
-            parts.append(".post{0}".format(self._version.post[1]))
+        if self.post is not None:
+            parts.append(f".post{self.post}")
 
         # Development release
-        if self._version.dev is not None:
-            parts.append(".dev{0}".format(self._version.dev[1]))
+        if self.dev is not None:
+            parts.append(f".dev{self.dev}")
 
         # Local version segment
-        if self._version.local is not None:
-            parts.append(
-                "+{0}".format(".".join(str(x) for x in self._version.local))
-            )
+        if self.local is not None:
+            parts.append(f"+{self.local}")
 
         return "".join(parts)
 
     @property
-    def public(self):
+    def epoch(self) -> int:
+        """The epoch of the version.
+
+        >>> Version("2.0.0").epoch
+        0
+        >>> Version("1!2.0.0").epoch
+        1
+        """
+        _epoch: int = self._version.epoch
+        return _epoch
+
+    @property
+    def release(self) -> Tuple[int, ...]:
+        """The components of the "release" segment of the version.
+
+        >>> Version("1.2.3").release
+        (1, 2, 3)
+        >>> Version("2.0.0").release
+        (2, 0, 0)
+        >>> Version("1!2.0.0.post0").release
+        (2, 0, 0)
+
+        Includes trailing zeroes but not the epoch or any pre-release / development /
+        post-release suffixes.
+        """
+        _release: Tuple[int, ...] = self._version.release
+        return _release
+
+    @property
+    def pre(self) -> Optional[Tuple[str, int]]:
+        """The pre-release segment of the version.
+
+        >>> print(Version("1.2.3").pre)
+        None
+        >>> Version("1.2.3a1").pre
+        ('a', 1)
+        >>> Version("1.2.3b1").pre
+        ('b', 1)
+        >>> Version("1.2.3rc1").pre
+        ('rc', 1)
+        """
+        _pre: Optional[Tuple[str, int]] = self._version.pre
+        return _pre
+
+    @property
+    def post(self) -> Optional[int]:
+        """The post-release number of the version.
+
+        >>> print(Version("1.2.3").post)
+        None
+        >>> Version("1.2.3.post1").post
+        1
+        """
+        return self._version.post[1] if self._version.post else None
+
+    @property
+    def dev(self) -> Optional[int]:
+        """The development number of the version.
+
+        >>> print(Version("1.2.3").dev)
+        None
+        >>> Version("1.2.3.dev1").dev
+        1
+        """
+        return self._version.dev[1] if self._version.dev else None
+
+    @property
+    def local(self) -> Optional[str]:
+        """The local version segment of the version.
+
+        >>> print(Version("1.2.3").local)
+        None
+        >>> Version("1.2.3+abc").local
+        'abc'
+        """
+        if self._version.local:
+            return ".".join(str(x) for x in self._version.local)
+        else:
+            return None
+
+    @property
+    def public(self) -> str:
+        """The public portion of the version.
+
+        >>> Version("1.2.3").public
+        '1.2.3'
+        >>> Version("1.2.3+abc").public
+        '1.2.3'
+        >>> Version("1.2.3+abc.dev1").public
+        '1.2.3'
+        """
         return str(self).split("+", 1)[0]
 
     @property
-    def base_version(self):
+    def base_version(self) -> str:
+        """The "base version" of the version.
+
+        >>> Version("1.2.3").base_version
+        '1.2.3'
+        >>> Version("1.2.3+abc").base_version
+        '1.2.3'
+        >>> Version("1!1.2.3+abc.dev1").base_version
+        '1!1.2.3'
+
+        The "base version" is the public version of the project without any pre or post
+        release markers.
+        """
         parts = []
 
         # Epoch
-        if self._version.epoch != 0:
-            parts.append("{0}!".format(self._version.epoch))
+        if self.epoch != 0:
+            parts.append(f"{self.epoch}!")
 
         # Release segment
-        parts.append(".".join(str(x) for x in self._version.release))
+        parts.append(".".join(str(x) for x in self.release))
 
         return "".join(parts)
 
     @property
-    def local(self):
-        version_string = str(self)
-        if "+" in version_string:
-            return version_string.split("+", 1)[1]
+    def is_prerelease(self) -> bool:
+        """Whether this version is a pre-release.
+
+        >>> Version("1.2.3").is_prerelease
+        False
+        >>> Version("1.2.3a1").is_prerelease
+        True
+        >>> Version("1.2.3b1").is_prerelease
+        True
+        >>> Version("1.2.3rc1").is_prerelease
+        True
+        >>> Version("1.2.3dev1").is_prerelease
+        True
+        """
+        return self.dev is not None or self.pre is not None
 
     @property
-    def is_prerelease(self):
-        return bool(self._version.dev or self._version.pre)
+    def is_postrelease(self) -> bool:
+        """Whether this version is a post-release.
+
+        >>> Version("1.2.3").is_postrelease
+        False
+        >>> Version("1.2.3.post1").is_postrelease
+        True
+        """
+        return self.post is not None
 
     @property
-    def is_postrelease(self):
-        return bool(self._version.post)
+    def is_devrelease(self) -> bool:
+        """Whether this version is a development release.
+
+        >>> Version("1.2.3").is_devrelease
+        False
+        >>> Version("1.2.3.dev1").is_devrelease
+        True
+        """
+        return self.dev is not None
+
+    @property
+    def major(self) -> int:
+        """The first item of :attr:`release` or ``0`` if unavailable.
+
+        >>> Version("1.2.3").major
+        1
+        """
+        return self.release[0] if len(self.release) >= 1 else 0
+
+    @property
+    def minor(self) -> int:
+        """The second item of :attr:`release` or ``0`` if unavailable.
+
+        >>> Version("1.2.3").minor
+        2
+        >>> Version("1").minor
+        0
+        """
+        return self.release[1] if len(self.release) >= 2 else 0
+
+    @property
+    def micro(self) -> int:
+        """The third item of :attr:`release` or ``0`` if unavailable.
+
+        >>> Version("1.2.3").micro
+        3
+        >>> Version("1").micro
+        0
+        """
+        return self.release[2] if len(self.release) >= 3 else 0
 
 
-def _parse_letter_version(letter, number):
+def _parse_letter_version(
+    letter: str, number: Union[str, bytes, SupportsInt]
+) -> Optional[Tuple[str, int]]:
+
     if letter:
         # We consider there to be an implicit 0 in a pre-release if there is
         # not a numeral associated with it.
@@ -325,34 +483,40 @@ def _parse_letter_version(letter, number):
 
         return letter, int(number)
 
-
-_local_version_seperators = re.compile(r"[\._-]")
+    return None
 
 
-def _parse_local_version(local):
+_local_version_separators = re.compile(r"[\._-]")
+
+
+def _parse_local_version(local: str) -> Optional[LocalType]:
     """
     Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
     """
     if local is not None:
         return tuple(
             part.lower() if not part.isdigit() else int(part)
-            for part in _local_version_seperators.split(local)
+            for part in _local_version_separators.split(local)
         )
+    return None
 
 
-def _cmpkey(epoch, release, pre, post, dev, local):
+def _cmpkey(
+    epoch: int,
+    release: Tuple[int, ...],
+    pre: Optional[Tuple[str, int]],
+    post: Optional[Tuple[str, int]],
+    dev: Optional[Tuple[str, int]],
+    local: Optional[Tuple[SubLocalType]],
+) -> CmpKey:
+
     # When we compare a release version, we want to compare it with all of the
     # trailing zeros removed. So we'll use a reverse the list, drop all the now
     # leading zeros until we come to something non zero, then take the rest
     # re-reverse it back into the correct order and make it a tuple and use
     # that for our sorting key.
-    release = tuple(
-        reversed(list(
-            itertools.dropwhile(
-                lambda x: x == 0,
-                reversed(release),
-            )
-        ))
+    _release = tuple(
+        reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release))))
     )
 
     # We need to "trick" the sorting algorithm to put 1.0.dev0 before 1.0a0.
@@ -360,23 +524,31 @@ def _cmpkey(epoch, release, pre, post, dev, local):
     # if there is not a pre or a post segment. If we have one of those then
     # the normal sorting rules will handle this case correctly.
     if pre is None and post is None and dev is not None:
-        pre = -Infinity
+        _pre: PrePostDevType = NegativeInfinity
     # Versions without a pre-release (except as noted above) should sort after
     # those with one.
     elif pre is None:
-        pre = Infinity
+        _pre = Infinity
+    else:
+        _pre = pre
 
     # Versions without a post segment should sort before those with one.
     if post is None:
-        post = -Infinity
+        _post: PrePostDevType = NegativeInfinity
+
+    else:
+        _post = post
 
     # Versions without a development segment should sort after those with one.
     if dev is None:
-        dev = Infinity
+        _dev: PrePostDevType = Infinity
+
+    else:
+        _dev = dev
 
     if local is None:
         # Versions without a local segment should sort before those with one.
-        local = -Infinity
+        _local: LocalType = NegativeInfinity
     else:
         # Versions with a local segment need that segment parsed to implement
         # the sorting rules in PEP440.
@@ -385,9 +557,8 @@ def _cmpkey(epoch, release, pre, post, dev, local):
         # - Numeric segments sort numerically
         # - Shorter versions sort before longer versions when the prefixes
         #   match exactly
-        local = tuple(
-            (i, "") if isinstance(i, int) else (-Infinity, i)
-            for i in local
+        _local = tuple(
+            (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local
         )
 
-    return epoch, release, pre, post, dev, local
+    return epoch, _release, _pre, _post, _dev, _local
diff --git a/libs/common/pkg_resources/_vendor/pyparsing.py b/libs/common/pkg_resources/_vendor/pyparsing.py
deleted file mode 100644
index 4aa30ee6..00000000
--- a/libs/common/pkg_resources/_vendor/pyparsing.py
+++ /dev/null
@@ -1,5742 +0,0 @@
-# module pyparsing.py
-#
-# Copyright (c) 2003-2018  Paul T. McGuire
-#
-# Permission is hereby granted, free of charge, to any person obtaining
-# a copy of this software and associated documentation files (the
-# "Software"), to deal in the Software without restriction, including
-# without limitation the rights to use, copy, modify, merge, publish,
-# distribute, sublicense, and/or sell copies of the Software, and to
-# permit persons to whom the Software is furnished to do so, subject to
-# the following conditions:
-#
-# The above copyright notice and this permission notice shall be
-# included in all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
-# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
-# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
-# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-#
-
-__doc__ = \
-"""
-pyparsing module - Classes and methods to define and execute parsing grammars
-=============================================================================
-
-The pyparsing module is an alternative approach to creating and executing simple grammars,
-vs. the traditional lex/yacc approach, or the use of regular expressions.  With pyparsing, you
-don't need to learn a new syntax for defining grammars or matching expressions - the parsing module
-provides a library of classes that you use to construct the grammar directly in Python.
-
-Here is a program to parse "Hello, World!" (or any greeting of the form 
-C{", !"}), built up using L{Word}, L{Literal}, and L{And} elements 
-(L{'+'} operator gives L{And} expressions, strings are auto-converted to
-L{Literal} expressions)::
-
-    from pyparsing import Word, alphas
-
-    # define grammar of a greeting
-    greet = Word(alphas) + "," + Word(alphas) + "!"
-
-    hello = "Hello, World!"
-    print (hello, "->", greet.parseString(hello))
-
-The program outputs the following::
-
-    Hello, World! -> ['Hello', ',', 'World', '!']
-
-The Python representation of the grammar is quite readable, owing to the self-explanatory
-class names, and the use of '+', '|' and '^' operators.
-
-The L{ParseResults} object returned from L{ParserElement.parseString} can be accessed as a nested list, a dictionary, or an
-object with named attributes.
-
-The pyparsing module handles some of the problems that are typically vexing when writing text parsers:
- - extra or missing whitespace (the above program will also handle "Hello,World!", "Hello  ,  World  !", etc.)
- - quoted strings
- - embedded comments
-
-
-Getting Started -
------------------
-Visit the classes L{ParserElement} and L{ParseResults} to see the base classes that most other pyparsing
-classes inherit from. Use the docstrings for examples of how to:
- - construct literal match expressions from L{Literal} and L{CaselessLiteral} classes
- - construct character word-group expressions using the L{Word} class
- - see how to create repetitive expressions using L{ZeroOrMore} and L{OneOrMore} classes
- - use L{'+'}, L{'|'}, L{'^'}, and L{'&'} operators to combine simple expressions into more complex ones
- - associate names with your parsed results using L{ParserElement.setResultsName}
- - find some helpful expression short-cuts like L{delimitedList} and L{oneOf}
- - find more useful common expressions in the L{pyparsing_common} namespace class
-"""
-
-__version__ = "2.2.1"
-__versionTime__ = "18 Sep 2018 00:49 UTC"
-__author__ = "Paul McGuire "
-
-import string
-from weakref import ref as wkref
-import copy
-import sys
-import warnings
-import re
-import sre_constants
-import collections
-import pprint
-import traceback
-import types
-from datetime import datetime
-
-try:
-    from _thread import RLock
-except ImportError:
-    from threading import RLock
-
-try:
-    # Python 3
-    from collections.abc import Iterable
-    from collections.abc import MutableMapping
-except ImportError:
-    # Python 2.7
-    from collections import Iterable
-    from collections import MutableMapping
-
-try:
-    from collections import OrderedDict as _OrderedDict
-except ImportError:
-    try:
-        from ordereddict import OrderedDict as _OrderedDict
-    except ImportError:
-        _OrderedDict = None
-
-#~ sys.stderr.write( "testing pyparsing module, version %s, %s\n" % (__version__,__versionTime__ ) )
-
-__all__ = [
-'And', 'CaselessKeyword', 'CaselessLiteral', 'CharsNotIn', 'Combine', 'Dict', 'Each', 'Empty',
-'FollowedBy', 'Forward', 'GoToColumn', 'Group', 'Keyword', 'LineEnd', 'LineStart', 'Literal',
-'MatchFirst', 'NoMatch', 'NotAny', 'OneOrMore', 'OnlyOnce', 'Optional', 'Or',
-'ParseBaseException', 'ParseElementEnhance', 'ParseException', 'ParseExpression', 'ParseFatalException',
-'ParseResults', 'ParseSyntaxException', 'ParserElement', 'QuotedString', 'RecursiveGrammarException',
-'Regex', 'SkipTo', 'StringEnd', 'StringStart', 'Suppress', 'Token', 'TokenConverter', 
-'White', 'Word', 'WordEnd', 'WordStart', 'ZeroOrMore',
-'alphanums', 'alphas', 'alphas8bit', 'anyCloseTag', 'anyOpenTag', 'cStyleComment', 'col',
-'commaSeparatedList', 'commonHTMLEntity', 'countedArray', 'cppStyleComment', 'dblQuotedString',
-'dblSlashComment', 'delimitedList', 'dictOf', 'downcaseTokens', 'empty', 'hexnums',
-'htmlComment', 'javaStyleComment', 'line', 'lineEnd', 'lineStart', 'lineno',
-'makeHTMLTags', 'makeXMLTags', 'matchOnlyAtCol', 'matchPreviousExpr', 'matchPreviousLiteral',
-'nestedExpr', 'nullDebugAction', 'nums', 'oneOf', 'opAssoc', 'operatorPrecedence', 'printables',
-'punc8bit', 'pythonStyleComment', 'quotedString', 'removeQuotes', 'replaceHTMLEntity', 
-'replaceWith', 'restOfLine', 'sglQuotedString', 'srange', 'stringEnd',
-'stringStart', 'traceParseAction', 'unicodeString', 'upcaseTokens', 'withAttribute',
-'indentedBlock', 'originalTextFor', 'ungroup', 'infixNotation','locatedExpr', 'withClass',
-'CloseMatch', 'tokenMap', 'pyparsing_common',
-]
-
-system_version = tuple(sys.version_info)[:3]
-PY_3 = system_version[0] == 3
-if PY_3:
-    _MAX_INT = sys.maxsize
-    basestring = str
-    unichr = chr
-    _ustr = str
-
-    # build list of single arg builtins, that can be used as parse actions
-    singleArgBuiltins = [sum, len, sorted, reversed, list, tuple, set, any, all, min, max]
-
-else:
-    _MAX_INT = sys.maxint
-    range = xrange
-
-    def _ustr(obj):
-        """Drop-in replacement for str(obj) that tries to be Unicode friendly. It first tries
-           str(obj). If that fails with a UnicodeEncodeError, then it tries unicode(obj). It
-           then < returns the unicode object | encodes it with the default encoding | ... >.
-        """
-        if isinstance(obj,unicode):
-            return obj
-
-        try:
-            # If this works, then _ustr(obj) has the same behaviour as str(obj), so
-            # it won't break any existing code.
-            return str(obj)
-
-        except UnicodeEncodeError:
-            # Else encode it
-            ret = unicode(obj).encode(sys.getdefaultencoding(), 'xmlcharrefreplace')
-            xmlcharref = Regex(r'&#\d+;')
-            xmlcharref.setParseAction(lambda t: '\\u' + hex(int(t[0][2:-1]))[2:])
-            return xmlcharref.transformString(ret)
-
-    # build list of single arg builtins, tolerant of Python version, that can be used as parse actions
-    singleArgBuiltins = []
-    import __builtin__
-    for fname in "sum len sorted reversed list tuple set any all min max".split():
-        try:
-            singleArgBuiltins.append(getattr(__builtin__,fname))
-        except AttributeError:
-            continue
-            
-_generatorType = type((y for y in range(1)))
- 
-def _xml_escape(data):
-    """Escape &, <, >, ", ', etc. in a string of data."""
-
-    # ampersand must be replaced first
-    from_symbols = '&><"\''
-    to_symbols = ('&'+s+';' for s in "amp gt lt quot apos".split())
-    for from_,to_ in zip(from_symbols, to_symbols):
-        data = data.replace(from_, to_)
-    return data
-
-class _Constants(object):
-    pass
-
-alphas     = string.ascii_uppercase + string.ascii_lowercase
-nums       = "0123456789"
-hexnums    = nums + "ABCDEFabcdef"
-alphanums  = alphas + nums
-_bslash    = chr(92)
-printables = "".join(c for c in string.printable if c not in string.whitespace)
-
-class ParseBaseException(Exception):
-    """base exception class for all parsing runtime exceptions"""
-    # Performance tuning: we construct a *lot* of these, so keep this
-    # constructor as small and fast as possible
-    def __init__( self, pstr, loc=0, msg=None, elem=None ):
-        self.loc = loc
-        if msg is None:
-            self.msg = pstr
-            self.pstr = ""
-        else:
-            self.msg = msg
-            self.pstr = pstr
-        self.parserElement = elem
-        self.args = (pstr, loc, msg)
-
-    @classmethod
-    def _from_exception(cls, pe):
-        """
-        internal factory method to simplify creating one type of ParseException 
-        from another - avoids having __init__ signature conflicts among subclasses
-        """
-        return cls(pe.pstr, pe.loc, pe.msg, pe.parserElement)
-
-    def __getattr__( self, aname ):
-        """supported attributes by name are:
-            - lineno - returns the line number of the exception text
-            - col - returns the column number of the exception text
-            - line - returns the line containing the exception text
-        """
-        if( aname == "lineno" ):
-            return lineno( self.loc, self.pstr )
-        elif( aname in ("col", "column") ):
-            return col( self.loc, self.pstr )
-        elif( aname == "line" ):
-            return line( self.loc, self.pstr )
-        else:
-            raise AttributeError(aname)
-
-    def __str__( self ):
-        return "%s (at char %d), (line:%d, col:%d)" % \
-                ( self.msg, self.loc, self.lineno, self.column )
-    def __repr__( self ):
-        return _ustr(self)
-    def markInputline( self, markerString = ">!<" ):
-        """Extracts the exception line from the input string, and marks
-           the location of the exception with a special symbol.
-        """
-        line_str = self.line
-        line_column = self.column - 1
-        if markerString:
-            line_str = "".join((line_str[:line_column],
-                                markerString, line_str[line_column:]))
-        return line_str.strip()
-    def __dir__(self):
-        return "lineno col line".split() + dir(type(self))
-
-class ParseException(ParseBaseException):
-    """
-    Exception thrown when parse expressions don't match class;
-    supported attributes by name are:
-     - lineno - returns the line number of the exception text
-     - col - returns the column number of the exception text
-     - line - returns the line containing the exception text
-        
-    Example::
-        try:
-            Word(nums).setName("integer").parseString("ABC")
-        except ParseException as pe:
-            print(pe)
-            print("column: {}".format(pe.col))
-            
-    prints::
-       Expected integer (at char 0), (line:1, col:1)
-        column: 1
-    """
-    pass
-
-class ParseFatalException(ParseBaseException):
-    """user-throwable exception thrown when inconsistent parse content
-       is found; stops all parsing immediately"""
-    pass
-
-class ParseSyntaxException(ParseFatalException):
-    """just like L{ParseFatalException}, but thrown internally when an
-       L{ErrorStop} ('-' operator) indicates that parsing is to stop 
-       immediately because an unbacktrackable syntax error has been found"""
-    pass
-
-#~ class ReparseException(ParseBaseException):
-    #~ """Experimental class - parse actions can raise this exception to cause
-       #~ pyparsing to reparse the input string:
-        #~ - with a modified input string, and/or
-        #~ - with a modified start location
-       #~ Set the values of the ReparseException in the constructor, and raise the
-       #~ exception in a parse action to cause pyparsing to use the new string/location.
-       #~ Setting the values as None causes no change to be made.
-       #~ """
-    #~ def __init_( self, newstring, restartLoc ):
-        #~ self.newParseText = newstring
-        #~ self.reparseLoc = restartLoc
-
-class RecursiveGrammarException(Exception):
-    """exception thrown by L{ParserElement.validate} if the grammar could be improperly recursive"""
-    def __init__( self, parseElementList ):
-        self.parseElementTrace = parseElementList
-
-    def __str__( self ):
-        return "RecursiveGrammarException: %s" % self.parseElementTrace
-
-class _ParseResultsWithOffset(object):
-    def __init__(self,p1,p2):
-        self.tup = (p1,p2)
-    def __getitem__(self,i):
-        return self.tup[i]
-    def __repr__(self):
-        return repr(self.tup[0])
-    def setOffset(self,i):
-        self.tup = (self.tup[0],i)
-
-class ParseResults(object):
-    """
-    Structured parse results, to provide multiple means of access to the parsed data:
-       - as a list (C{len(results)})
-       - by list index (C{results[0], results[1]}, etc.)
-       - by attribute (C{results.} - see L{ParserElement.setResultsName})
-
-    Example::
-        integer = Word(nums)
-        date_str = (integer.setResultsName("year") + '/' 
-                        + integer.setResultsName("month") + '/' 
-                        + integer.setResultsName("day"))
-        # equivalent form:
-        # date_str = integer("year") + '/' + integer("month") + '/' + integer("day")
-
-        # parseString returns a ParseResults object
-        result = date_str.parseString("1999/12/31")
-
-        def test(s, fn=repr):
-            print("%s -> %s" % (s, fn(eval(s))))
-        test("list(result)")
-        test("result[0]")
-        test("result['month']")
-        test("result.day")
-        test("'month' in result")
-        test("'minutes' in result")
-        test("result.dump()", str)
-    prints::
-        list(result) -> ['1999', '/', '12', '/', '31']
-        result[0] -> '1999'
-        result['month'] -> '12'
-        result.day -> '31'
-        'month' in result -> True
-        'minutes' in result -> False
-        result.dump() -> ['1999', '/', '12', '/', '31']
-        - day: 31
-        - month: 12
-        - year: 1999
-    """
-    def __new__(cls, toklist=None, name=None, asList=True, modal=True ):
-        if isinstance(toklist, cls):
-            return toklist
-        retobj = object.__new__(cls)
-        retobj.__doinit = True
-        return retobj
-
-    # Performance tuning: we construct a *lot* of these, so keep this
-    # constructor as small and fast as possible
-    def __init__( self, toklist=None, name=None, asList=True, modal=True, isinstance=isinstance ):
-        if self.__doinit:
-            self.__doinit = False
-            self.__name = None
-            self.__parent = None
-            self.__accumNames = {}
-            self.__asList = asList
-            self.__modal = modal
-            if toklist is None:
-                toklist = []
-            if isinstance(toklist, list):
-                self.__toklist = toklist[:]
-            elif isinstance(toklist, _generatorType):
-                self.__toklist = list(toklist)
-            else:
-                self.__toklist = [toklist]
-            self.__tokdict = dict()
-
-        if name is not None and name:
-            if not modal:
-                self.__accumNames[name] = 0
-            if isinstance(name,int):
-                name = _ustr(name) # will always return a str, but use _ustr for consistency
-            self.__name = name
-            if not (isinstance(toklist, (type(None), basestring, list)) and toklist in (None,'',[])):
-                if isinstance(toklist,basestring):
-                    toklist = [ toklist ]
-                if asList:
-                    if isinstance(toklist,ParseResults):
-                        self[name] = _ParseResultsWithOffset(toklist.copy(),0)
-                    else:
-                        self[name] = _ParseResultsWithOffset(ParseResults(toklist[0]),0)
-                    self[name].__name = name
-                else:
-                    try:
-                        self[name] = toklist[0]
-                    except (KeyError,TypeError,IndexError):
-                        self[name] = toklist
-
-    def __getitem__( self, i ):
-        if isinstance( i, (int,slice) ):
-            return self.__toklist[i]
-        else:
-            if i not in self.__accumNames:
-                return self.__tokdict[i][-1][0]
-            else:
-                return ParseResults([ v[0] for v in self.__tokdict[i] ])
-
-    def __setitem__( self, k, v, isinstance=isinstance ):
-        if isinstance(v,_ParseResultsWithOffset):
-            self.__tokdict[k] = self.__tokdict.get(k,list()) + [v]
-            sub = v[0]
-        elif isinstance(k,(int,slice)):
-            self.__toklist[k] = v
-            sub = v
-        else:
-            self.__tokdict[k] = self.__tokdict.get(k,list()) + [_ParseResultsWithOffset(v,0)]
-            sub = v
-        if isinstance(sub,ParseResults):
-            sub.__parent = wkref(self)
-
-    def __delitem__( self, i ):
-        if isinstance(i,(int,slice)):
-            mylen = len( self.__toklist )
-            del self.__toklist[i]
-
-            # convert int to slice
-            if isinstance(i, int):
-                if i < 0:
-                    i += mylen
-                i = slice(i, i+1)
-            # get removed indices
-            removed = list(range(*i.indices(mylen)))
-            removed.reverse()
-            # fixup indices in token dictionary
-            for name,occurrences in self.__tokdict.items():
-                for j in removed:
-                    for k, (value, position) in enumerate(occurrences):
-                        occurrences[k] = _ParseResultsWithOffset(value, position - (position > j))
-        else:
-            del self.__tokdict[i]
-
-    def __contains__( self, k ):
-        return k in self.__tokdict
-
-    def __len__( self ): return len( self.__toklist )
-    def __bool__(self): return ( not not self.__toklist )
-    __nonzero__ = __bool__
-    def __iter__( self ): return iter( self.__toklist )
-    def __reversed__( self ): return iter( self.__toklist[::-1] )
-    def _iterkeys( self ):
-        if hasattr(self.__tokdict, "iterkeys"):
-            return self.__tokdict.iterkeys()
-        else:
-            return iter(self.__tokdict)
-
-    def _itervalues( self ):
-        return (self[k] for k in self._iterkeys())
-            
-    def _iteritems( self ):
-        return ((k, self[k]) for k in self._iterkeys())
-
-    if PY_3:
-        keys = _iterkeys       
-        """Returns an iterator of all named result keys (Python 3.x only)."""
-
-        values = _itervalues
-        """Returns an iterator of all named result values (Python 3.x only)."""
-
-        items = _iteritems
-        """Returns an iterator of all named result key-value tuples (Python 3.x only)."""
-
-    else:
-        iterkeys = _iterkeys
-        """Returns an iterator of all named result keys (Python 2.x only)."""
-
-        itervalues = _itervalues
-        """Returns an iterator of all named result values (Python 2.x only)."""
-
-        iteritems = _iteritems
-        """Returns an iterator of all named result key-value tuples (Python 2.x only)."""
-
-        def keys( self ):
-            """Returns all named result keys (as a list in Python 2.x, as an iterator in Python 3.x)."""
-            return list(self.iterkeys())
-
-        def values( self ):
-            """Returns all named result values (as a list in Python 2.x, as an iterator in Python 3.x)."""
-            return list(self.itervalues())
-                
-        def items( self ):
-            """Returns all named result key-values (as a list of tuples in Python 2.x, as an iterator in Python 3.x)."""
-            return list(self.iteritems())
-
-    def haskeys( self ):
-        """Since keys() returns an iterator, this method is helpful in bypassing
-           code that looks for the existence of any defined results names."""
-        return bool(self.__tokdict)
-        
-    def pop( self, *args, **kwargs):
-        """
-        Removes and returns item at specified index (default=C{last}).
-        Supports both C{list} and C{dict} semantics for C{pop()}. If passed no
-        argument or an integer argument, it will use C{list} semantics
-        and pop tokens from the list of parsed tokens. If passed a 
-        non-integer argument (most likely a string), it will use C{dict}
-        semantics and pop the corresponding value from any defined 
-        results names. A second default return value argument is 
-        supported, just as in C{dict.pop()}.
-
-        Example::
-            def remove_first(tokens):
-                tokens.pop(0)
-            print(OneOrMore(Word(nums)).parseString("0 123 321")) # -> ['0', '123', '321']
-            print(OneOrMore(Word(nums)).addParseAction(remove_first).parseString("0 123 321")) # -> ['123', '321']
-
-            label = Word(alphas)
-            patt = label("LABEL") + OneOrMore(Word(nums))
-            print(patt.parseString("AAB 123 321").dump())
-
-            # Use pop() in a parse action to remove named result (note that corresponding value is not
-            # removed from list form of results)
-            def remove_LABEL(tokens):
-                tokens.pop("LABEL")
-                return tokens
-            patt.addParseAction(remove_LABEL)
-            print(patt.parseString("AAB 123 321").dump())
-        prints::
-            ['AAB', '123', '321']
-            - LABEL: AAB
-
-            ['AAB', '123', '321']
-        """
-        if not args:
-            args = [-1]
-        for k,v in kwargs.items():
-            if k == 'default':
-                args = (args[0], v)
-            else:
-                raise TypeError("pop() got an unexpected keyword argument '%s'" % k)
-        if (isinstance(args[0], int) or 
-                        len(args) == 1 or 
-                        args[0] in self):
-            index = args[0]
-            ret = self[index]
-            del self[index]
-            return ret
-        else:
-            defaultvalue = args[1]
-            return defaultvalue
-
-    def get(self, key, defaultValue=None):
-        """
-        Returns named result matching the given key, or if there is no
-        such name, then returns the given C{defaultValue} or C{None} if no
-        C{defaultValue} is specified.
-
-        Similar to C{dict.get()}.
-        
-        Example::
-            integer = Word(nums)
-            date_str = integer("year") + '/' + integer("month") + '/' + integer("day")           
-
-            result = date_str.parseString("1999/12/31")
-            print(result.get("year")) # -> '1999'
-            print(result.get("hour", "not specified")) # -> 'not specified'
-            print(result.get("hour")) # -> None
-        """
-        if key in self:
-            return self[key]
-        else:
-            return defaultValue
-
-    def insert( self, index, insStr ):
-        """
-        Inserts new element at location index in the list of parsed tokens.
-        
-        Similar to C{list.insert()}.
-
-        Example::
-            print(OneOrMore(Word(nums)).parseString("0 123 321")) # -> ['0', '123', '321']
-
-            # use a parse action to insert the parse location in the front of the parsed results
-            def insert_locn(locn, tokens):
-                tokens.insert(0, locn)
-            print(OneOrMore(Word(nums)).addParseAction(insert_locn).parseString("0 123 321")) # -> [0, '0', '123', '321']
-        """
-        self.__toklist.insert(index, insStr)
-        # fixup indices in token dictionary
-        for name,occurrences in self.__tokdict.items():
-            for k, (value, position) in enumerate(occurrences):
-                occurrences[k] = _ParseResultsWithOffset(value, position + (position > index))
-
-    def append( self, item ):
-        """
-        Add single element to end of ParseResults list of elements.
-
-        Example::
-            print(OneOrMore(Word(nums)).parseString("0 123 321")) # -> ['0', '123', '321']
-            
-            # use a parse action to compute the sum of the parsed integers, and add it to the end
-            def append_sum(tokens):
-                tokens.append(sum(map(int, tokens)))
-            print(OneOrMore(Word(nums)).addParseAction(append_sum).parseString("0 123 321")) # -> ['0', '123', '321', 444]
-        """
-        self.__toklist.append(item)
-
-    def extend( self, itemseq ):
-        """
-        Add sequence of elements to end of ParseResults list of elements.
-
-        Example::
-            patt = OneOrMore(Word(alphas))
-            
-            # use a parse action to append the reverse of the matched strings, to make a palindrome
-            def make_palindrome(tokens):
-                tokens.extend(reversed([t[::-1] for t in tokens]))
-                return ''.join(tokens)
-            print(patt.addParseAction(make_palindrome).parseString("lskdj sdlkjf lksd")) # -> 'lskdjsdlkjflksddsklfjkldsjdksl'
-        """
-        if isinstance(itemseq, ParseResults):
-            self += itemseq
-        else:
-            self.__toklist.extend(itemseq)
-
-    def clear( self ):
-        """
-        Clear all elements and results names.
-        """
-        del self.__toklist[:]
-        self.__tokdict.clear()
-
-    def __getattr__( self, name ):
-        try:
-            return self[name]
-        except KeyError:
-            return ""
-            
-        if name in self.__tokdict:
-            if name not in self.__accumNames:
-                return self.__tokdict[name][-1][0]
-            else:
-                return ParseResults([ v[0] for v in self.__tokdict[name] ])
-        else:
-            return ""
-
-    def __add__( self, other ):
-        ret = self.copy()
-        ret += other
-        return ret
-
-    def __iadd__( self, other ):
-        if other.__tokdict:
-            offset = len(self.__toklist)
-            addoffset = lambda a: offset if a<0 else a+offset
-            otheritems = other.__tokdict.items()
-            otherdictitems = [(k, _ParseResultsWithOffset(v[0],addoffset(v[1])) )
-                                for (k,vlist) in otheritems for v in vlist]
-            for k,v in otherdictitems:
-                self[k] = v
-                if isinstance(v[0],ParseResults):
-                    v[0].__parent = wkref(self)
-            
-        self.__toklist += other.__toklist
-        self.__accumNames.update( other.__accumNames )
-        return self
-
-    def __radd__(self, other):
-        if isinstance(other,int) and other == 0:
-            # useful for merging many ParseResults using sum() builtin
-            return self.copy()
-        else:
-            # this may raise a TypeError - so be it
-            return other + self
-        
-    def __repr__( self ):
-        return "(%s, %s)" % ( repr( self.__toklist ), repr( self.__tokdict ) )
-
-    def __str__( self ):
-        return '[' + ', '.join(_ustr(i) if isinstance(i, ParseResults) else repr(i) for i in self.__toklist) + ']'
-
-    def _asStringList( self, sep='' ):
-        out = []
-        for item in self.__toklist:
-            if out and sep:
-                out.append(sep)
-            if isinstance( item, ParseResults ):
-                out += item._asStringList()
-            else:
-                out.append( _ustr(item) )
-        return out
-
-    def asList( self ):
-        """
-        Returns the parse results as a nested list of matching tokens, all converted to strings.
-
-        Example::
-            patt = OneOrMore(Word(alphas))
-            result = patt.parseString("sldkj lsdkj sldkj")
-            # even though the result prints in string-like form, it is actually a pyparsing ParseResults
-            print(type(result), result) # ->  ['sldkj', 'lsdkj', 'sldkj']
-            
-            # Use asList() to create an actual list
-            result_list = result.asList()
-            print(type(result_list), result_list) # ->  ['sldkj', 'lsdkj', 'sldkj']
-        """
-        return [res.asList() if isinstance(res,ParseResults) else res for res in self.__toklist]
-
-    def asDict( self ):
-        """
-        Returns the named parse results as a nested dictionary.
-
-        Example::
-            integer = Word(nums)
-            date_str = integer("year") + '/' + integer("month") + '/' + integer("day")
-            
-            result = date_str.parseString('12/31/1999')
-            print(type(result), repr(result)) # ->  (['12', '/', '31', '/', '1999'], {'day': [('1999', 4)], 'year': [('12', 0)], 'month': [('31', 2)]})
-            
-            result_dict = result.asDict()
-            print(type(result_dict), repr(result_dict)) # ->  {'day': '1999', 'year': '12', 'month': '31'}
-
-            # even though a ParseResults supports dict-like access, sometime you just need to have a dict
-            import json
-            print(json.dumps(result)) # -> Exception: TypeError: ... is not JSON serializable
-            print(json.dumps(result.asDict())) # -> {"month": "31", "day": "1999", "year": "12"}
-        """
-        if PY_3:
-            item_fn = self.items
-        else:
-            item_fn = self.iteritems
-            
-        def toItem(obj):
-            if isinstance(obj, ParseResults):
-                if obj.haskeys():
-                    return obj.asDict()
-                else:
-                    return [toItem(v) for v in obj]
-            else:
-                return obj
-                
-        return dict((k,toItem(v)) for k,v in item_fn())
-
-    def copy( self ):
-        """
-        Returns a new copy of a C{ParseResults} object.
-        """
-        ret = ParseResults( self.__toklist )
-        ret.__tokdict = self.__tokdict.copy()
-        ret.__parent = self.__parent
-        ret.__accumNames.update( self.__accumNames )
-        ret.__name = self.__name
-        return ret
-
-    def asXML( self, doctag=None, namedItemsOnly=False, indent="", formatted=True ):
-        """
-        (Deprecated) Returns the parse results as XML. Tags are created for tokens and lists that have defined results names.
-        """
-        nl = "\n"
-        out = []
-        namedItems = dict((v[1],k) for (k,vlist) in self.__tokdict.items()
-                                                            for v in vlist)
-        nextLevelIndent = indent + "  "
-
-        # collapse out indents if formatting is not desired
-        if not formatted:
-            indent = ""
-            nextLevelIndent = ""
-            nl = ""
-
-        selfTag = None
-        if doctag is not None:
-            selfTag = doctag
-        else:
-            if self.__name:
-                selfTag = self.__name
-
-        if not selfTag:
-            if namedItemsOnly:
-                return ""
-            else:
-                selfTag = "ITEM"
-
-        out += [ nl, indent, "<", selfTag, ">" ]
-
-        for i,res in enumerate(self.__toklist):
-            if isinstance(res,ParseResults):
-                if i in namedItems:
-                    out += [ res.asXML(namedItems[i],
-                                        namedItemsOnly and doctag is None,
-                                        nextLevelIndent,
-                                        formatted)]
-                else:
-                    out += [ res.asXML(None,
-                                        namedItemsOnly and doctag is None,
-                                        nextLevelIndent,
-                                        formatted)]
-            else:
-                # individual token, see if there is a name for it
-                resTag = None
-                if i in namedItems:
-                    resTag = namedItems[i]
-                if not resTag:
-                    if namedItemsOnly:
-                        continue
-                    else:
-                        resTag = "ITEM"
-                xmlBodyText = _xml_escape(_ustr(res))
-                out += [ nl, nextLevelIndent, "<", resTag, ">",
-                                                xmlBodyText,
-                                                "" ]
-
-        out += [ nl, indent, "" ]
-        return "".join(out)
-
-    def __lookup(self,sub):
-        for k,vlist in self.__tokdict.items():
-            for v,loc in vlist:
-                if sub is v:
-                    return k
-        return None
-
-    def getName(self):
-        r"""
-        Returns the results name for this token expression. Useful when several 
-        different expressions might match at a particular location.
-
-        Example::
-            integer = Word(nums)
-            ssn_expr = Regex(r"\d\d\d-\d\d-\d\d\d\d")
-            house_number_expr = Suppress('#') + Word(nums, alphanums)
-            user_data = (Group(house_number_expr)("house_number") 
-                        | Group(ssn_expr)("ssn")
-                        | Group(integer)("age"))
-            user_info = OneOrMore(user_data)
-            
-            result = user_info.parseString("22 111-22-3333 #221B")
-            for item in result:
-                print(item.getName(), ':', item[0])
-        prints::
-            age : 22
-            ssn : 111-22-3333
-            house_number : 221B
-        """
-        if self.__name:
-            return self.__name
-        elif self.__parent:
-            par = self.__parent()
-            if par:
-                return par.__lookup(self)
-            else:
-                return None
-        elif (len(self) == 1 and
-               len(self.__tokdict) == 1 and
-               next(iter(self.__tokdict.values()))[0][1] in (0,-1)):
-            return next(iter(self.__tokdict.keys()))
-        else:
-            return None
-
-    def dump(self, indent='', depth=0, full=True):
-        """
-        Diagnostic method for listing out the contents of a C{ParseResults}.
-        Accepts an optional C{indent} argument so that this string can be embedded
-        in a nested display of other data.
-
-        Example::
-            integer = Word(nums)
-            date_str = integer("year") + '/' + integer("month") + '/' + integer("day")
-            
-            result = date_str.parseString('12/31/1999')
-            print(result.dump())
-        prints::
-            ['12', '/', '31', '/', '1999']
-            - day: 1999
-            - month: 31
-            - year: 12
-        """
-        out = []
-        NL = '\n'
-        out.append( indent+_ustr(self.asList()) )
-        if full:
-            if self.haskeys():
-                items = sorted((str(k), v) for k,v in self.items())
-                for k,v in items:
-                    if out:
-                        out.append(NL)
-                    out.append( "%s%s- %s: " % (indent,('  '*depth), k) )
-                    if isinstance(v,ParseResults):
-                        if v:
-                            out.append( v.dump(indent,depth+1) )
-                        else:
-                            out.append(_ustr(v))
-                    else:
-                        out.append(repr(v))
-            elif any(isinstance(vv,ParseResults) for vv in self):
-                v = self
-                for i,vv in enumerate(v):
-                    if isinstance(vv,ParseResults):
-                        out.append("\n%s%s[%d]:\n%s%s%s" % (indent,('  '*(depth)),i,indent,('  '*(depth+1)),vv.dump(indent,depth+1) ))
-                    else:
-                        out.append("\n%s%s[%d]:\n%s%s%s" % (indent,('  '*(depth)),i,indent,('  '*(depth+1)),_ustr(vv)))
-            
-        return "".join(out)
-
-    def pprint(self, *args, **kwargs):
-        """
-        Pretty-printer for parsed results as a list, using the C{pprint} module.
-        Accepts additional positional or keyword args as defined for the 
-        C{pprint.pprint} method. (U{http://docs.python.org/3/library/pprint.html#pprint.pprint})
-
-        Example::
-            ident = Word(alphas, alphanums)
-            num = Word(nums)
-            func = Forward()
-            term = ident | num | Group('(' + func + ')')
-            func <<= ident + Group(Optional(delimitedList(term)))
-            result = func.parseString("fna a,b,(fnb c,d,200),100")
-            result.pprint(width=40)
-        prints::
-            ['fna',
-             ['a',
-              'b',
-              ['(', 'fnb', ['c', 'd', '200'], ')'],
-              '100']]
-        """
-        pprint.pprint(self.asList(), *args, **kwargs)
-
-    # add support for pickle protocol
-    def __getstate__(self):
-        return ( self.__toklist,
-                 ( self.__tokdict.copy(),
-                   self.__parent is not None and self.__parent() or None,
-                   self.__accumNames,
-                   self.__name ) )
-
-    def __setstate__(self,state):
-        self.__toklist = state[0]
-        (self.__tokdict,
-         par,
-         inAccumNames,
-         self.__name) = state[1]
-        self.__accumNames = {}
-        self.__accumNames.update(inAccumNames)
-        if par is not None:
-            self.__parent = wkref(par)
-        else:
-            self.__parent = None
-
-    def __getnewargs__(self):
-        return self.__toklist, self.__name, self.__asList, self.__modal
-
-    def __dir__(self):
-        return (dir(type(self)) + list(self.keys()))
-
-MutableMapping.register(ParseResults)
-
-def col (loc,strg):
-    """Returns current column within a string, counting newlines as line separators.
-   The first column is number 1.
-
-   Note: the default parsing behavior is to expand tabs in the input string
-   before starting the parsing process.  See L{I{ParserElement.parseString}} for more information
-   on parsing strings containing C{}s, and suggested methods to maintain a
-   consistent view of the parsed string, the parse location, and line and column
-   positions within the parsed string.
-   """
-    s = strg
-    return 1 if 0} for more information
-   on parsing strings containing C{}s, and suggested methods to maintain a
-   consistent view of the parsed string, the parse location, and line and column
-   positions within the parsed string.
-   """
-    return strg.count("\n",0,loc) + 1
-
-def line( loc, strg ):
-    """Returns the line of text containing loc within a string, counting newlines as line separators.
-       """
-    lastCR = strg.rfind("\n", 0, loc)
-    nextCR = strg.find("\n", loc)
-    if nextCR >= 0:
-        return strg[lastCR+1:nextCR]
-    else:
-        return strg[lastCR+1:]
-
-def _defaultStartDebugAction( instring, loc, expr ):
-    print (("Match " + _ustr(expr) + " at loc " + _ustr(loc) + "(%d,%d)" % ( lineno(loc,instring), col(loc,instring) )))
-
-def _defaultSuccessDebugAction( instring, startloc, endloc, expr, toks ):
-    print ("Matched " + _ustr(expr) + " -> " + str(toks.asList()))
-
-def _defaultExceptionDebugAction( instring, loc, expr, exc ):
-    print ("Exception raised:" + _ustr(exc))
-
-def nullDebugAction(*args):
-    """'Do-nothing' debug action, to suppress debugging output during parsing."""
-    pass
-
-# Only works on Python 3.x - nonlocal is toxic to Python 2 installs
-#~ 'decorator to trim function calls to match the arity of the target'
-#~ def _trim_arity(func, maxargs=3):
-    #~ if func in singleArgBuiltins:
-        #~ return lambda s,l,t: func(t)
-    #~ limit = 0
-    #~ foundArity = False
-    #~ def wrapper(*args):
-        #~ nonlocal limit,foundArity
-        #~ while 1:
-            #~ try:
-                #~ ret = func(*args[limit:])
-                #~ foundArity = True
-                #~ return ret
-            #~ except TypeError:
-                #~ if limit == maxargs or foundArity:
-                    #~ raise
-                #~ limit += 1
-                #~ continue
-    #~ return wrapper
-
-# this version is Python 2.x-3.x cross-compatible
-'decorator to trim function calls to match the arity of the target'
-def _trim_arity(func, maxargs=2):
-    if func in singleArgBuiltins:
-        return lambda s,l,t: func(t)
-    limit = [0]
-    foundArity = [False]
-    
-    # traceback return data structure changed in Py3.5 - normalize back to plain tuples
-    if system_version[:2] >= (3,5):
-        def extract_stack(limit=0):
-            # special handling for Python 3.5.0 - extra deep call stack by 1
-            offset = -3 if system_version == (3,5,0) else -2
-            frame_summary = traceback.extract_stack(limit=-offset+limit-1)[offset]
-            return [frame_summary[:2]]
-        def extract_tb(tb, limit=0):
-            frames = traceback.extract_tb(tb, limit=limit)
-            frame_summary = frames[-1]
-            return [frame_summary[:2]]
-    else:
-        extract_stack = traceback.extract_stack
-        extract_tb = traceback.extract_tb
-    
-    # synthesize what would be returned by traceback.extract_stack at the call to 
-    # user's parse action 'func', so that we don't incur call penalty at parse time
-    
-    LINE_DIFF = 6
-    # IF ANY CODE CHANGES, EVEN JUST COMMENTS OR BLANK LINES, BETWEEN THE NEXT LINE AND 
-    # THE CALL TO FUNC INSIDE WRAPPER, LINE_DIFF MUST BE MODIFIED!!!!
-    this_line = extract_stack(limit=2)[-1]
-    pa_call_line_synth = (this_line[0], this_line[1]+LINE_DIFF)
-
-    def wrapper(*args):
-        while 1:
-            try:
-                ret = func(*args[limit[0]:])
-                foundArity[0] = True
-                return ret
-            except TypeError:
-                # re-raise TypeErrors if they did not come from our arity testing
-                if foundArity[0]:
-                    raise
-                else:
-                    try:
-                        tb = sys.exc_info()[-1]
-                        if not extract_tb(tb, limit=2)[-1][:2] == pa_call_line_synth:
-                            raise
-                    finally:
-                        del tb
-
-                if limit[0] <= maxargs:
-                    limit[0] += 1
-                    continue
-                raise
-
-    # copy func name to wrapper for sensible debug output
-    func_name = ""
-    try:
-        func_name = getattr(func, '__name__', 
-                            getattr(func, '__class__').__name__)
-    except Exception:
-        func_name = str(func)
-    wrapper.__name__ = func_name
-
-    return wrapper
-
-class ParserElement(object):
-    """Abstract base level parser element class."""
-    DEFAULT_WHITE_CHARS = " \n\t\r"
-    verbose_stacktrace = False
-
-    @staticmethod
-    def setDefaultWhitespaceChars( chars ):
-        r"""
-        Overrides the default whitespace chars
-
-        Example::
-            # default whitespace chars are space,  and newline
-            OneOrMore(Word(alphas)).parseString("abc def\nghi jkl")  # -> ['abc', 'def', 'ghi', 'jkl']
-            
-            # change to just treat newline as significant
-            ParserElement.setDefaultWhitespaceChars(" \t")
-            OneOrMore(Word(alphas)).parseString("abc def\nghi jkl")  # -> ['abc', 'def']
-        """
-        ParserElement.DEFAULT_WHITE_CHARS = chars
-
-    @staticmethod
-    def inlineLiteralsUsing(cls):
-        """
-        Set class to be used for inclusion of string literals into a parser.
-        
-        Example::
-            # default literal class used is Literal
-            integer = Word(nums)
-            date_str = integer("year") + '/' + integer("month") + '/' + integer("day")           
-
-            date_str.parseString("1999/12/31")  # -> ['1999', '/', '12', '/', '31']
-
-
-            # change to Suppress
-            ParserElement.inlineLiteralsUsing(Suppress)
-            date_str = integer("year") + '/' + integer("month") + '/' + integer("day")           
-
-            date_str.parseString("1999/12/31")  # -> ['1999', '12', '31']
-        """
-        ParserElement._literalStringClass = cls
-
-    def __init__( self, savelist=False ):
-        self.parseAction = list()
-        self.failAction = None
-        #~ self.name = ""  # don't define self.name, let subclasses try/except upcall
-        self.strRepr = None
-        self.resultsName = None
-        self.saveAsList = savelist
-        self.skipWhitespace = True
-        self.whiteChars = ParserElement.DEFAULT_WHITE_CHARS
-        self.copyDefaultWhiteChars = True
-        self.mayReturnEmpty = False # used when checking for left-recursion
-        self.keepTabs = False
-        self.ignoreExprs = list()
-        self.debug = False
-        self.streamlined = False
-        self.mayIndexError = True # used to optimize exception handling for subclasses that don't advance parse index
-        self.errmsg = ""
-        self.modalResults = True # used to mark results names as modal (report only last) or cumulative (list all)
-        self.debugActions = ( None, None, None ) #custom debug actions
-        self.re = None
-        self.callPreparse = True # used to avoid redundant calls to preParse
-        self.callDuringTry = False
-
-    def copy( self ):
-        """
-        Make a copy of this C{ParserElement}.  Useful for defining different parse actions
-        for the same parsing pattern, using copies of the original parse element.
-        
-        Example::
-            integer = Word(nums).setParseAction(lambda toks: int(toks[0]))
-            integerK = integer.copy().addParseAction(lambda toks: toks[0]*1024) + Suppress("K")
-            integerM = integer.copy().addParseAction(lambda toks: toks[0]*1024*1024) + Suppress("M")
-            
-            print(OneOrMore(integerK | integerM | integer).parseString("5K 100 640K 256M"))
-        prints::
-            [5120, 100, 655360, 268435456]
-        Equivalent form of C{expr.copy()} is just C{expr()}::
-            integerM = integer().addParseAction(lambda toks: toks[0]*1024*1024) + Suppress("M")
-        """
-        cpy = copy.copy( self )
-        cpy.parseAction = self.parseAction[:]
-        cpy.ignoreExprs = self.ignoreExprs[:]
-        if self.copyDefaultWhiteChars:
-            cpy.whiteChars = ParserElement.DEFAULT_WHITE_CHARS
-        return cpy
-
-    def setName( self, name ):
-        """
-        Define name for this expression, makes debugging and exception messages clearer.
-        
-        Example::
-            Word(nums).parseString("ABC")  # -> Exception: Expected W:(0123...) (at char 0), (line:1, col:1)
-            Word(nums).setName("integer").parseString("ABC")  # -> Exception: Expected integer (at char 0), (line:1, col:1)
-        """
-        self.name = name
-        self.errmsg = "Expected " + self.name
-        if hasattr(self,"exception"):
-            self.exception.msg = self.errmsg
-        return self
-
-    def setResultsName( self, name, listAllMatches=False ):
-        """
-        Define name for referencing matching tokens as a nested attribute
-        of the returned parse results.
-        NOTE: this returns a *copy* of the original C{ParserElement} object;
-        this is so that the client can define a basic element, such as an
-        integer, and reference it in multiple places with different names.
-
-        You can also set results names using the abbreviated syntax,
-        C{expr("name")} in place of C{expr.setResultsName("name")} - 
-        see L{I{__call__}<__call__>}.
-
-        Example::
-            date_str = (integer.setResultsName("year") + '/' 
-                        + integer.setResultsName("month") + '/' 
-                        + integer.setResultsName("day"))
-
-            # equivalent form:
-            date_str = integer("year") + '/' + integer("month") + '/' + integer("day")
-        """
-        newself = self.copy()
-        if name.endswith("*"):
-            name = name[:-1]
-            listAllMatches=True
-        newself.resultsName = name
-        newself.modalResults = not listAllMatches
-        return newself
-
-    def setBreak(self,breakFlag = True):
-        """Method to invoke the Python pdb debugger when this element is
-           about to be parsed. Set C{breakFlag} to True to enable, False to
-           disable.
-        """
-        if breakFlag:
-            _parseMethod = self._parse
-            def breaker(instring, loc, doActions=True, callPreParse=True):
-                import pdb
-                pdb.set_trace()
-                return _parseMethod( instring, loc, doActions, callPreParse )
-            breaker._originalParseMethod = _parseMethod
-            self._parse = breaker
-        else:
-            if hasattr(self._parse,"_originalParseMethod"):
-                self._parse = self._parse._originalParseMethod
-        return self
-
-    def setParseAction( self, *fns, **kwargs ):
-        """
-        Define one or more actions to perform when successfully matching parse element definition.
-        Parse action fn is a callable method with 0-3 arguments, called as C{fn(s,loc,toks)},
-        C{fn(loc,toks)}, C{fn(toks)}, or just C{fn()}, where:
-         - s   = the original string being parsed (see note below)
-         - loc = the location of the matching substring
-         - toks = a list of the matched tokens, packaged as a C{L{ParseResults}} object
-        If the functions in fns modify the tokens, they can return them as the return
-        value from fn, and the modified list of tokens will replace the original.
-        Otherwise, fn does not need to return any value.
-
-        Optional keyword arguments:
-         - callDuringTry = (default=C{False}) indicate if parse action should be run during lookaheads and alternate testing
-
-        Note: the default parsing behavior is to expand tabs in the input string
-        before starting the parsing process.  See L{I{parseString}} for more information
-        on parsing strings containing C{}s, and suggested methods to maintain a
-        consistent view of the parsed string, the parse location, and line and column
-        positions within the parsed string.
-        
-        Example::
-            integer = Word(nums)
-            date_str = integer + '/' + integer + '/' + integer
-
-            date_str.parseString("1999/12/31")  # -> ['1999', '/', '12', '/', '31']
-
-            # use parse action to convert to ints at parse time
-            integer = Word(nums).setParseAction(lambda toks: int(toks[0]))
-            date_str = integer + '/' + integer + '/' + integer
-
-            # note that integer fields are now ints, not strings
-            date_str.parseString("1999/12/31")  # -> [1999, '/', 12, '/', 31]
-        """
-        self.parseAction = list(map(_trim_arity, list(fns)))
-        self.callDuringTry = kwargs.get("callDuringTry", False)
-        return self
-
-    def addParseAction( self, *fns, **kwargs ):
-        """
-        Add one or more parse actions to expression's list of parse actions. See L{I{setParseAction}}.
-        
-        See examples in L{I{copy}}.
-        """
-        self.parseAction += list(map(_trim_arity, list(fns)))
-        self.callDuringTry = self.callDuringTry or kwargs.get("callDuringTry", False)
-        return self
-
-    def addCondition(self, *fns, **kwargs):
-        """Add a boolean predicate function to expression's list of parse actions. See 
-        L{I{setParseAction}} for function call signatures. Unlike C{setParseAction}, 
-        functions passed to C{addCondition} need to return boolean success/fail of the condition.
-
-        Optional keyword arguments:
-         - message = define a custom message to be used in the raised exception
-         - fatal   = if True, will raise ParseFatalException to stop parsing immediately; otherwise will raise ParseException
-         
-        Example::
-            integer = Word(nums).setParseAction(lambda toks: int(toks[0]))
-            year_int = integer.copy()
-            year_int.addCondition(lambda toks: toks[0] >= 2000, message="Only support years 2000 and later")
-            date_str = year_int + '/' + integer + '/' + integer
-
-            result = date_str.parseString("1999/12/31")  # -> Exception: Only support years 2000 and later (at char 0), (line:1, col:1)
-        """
-        msg = kwargs.get("message", "failed user-defined condition")
-        exc_type = ParseFatalException if kwargs.get("fatal", False) else ParseException
-        for fn in fns:
-            def pa(s,l,t):
-                if not bool(_trim_arity(fn)(s,l,t)):
-                    raise exc_type(s,l,msg)
-            self.parseAction.append(pa)
-        self.callDuringTry = self.callDuringTry or kwargs.get("callDuringTry", False)
-        return self
-
-    def setFailAction( self, fn ):
-        """Define action to perform if parsing fails at this expression.
-           Fail acton fn is a callable function that takes the arguments
-           C{fn(s,loc,expr,err)} where:
-            - s = string being parsed
-            - loc = location where expression match was attempted and failed
-            - expr = the parse expression that failed
-            - err = the exception thrown
-           The function returns no value.  It may throw C{L{ParseFatalException}}
-           if it is desired to stop parsing immediately."""
-        self.failAction = fn
-        return self
-
-    def _skipIgnorables( self, instring, loc ):
-        exprsFound = True
-        while exprsFound:
-            exprsFound = False
-            for e in self.ignoreExprs:
-                try:
-                    while 1:
-                        loc,dummy = e._parse( instring, loc )
-                        exprsFound = True
-                except ParseException:
-                    pass
-        return loc
-
-    def preParse( self, instring, loc ):
-        if self.ignoreExprs:
-            loc = self._skipIgnorables( instring, loc )
-
-        if self.skipWhitespace:
-            wt = self.whiteChars
-            instrlen = len(instring)
-            while loc < instrlen and instring[loc] in wt:
-                loc += 1
-
-        return loc
-
-    def parseImpl( self, instring, loc, doActions=True ):
-        return loc, []
-
-    def postParse( self, instring, loc, tokenlist ):
-        return tokenlist
-
-    #~ @profile
-    def _parseNoCache( self, instring, loc, doActions=True, callPreParse=True ):
-        debugging = ( self.debug ) #and doActions )
-
-        if debugging or self.failAction:
-            #~ print ("Match",self,"at loc",loc,"(%d,%d)" % ( lineno(loc,instring), col(loc,instring) ))
-            if (self.debugActions[0] ):
-                self.debugActions[0]( instring, loc, self )
-            if callPreParse and self.callPreparse:
-                preloc = self.preParse( instring, loc )
-            else:
-                preloc = loc
-            tokensStart = preloc
-            try:
-                try:
-                    loc,tokens = self.parseImpl( instring, preloc, doActions )
-                except IndexError:
-                    raise ParseException( instring, len(instring), self.errmsg, self )
-            except ParseBaseException as err:
-                #~ print ("Exception raised:", err)
-                if self.debugActions[2]:
-                    self.debugActions[2]( instring, tokensStart, self, err )
-                if self.failAction:
-                    self.failAction( instring, tokensStart, self, err )
-                raise
-        else:
-            if callPreParse and self.callPreparse:
-                preloc = self.preParse( instring, loc )
-            else:
-                preloc = loc
-            tokensStart = preloc
-            if self.mayIndexError or preloc >= len(instring):
-                try:
-                    loc,tokens = self.parseImpl( instring, preloc, doActions )
-                except IndexError:
-                    raise ParseException( instring, len(instring), self.errmsg, self )
-            else:
-                loc,tokens = self.parseImpl( instring, preloc, doActions )
-
-        tokens = self.postParse( instring, loc, tokens )
-
-        retTokens = ParseResults( tokens, self.resultsName, asList=self.saveAsList, modal=self.modalResults )
-        if self.parseAction and (doActions or self.callDuringTry):
-            if debugging:
-                try:
-                    for fn in self.parseAction:
-                        tokens = fn( instring, tokensStart, retTokens )
-                        if tokens is not None:
-                            retTokens = ParseResults( tokens,
-                                                      self.resultsName,
-                                                      asList=self.saveAsList and isinstance(tokens,(ParseResults,list)),
-                                                      modal=self.modalResults )
-                except ParseBaseException as err:
-                    #~ print "Exception raised in user parse action:", err
-                    if (self.debugActions[2] ):
-                        self.debugActions[2]( instring, tokensStart, self, err )
-                    raise
-            else:
-                for fn in self.parseAction:
-                    tokens = fn( instring, tokensStart, retTokens )
-                    if tokens is not None:
-                        retTokens = ParseResults( tokens,
-                                                  self.resultsName,
-                                                  asList=self.saveAsList and isinstance(tokens,(ParseResults,list)),
-                                                  modal=self.modalResults )
-        if debugging:
-            #~ print ("Matched",self,"->",retTokens.asList())
-            if (self.debugActions[1] ):
-                self.debugActions[1]( instring, tokensStart, loc, self, retTokens )
-
-        return loc, retTokens
-
-    def tryParse( self, instring, loc ):
-        try:
-            return self._parse( instring, loc, doActions=False )[0]
-        except ParseFatalException:
-            raise ParseException( instring, loc, self.errmsg, self)
-    
-    def canParseNext(self, instring, loc):
-        try:
-            self.tryParse(instring, loc)
-        except (ParseException, IndexError):
-            return False
-        else:
-            return True
-
-    class _UnboundedCache(object):
-        def __init__(self):
-            cache = {}
-            self.not_in_cache = not_in_cache = object()
-
-            def get(self, key):
-                return cache.get(key, not_in_cache)
-
-            def set(self, key, value):
-                cache[key] = value
-
-            def clear(self):
-                cache.clear()
-                
-            def cache_len(self):
-                return len(cache)
-
-            self.get = types.MethodType(get, self)
-            self.set = types.MethodType(set, self)
-            self.clear = types.MethodType(clear, self)
-            self.__len__ = types.MethodType(cache_len, self)
-
-    if _OrderedDict is not None:
-        class _FifoCache(object):
-            def __init__(self, size):
-                self.not_in_cache = not_in_cache = object()
-
-                cache = _OrderedDict()
-
-                def get(self, key):
-                    return cache.get(key, not_in_cache)
-
-                def set(self, key, value):
-                    cache[key] = value
-                    while len(cache) > size:
-                        try:
-                            cache.popitem(False)
-                        except KeyError:
-                            pass
-
-                def clear(self):
-                    cache.clear()
-
-                def cache_len(self):
-                    return len(cache)
-
-                self.get = types.MethodType(get, self)
-                self.set = types.MethodType(set, self)
-                self.clear = types.MethodType(clear, self)
-                self.__len__ = types.MethodType(cache_len, self)
-
-    else:
-        class _FifoCache(object):
-            def __init__(self, size):
-                self.not_in_cache = not_in_cache = object()
-
-                cache = {}
-                key_fifo = collections.deque([], size)
-
-                def get(self, key):
-                    return cache.get(key, not_in_cache)
-
-                def set(self, key, value):
-                    cache[key] = value
-                    while len(key_fifo) > size:
-                        cache.pop(key_fifo.popleft(), None)
-                    key_fifo.append(key)
-
-                def clear(self):
-                    cache.clear()
-                    key_fifo.clear()
-
-                def cache_len(self):
-                    return len(cache)
-
-                self.get = types.MethodType(get, self)
-                self.set = types.MethodType(set, self)
-                self.clear = types.MethodType(clear, self)
-                self.__len__ = types.MethodType(cache_len, self)
-
-    # argument cache for optimizing repeated calls when backtracking through recursive expressions
-    packrat_cache = {} # this is set later by enabledPackrat(); this is here so that resetCache() doesn't fail
-    packrat_cache_lock = RLock()
-    packrat_cache_stats = [0, 0]
-
-    # this method gets repeatedly called during backtracking with the same arguments -
-    # we can cache these arguments and save ourselves the trouble of re-parsing the contained expression
-    def _parseCache( self, instring, loc, doActions=True, callPreParse=True ):
-        HIT, MISS = 0, 1
-        lookup = (self, instring, loc, callPreParse, doActions)
-        with ParserElement.packrat_cache_lock:
-            cache = ParserElement.packrat_cache
-            value = cache.get(lookup)
-            if value is cache.not_in_cache:
-                ParserElement.packrat_cache_stats[MISS] += 1
-                try:
-                    value = self._parseNoCache(instring, loc, doActions, callPreParse)
-                except ParseBaseException as pe:
-                    # cache a copy of the exception, without the traceback
-                    cache.set(lookup, pe.__class__(*pe.args))
-                    raise
-                else:
-                    cache.set(lookup, (value[0], value[1].copy()))
-                    return value
-            else:
-                ParserElement.packrat_cache_stats[HIT] += 1
-                if isinstance(value, Exception):
-                    raise value
-                return (value[0], value[1].copy())
-
-    _parse = _parseNoCache
-
-    @staticmethod
-    def resetCache():
-        ParserElement.packrat_cache.clear()
-        ParserElement.packrat_cache_stats[:] = [0] * len(ParserElement.packrat_cache_stats)
-
-    _packratEnabled = False
-    @staticmethod
-    def enablePackrat(cache_size_limit=128):
-        """Enables "packrat" parsing, which adds memoizing to the parsing logic.
-           Repeated parse attempts at the same string location (which happens
-           often in many complex grammars) can immediately return a cached value,
-           instead of re-executing parsing/validating code.  Memoizing is done of
-           both valid results and parsing exceptions.
-           
-           Parameters:
-            - cache_size_limit - (default=C{128}) - if an integer value is provided
-              will limit the size of the packrat cache; if None is passed, then
-              the cache size will be unbounded; if 0 is passed, the cache will
-              be effectively disabled.
-            
-           This speedup may break existing programs that use parse actions that
-           have side-effects.  For this reason, packrat parsing is disabled when
-           you first import pyparsing.  To activate the packrat feature, your
-           program must call the class method C{ParserElement.enablePackrat()}.  If
-           your program uses C{psyco} to "compile as you go", you must call
-           C{enablePackrat} before calling C{psyco.full()}.  If you do not do this,
-           Python will crash.  For best results, call C{enablePackrat()} immediately
-           after importing pyparsing.
-           
-           Example::
-               import pyparsing
-               pyparsing.ParserElement.enablePackrat()
-        """
-        if not ParserElement._packratEnabled:
-            ParserElement._packratEnabled = True
-            if cache_size_limit is None:
-                ParserElement.packrat_cache = ParserElement._UnboundedCache()
-            else:
-                ParserElement.packrat_cache = ParserElement._FifoCache(cache_size_limit)
-            ParserElement._parse = ParserElement._parseCache
-
-    def parseString( self, instring, parseAll=False ):
-        """
-        Execute the parse expression with the given string.
-        This is the main interface to the client code, once the complete
-        expression has been built.
-
-        If you want the grammar to require that the entire input string be
-        successfully parsed, then set C{parseAll} to True (equivalent to ending
-        the grammar with C{L{StringEnd()}}).
-
-        Note: C{parseString} implicitly calls C{expandtabs()} on the input string,
-        in order to report proper column numbers in parse actions.
-        If the input string contains tabs and
-        the grammar uses parse actions that use the C{loc} argument to index into the
-        string being parsed, you can ensure you have a consistent view of the input
-        string by:
-         - calling C{parseWithTabs} on your grammar before calling C{parseString}
-           (see L{I{parseWithTabs}})
-         - define your parse action using the full C{(s,loc,toks)} signature, and
-           reference the input string using the parse action's C{s} argument
-         - explictly expand the tabs in your input string before calling
-           C{parseString}
-        
-        Example::
-            Word('a').parseString('aaaaabaaa')  # -> ['aaaaa']
-            Word('a').parseString('aaaaabaaa', parseAll=True)  # -> Exception: Expected end of text
-        """
-        ParserElement.resetCache()
-        if not self.streamlined:
-            self.streamline()
-            #~ self.saveAsList = True
-        for e in self.ignoreExprs:
-            e.streamline()
-        if not self.keepTabs:
-            instring = instring.expandtabs()
-        try:
-            loc, tokens = self._parse( instring, 0 )
-            if parseAll:
-                loc = self.preParse( instring, loc )
-                se = Empty() + StringEnd()
-                se._parse( instring, loc )
-        except ParseBaseException as exc:
-            if ParserElement.verbose_stacktrace:
-                raise
-            else:
-                # catch and re-raise exception from here, clears out pyparsing internal stack trace
-                raise exc
-        else:
-            return tokens
-
-    def scanString( self, instring, maxMatches=_MAX_INT, overlap=False ):
-        """
-        Scan the input string for expression matches.  Each match will return the
-        matching tokens, start location, and end location.  May be called with optional
-        C{maxMatches} argument, to clip scanning after 'n' matches are found.  If
-        C{overlap} is specified, then overlapping matches will be reported.
-
-        Note that the start and end locations are reported relative to the string
-        being parsed.  See L{I{parseString}} for more information on parsing
-        strings with embedded tabs.
-
-        Example::
-            source = "sldjf123lsdjjkf345sldkjf879lkjsfd987"
-            print(source)
-            for tokens,start,end in Word(alphas).scanString(source):
-                print(' '*start + '^'*(end-start))
-                print(' '*start + tokens[0])
-        
-        prints::
-        
-            sldjf123lsdjjkf345sldkjf879lkjsfd987
-            ^^^^^
-            sldjf
-                    ^^^^^^^
-                    lsdjjkf
-                              ^^^^^^
-                              sldkjf
-                                       ^^^^^^
-                                       lkjsfd
-        """
-        if not self.streamlined:
-            self.streamline()
-        for e in self.ignoreExprs:
-            e.streamline()
-
-        if not self.keepTabs:
-            instring = _ustr(instring).expandtabs()
-        instrlen = len(instring)
-        loc = 0
-        preparseFn = self.preParse
-        parseFn = self._parse
-        ParserElement.resetCache()
-        matches = 0
-        try:
-            while loc <= instrlen and matches < maxMatches:
-                try:
-                    preloc = preparseFn( instring, loc )
-                    nextLoc,tokens = parseFn( instring, preloc, callPreParse=False )
-                except ParseException:
-                    loc = preloc+1
-                else:
-                    if nextLoc > loc:
-                        matches += 1
-                        yield tokens, preloc, nextLoc
-                        if overlap:
-                            nextloc = preparseFn( instring, loc )
-                            if nextloc > loc:
-                                loc = nextLoc
-                            else:
-                                loc += 1
-                        else:
-                            loc = nextLoc
-                    else:
-                        loc = preloc+1
-        except ParseBaseException as exc:
-            if ParserElement.verbose_stacktrace:
-                raise
-            else:
-                # catch and re-raise exception from here, clears out pyparsing internal stack trace
-                raise exc
-
-    def transformString( self, instring ):
-        """
-        Extension to C{L{scanString}}, to modify matching text with modified tokens that may
-        be returned from a parse action.  To use C{transformString}, define a grammar and
-        attach a parse action to it that modifies the returned token list.
-        Invoking C{transformString()} on a target string will then scan for matches,
-        and replace the matched text patterns according to the logic in the parse
-        action.  C{transformString()} returns the resulting transformed string.
-        
-        Example::
-            wd = Word(alphas)
-            wd.setParseAction(lambda toks: toks[0].title())
-            
-            print(wd.transformString("now is the winter of our discontent made glorious summer by this sun of york."))
-        Prints::
-            Now Is The Winter Of Our Discontent Made Glorious Summer By This Sun Of York.
-        """
-        out = []
-        lastE = 0
-        # force preservation of s, to minimize unwanted transformation of string, and to
-        # keep string locs straight between transformString and scanString
-        self.keepTabs = True
-        try:
-            for t,s,e in self.scanString( instring ):
-                out.append( instring[lastE:s] )
-                if t:
-                    if isinstance(t,ParseResults):
-                        out += t.asList()
-                    elif isinstance(t,list):
-                        out += t
-                    else:
-                        out.append(t)
-                lastE = e
-            out.append(instring[lastE:])
-            out = [o for o in out if o]
-            return "".join(map(_ustr,_flatten(out)))
-        except ParseBaseException as exc:
-            if ParserElement.verbose_stacktrace:
-                raise
-            else:
-                # catch and re-raise exception from here, clears out pyparsing internal stack trace
-                raise exc
-
-    def searchString( self, instring, maxMatches=_MAX_INT ):
-        """
-        Another extension to C{L{scanString}}, simplifying the access to the tokens found
-        to match the given parse expression.  May be called with optional
-        C{maxMatches} argument, to clip searching after 'n' matches are found.
-        
-        Example::
-            # a capitalized word starts with an uppercase letter, followed by zero or more lowercase letters
-            cap_word = Word(alphas.upper(), alphas.lower())
-            
-            print(cap_word.searchString("More than Iron, more than Lead, more than Gold I need Electricity"))
-
-            # the sum() builtin can be used to merge results into a single ParseResults object
-            print(sum(cap_word.searchString("More than Iron, more than Lead, more than Gold I need Electricity")))
-        prints::
-            [['More'], ['Iron'], ['Lead'], ['Gold'], ['I'], ['Electricity']]
-            ['More', 'Iron', 'Lead', 'Gold', 'I', 'Electricity']
-        """
-        try:
-            return ParseResults([ t for t,s,e in self.scanString( instring, maxMatches ) ])
-        except ParseBaseException as exc:
-            if ParserElement.verbose_stacktrace:
-                raise
-            else:
-                # catch and re-raise exception from here, clears out pyparsing internal stack trace
-                raise exc
-
-    def split(self, instring, maxsplit=_MAX_INT, includeSeparators=False):
-        """
-        Generator method to split a string using the given expression as a separator.
-        May be called with optional C{maxsplit} argument, to limit the number of splits;
-        and the optional C{includeSeparators} argument (default=C{False}), if the separating
-        matching text should be included in the split results.
-        
-        Example::        
-            punc = oneOf(list(".,;:/-!?"))
-            print(list(punc.split("This, this?, this sentence, is badly punctuated!")))
-        prints::
-            ['This', ' this', '', ' this sentence', ' is badly punctuated', '']
-        """
-        splits = 0
-        last = 0
-        for t,s,e in self.scanString(instring, maxMatches=maxsplit):
-            yield instring[last:s]
-            if includeSeparators:
-                yield t[0]
-            last = e
-        yield instring[last:]
-
-    def __add__(self, other ):
-        """
-        Implementation of + operator - returns C{L{And}}. Adding strings to a ParserElement
-        converts them to L{Literal}s by default.
-        
-        Example::
-            greet = Word(alphas) + "," + Word(alphas) + "!"
-            hello = "Hello, World!"
-            print (hello, "->", greet.parseString(hello))
-        Prints::
-            Hello, World! -> ['Hello', ',', 'World', '!']
-        """
-        if isinstance( other, basestring ):
-            other = ParserElement._literalStringClass( other )
-        if not isinstance( other, ParserElement ):
-            warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
-                    SyntaxWarning, stacklevel=2)
-            return None
-        return And( [ self, other ] )
-
-    def __radd__(self, other ):
-        """
-        Implementation of + operator when left operand is not a C{L{ParserElement}}
-        """
-        if isinstance( other, basestring ):
-            other = ParserElement._literalStringClass( other )
-        if not isinstance( other, ParserElement ):
-            warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
-                    SyntaxWarning, stacklevel=2)
-            return None
-        return other + self
-
-    def __sub__(self, other):
-        """
-        Implementation of - operator, returns C{L{And}} with error stop
-        """
-        if isinstance( other, basestring ):
-            other = ParserElement._literalStringClass( other )
-        if not isinstance( other, ParserElement ):
-            warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
-                    SyntaxWarning, stacklevel=2)
-            return None
-        return self + And._ErrorStop() + other
-
-    def __rsub__(self, other ):
-        """
-        Implementation of - operator when left operand is not a C{L{ParserElement}}
-        """
-        if isinstance( other, basestring ):
-            other = ParserElement._literalStringClass( other )
-        if not isinstance( other, ParserElement ):
-            warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
-                    SyntaxWarning, stacklevel=2)
-            return None
-        return other - self
-
-    def __mul__(self,other):
-        """
-        Implementation of * operator, allows use of C{expr * 3} in place of
-        C{expr + expr + expr}.  Expressions may also me multiplied by a 2-integer
-        tuple, similar to C{{min,max}} multipliers in regular expressions.  Tuples
-        may also include C{None} as in:
-         - C{expr*(n,None)} or C{expr*(n,)} is equivalent
-              to C{expr*n + L{ZeroOrMore}(expr)}
-              (read as "at least n instances of C{expr}")
-         - C{expr*(None,n)} is equivalent to C{expr*(0,n)}
-              (read as "0 to n instances of C{expr}")
-         - C{expr*(None,None)} is equivalent to C{L{ZeroOrMore}(expr)}
-         - C{expr*(1,None)} is equivalent to C{L{OneOrMore}(expr)}
-
-        Note that C{expr*(None,n)} does not raise an exception if
-        more than n exprs exist in the input stream; that is,
-        C{expr*(None,n)} does not enforce a maximum number of expr
-        occurrences.  If this behavior is desired, then write
-        C{expr*(None,n) + ~expr}
-        """
-        if isinstance(other,int):
-            minElements, optElements = other,0
-        elif isinstance(other,tuple):
-            other = (other + (None, None))[:2]
-            if other[0] is None:
-                other = (0, other[1])
-            if isinstance(other[0],int) and other[1] is None:
-                if other[0] == 0:
-                    return ZeroOrMore(self)
-                if other[0] == 1:
-                    return OneOrMore(self)
-                else:
-                    return self*other[0] + ZeroOrMore(self)
-            elif isinstance(other[0],int) and isinstance(other[1],int):
-                minElements, optElements = other
-                optElements -= minElements
-            else:
-                raise TypeError("cannot multiply 'ParserElement' and ('%s','%s') objects", type(other[0]),type(other[1]))
-        else:
-            raise TypeError("cannot multiply 'ParserElement' and '%s' objects", type(other))
-
-        if minElements < 0:
-            raise ValueError("cannot multiply ParserElement by negative value")
-        if optElements < 0:
-            raise ValueError("second tuple value must be greater or equal to first tuple value")
-        if minElements == optElements == 0:
-            raise ValueError("cannot multiply ParserElement by 0 or (0,0)")
-
-        if (optElements):
-            def makeOptionalList(n):
-                if n>1:
-                    return Optional(self + makeOptionalList(n-1))
-                else:
-                    return Optional(self)
-            if minElements:
-                if minElements == 1:
-                    ret = self + makeOptionalList(optElements)
-                else:
-                    ret = And([self]*minElements) + makeOptionalList(optElements)
-            else:
-                ret = makeOptionalList(optElements)
-        else:
-            if minElements == 1:
-                ret = self
-            else:
-                ret = And([self]*minElements)
-        return ret
-
-    def __rmul__(self, other):
-        return self.__mul__(other)
-
-    def __or__(self, other ):
-        """
-        Implementation of | operator - returns C{L{MatchFirst}}
-        """
-        if isinstance( other, basestring ):
-            other = ParserElement._literalStringClass( other )
-        if not isinstance( other, ParserElement ):
-            warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
-                    SyntaxWarning, stacklevel=2)
-            return None
-        return MatchFirst( [ self, other ] )
-
-    def __ror__(self, other ):
-        """
-        Implementation of | operator when left operand is not a C{L{ParserElement}}
-        """
-        if isinstance( other, basestring ):
-            other = ParserElement._literalStringClass( other )
-        if not isinstance( other, ParserElement ):
-            warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
-                    SyntaxWarning, stacklevel=2)
-            return None
-        return other | self
-
-    def __xor__(self, other ):
-        """
-        Implementation of ^ operator - returns C{L{Or}}
-        """
-        if isinstance( other, basestring ):
-            other = ParserElement._literalStringClass( other )
-        if not isinstance( other, ParserElement ):
-            warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
-                    SyntaxWarning, stacklevel=2)
-            return None
-        return Or( [ self, other ] )
-
-    def __rxor__(self, other ):
-        """
-        Implementation of ^ operator when left operand is not a C{L{ParserElement}}
-        """
-        if isinstance( other, basestring ):
-            other = ParserElement._literalStringClass( other )
-        if not isinstance( other, ParserElement ):
-            warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
-                    SyntaxWarning, stacklevel=2)
-            return None
-        return other ^ self
-
-    def __and__(self, other ):
-        """
-        Implementation of & operator - returns C{L{Each}}
-        """
-        if isinstance( other, basestring ):
-            other = ParserElement._literalStringClass( other )
-        if not isinstance( other, ParserElement ):
-            warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
-                    SyntaxWarning, stacklevel=2)
-            return None
-        return Each( [ self, other ] )
-
-    def __rand__(self, other ):
-        """
-        Implementation of & operator when left operand is not a C{L{ParserElement}}
-        """
-        if isinstance( other, basestring ):
-            other = ParserElement._literalStringClass( other )
-        if not isinstance( other, ParserElement ):
-            warnings.warn("Cannot combine element of type %s with ParserElement" % type(other),
-                    SyntaxWarning, stacklevel=2)
-            return None
-        return other & self
-
-    def __invert__( self ):
-        """
-        Implementation of ~ operator - returns C{L{NotAny}}
-        """
-        return NotAny( self )
-
-    def __call__(self, name=None):
-        """
-        Shortcut for C{L{setResultsName}}, with C{listAllMatches=False}.
-        
-        If C{name} is given with a trailing C{'*'} character, then C{listAllMatches} will be
-        passed as C{True}.
-           
-        If C{name} is omitted, same as calling C{L{copy}}.
-
-        Example::
-            # these are equivalent
-            userdata = Word(alphas).setResultsName("name") + Word(nums+"-").setResultsName("socsecno")
-            userdata = Word(alphas)("name") + Word(nums+"-")("socsecno")             
-        """
-        if name is not None:
-            return self.setResultsName(name)
-        else:
-            return self.copy()
-
-    def suppress( self ):
-        """
-        Suppresses the output of this C{ParserElement}; useful to keep punctuation from
-        cluttering up returned output.
-        """
-        return Suppress( self )
-
-    def leaveWhitespace( self ):
-        """
-        Disables the skipping of whitespace before matching the characters in the
-        C{ParserElement}'s defined pattern.  This is normally only used internally by
-        the pyparsing module, but may be needed in some whitespace-sensitive grammars.
-        """
-        self.skipWhitespace = False
-        return self
-
-    def setWhitespaceChars( self, chars ):
-        """
-        Overrides the default whitespace chars
-        """
-        self.skipWhitespace = True
-        self.whiteChars = chars
-        self.copyDefaultWhiteChars = False
-        return self
-
-    def parseWithTabs( self ):
-        """
-        Overrides default behavior to expand C{}s to spaces before parsing the input string.
-        Must be called before C{parseString} when the input grammar contains elements that
-        match C{} characters.
-        """
-        self.keepTabs = True
-        return self
-
-    def ignore( self, other ):
-        """
-        Define expression to be ignored (e.g., comments) while doing pattern
-        matching; may be called repeatedly, to define multiple comment or other
-        ignorable patterns.
-        
-        Example::
-            patt = OneOrMore(Word(alphas))
-            patt.parseString('ablaj /* comment */ lskjd') # -> ['ablaj']
-            
-            patt.ignore(cStyleComment)
-            patt.parseString('ablaj /* comment */ lskjd') # -> ['ablaj', 'lskjd']
-        """
-        if isinstance(other, basestring):
-            other = Suppress(other)
-
-        if isinstance( other, Suppress ):
-            if other not in self.ignoreExprs:
-                self.ignoreExprs.append(other)
-        else:
-            self.ignoreExprs.append( Suppress( other.copy() ) )
-        return self
-
-    def setDebugActions( self, startAction, successAction, exceptionAction ):
-        """
-        Enable display of debugging messages while doing pattern matching.
-        """
-        self.debugActions = (startAction or _defaultStartDebugAction,
-                             successAction or _defaultSuccessDebugAction,
-                             exceptionAction or _defaultExceptionDebugAction)
-        self.debug = True
-        return self
-
-    def setDebug( self, flag=True ):
-        """
-        Enable display of debugging messages while doing pattern matching.
-        Set C{flag} to True to enable, False to disable.
-
-        Example::
-            wd = Word(alphas).setName("alphaword")
-            integer = Word(nums).setName("numword")
-            term = wd | integer
-            
-            # turn on debugging for wd
-            wd.setDebug()
-
-            OneOrMore(term).parseString("abc 123 xyz 890")
-        
-        prints::
-            Match alphaword at loc 0(1,1)
-            Matched alphaword -> ['abc']
-            Match alphaword at loc 3(1,4)
-            Exception raised:Expected alphaword (at char 4), (line:1, col:5)
-            Match alphaword at loc 7(1,8)
-            Matched alphaword -> ['xyz']
-            Match alphaword at loc 11(1,12)
-            Exception raised:Expected alphaword (at char 12), (line:1, col:13)
-            Match alphaword at loc 15(1,16)
-            Exception raised:Expected alphaword (at char 15), (line:1, col:16)
-
-        The output shown is that produced by the default debug actions - custom debug actions can be
-        specified using L{setDebugActions}. Prior to attempting
-        to match the C{wd} expression, the debugging message C{"Match  at loc (,)"}
-        is shown. Then if the parse succeeds, a C{"Matched"} message is shown, or an C{"Exception raised"}
-        message is shown. Also note the use of L{setName} to assign a human-readable name to the expression,
-        which makes debugging and exception messages easier to understand - for instance, the default
-        name created for the C{Word} expression without calling C{setName} is C{"W:(ABCD...)"}.
-        """
-        if flag:
-            self.setDebugActions( _defaultStartDebugAction, _defaultSuccessDebugAction, _defaultExceptionDebugAction )
-        else:
-            self.debug = False
-        return self
-
-    def __str__( self ):
-        return self.name
-
-    def __repr__( self ):
-        return _ustr(self)
-
-    def streamline( self ):
-        self.streamlined = True
-        self.strRepr = None
-        return self
-
-    def checkRecursion( self, parseElementList ):
-        pass
-
-    def validate( self, validateTrace=[] ):
-        """
-        Check defined expressions for valid structure, check for infinite recursive definitions.
-        """
-        self.checkRecursion( [] )
-
-    def parseFile( self, file_or_filename, parseAll=False ):
-        """
-        Execute the parse expression on the given file or filename.
-        If a filename is specified (instead of a file object),
-        the entire file is opened, read, and closed before parsing.
-        """
-        try:
-            file_contents = file_or_filename.read()
-        except AttributeError:
-            with open(file_or_filename, "r") as f:
-                file_contents = f.read()
-        try:
-            return self.parseString(file_contents, parseAll)
-        except ParseBaseException as exc:
-            if ParserElement.verbose_stacktrace:
-                raise
-            else:
-                # catch and re-raise exception from here, clears out pyparsing internal stack trace
-                raise exc
-
-    def __eq__(self,other):
-        if isinstance(other, ParserElement):
-            return self is other or vars(self) == vars(other)
-        elif isinstance(other, basestring):
-            return self.matches(other)
-        else:
-            return super(ParserElement,self)==other
-
-    def __ne__(self,other):
-        return not (self == other)
-
-    def __hash__(self):
-        return hash(id(self))
-
-    def __req__(self,other):
-        return self == other
-
-    def __rne__(self,other):
-        return not (self == other)
-
-    def matches(self, testString, parseAll=True):
-        """
-        Method for quick testing of a parser against a test string. Good for simple 
-        inline microtests of sub expressions while building up larger parser.
-           
-        Parameters:
-         - testString - to test against this expression for a match
-         - parseAll - (default=C{True}) - flag to pass to C{L{parseString}} when running tests
-            
-        Example::
-            expr = Word(nums)
-            assert expr.matches("100")
-        """
-        try:
-            self.parseString(_ustr(testString), parseAll=parseAll)
-            return True
-        except ParseBaseException:
-            return False
-                
-    def runTests(self, tests, parseAll=True, comment='#', fullDump=True, printResults=True, failureTests=False):
-        """
-        Execute the parse expression on a series of test strings, showing each
-        test, the parsed results or where the parse failed. Quick and easy way to
-        run a parse expression against a list of sample strings.
-           
-        Parameters:
-         - tests - a list of separate test strings, or a multiline string of test strings
-         - parseAll - (default=C{True}) - flag to pass to C{L{parseString}} when running tests           
-         - comment - (default=C{'#'}) - expression for indicating embedded comments in the test 
-              string; pass None to disable comment filtering
-         - fullDump - (default=C{True}) - dump results as list followed by results names in nested outline;
-              if False, only dump nested list
-         - printResults - (default=C{True}) prints test output to stdout
-         - failureTests - (default=C{False}) indicates if these tests are expected to fail parsing
-
-        Returns: a (success, results) tuple, where success indicates that all tests succeeded
-        (or failed if C{failureTests} is True), and the results contain a list of lines of each 
-        test's output
-        
-        Example::
-            number_expr = pyparsing_common.number.copy()
-
-            result = number_expr.runTests('''
-                # unsigned integer
-                100
-                # negative integer
-                -100
-                # float with scientific notation
-                6.02e23
-                # integer with scientific notation
-                1e-12
-                ''')
-            print("Success" if result[0] else "Failed!")
-
-            result = number_expr.runTests('''
-                # stray character
-                100Z
-                # missing leading digit before '.'
-                -.100
-                # too many '.'
-                3.14.159
-                ''', failureTests=True)
-            print("Success" if result[0] else "Failed!")
-        prints::
-            # unsigned integer
-            100
-            [100]
-
-            # negative integer
-            -100
-            [-100]
-
-            # float with scientific notation
-            6.02e23
-            [6.02e+23]
-
-            # integer with scientific notation
-            1e-12
-            [1e-12]
-
-            Success
-            
-            # stray character
-            100Z
-               ^
-            FAIL: Expected end of text (at char 3), (line:1, col:4)
-
-            # missing leading digit before '.'
-            -.100
-            ^
-            FAIL: Expected {real number with scientific notation | real number | signed integer} (at char 0), (line:1, col:1)
-
-            # too many '.'
-            3.14.159
-                ^
-            FAIL: Expected end of text (at char 4), (line:1, col:5)
-
-            Success
-
-        Each test string must be on a single line. If you want to test a string that spans multiple
-        lines, create a test like this::
-
-            expr.runTest(r"this is a test\\n of strings that spans \\n 3 lines")
-        
-        (Note that this is a raw string literal, you must include the leading 'r'.)
-        """
-        if isinstance(tests, basestring):
-            tests = list(map(str.strip, tests.rstrip().splitlines()))
-        if isinstance(comment, basestring):
-            comment = Literal(comment)
-        allResults = []
-        comments = []
-        success = True
-        for t in tests:
-            if comment is not None and comment.matches(t, False) or comments and not t:
-                comments.append(t)
-                continue
-            if not t:
-                continue
-            out = ['\n'.join(comments), t]
-            comments = []
-            try:
-                t = t.replace(r'\n','\n')
-                result = self.parseString(t, parseAll=parseAll)
-                out.append(result.dump(full=fullDump))
-                success = success and not failureTests
-            except ParseBaseException as pe:
-                fatal = "(FATAL)" if isinstance(pe, ParseFatalException) else ""
-                if '\n' in t:
-                    out.append(line(pe.loc, t))
-                    out.append(' '*(col(pe.loc,t)-1) + '^' + fatal)
-                else:
-                    out.append(' '*pe.loc + '^' + fatal)
-                out.append("FAIL: " + str(pe))
-                success = success and failureTests
-                result = pe
-            except Exception as exc:
-                out.append("FAIL-EXCEPTION: " + str(exc))
-                success = success and failureTests
-                result = exc
-
-            if printResults:
-                if fullDump:
-                    out.append('')
-                print('\n'.join(out))
-
-            allResults.append((t, result))
-        
-        return success, allResults
-
-        
-class Token(ParserElement):
-    """
-    Abstract C{ParserElement} subclass, for defining atomic matching patterns.
-    """
-    def __init__( self ):
-        super(Token,self).__init__( savelist=False )
-
-
-class Empty(Token):
-    """
-    An empty token, will always match.
-    """
-    def __init__( self ):
-        super(Empty,self).__init__()
-        self.name = "Empty"
-        self.mayReturnEmpty = True
-        self.mayIndexError = False
-
-
-class NoMatch(Token):
-    """
-    A token that will never match.
-    """
-    def __init__( self ):
-        super(NoMatch,self).__init__()
-        self.name = "NoMatch"
-        self.mayReturnEmpty = True
-        self.mayIndexError = False
-        self.errmsg = "Unmatchable token"
-
-    def parseImpl( self, instring, loc, doActions=True ):
-        raise ParseException(instring, loc, self.errmsg, self)
-
-
-class Literal(Token):
-    """
-    Token to exactly match a specified string.
-    
-    Example::
-        Literal('blah').parseString('blah')  # -> ['blah']
-        Literal('blah').parseString('blahfooblah')  # -> ['blah']
-        Literal('blah').parseString('bla')  # -> Exception: Expected "blah"
-    
-    For case-insensitive matching, use L{CaselessLiteral}.
-    
-    For keyword matching (force word break before and after the matched string),
-    use L{Keyword} or L{CaselessKeyword}.
-    """
-    def __init__( self, matchString ):
-        super(Literal,self).__init__()
-        self.match = matchString
-        self.matchLen = len(matchString)
-        try:
-            self.firstMatchChar = matchString[0]
-        except IndexError:
-            warnings.warn("null string passed to Literal; use Empty() instead",
-                            SyntaxWarning, stacklevel=2)
-            self.__class__ = Empty
-        self.name = '"%s"' % _ustr(self.match)
-        self.errmsg = "Expected " + self.name
-        self.mayReturnEmpty = False
-        self.mayIndexError = False
-
-    # Performance tuning: this routine gets called a *lot*
-    # if this is a single character match string  and the first character matches,
-    # short-circuit as quickly as possible, and avoid calling startswith
-    #~ @profile
-    def parseImpl( self, instring, loc, doActions=True ):
-        if (instring[loc] == self.firstMatchChar and
-            (self.matchLen==1 or instring.startswith(self.match,loc)) ):
-            return loc+self.matchLen, self.match
-        raise ParseException(instring, loc, self.errmsg, self)
-_L = Literal
-ParserElement._literalStringClass = Literal
-
-class Keyword(Token):
-    """
-    Token to exactly match a specified string as a keyword, that is, it must be
-    immediately followed by a non-keyword character.  Compare with C{L{Literal}}:
-     - C{Literal("if")} will match the leading C{'if'} in C{'ifAndOnlyIf'}.
-     - C{Keyword("if")} will not; it will only match the leading C{'if'} in C{'if x=1'}, or C{'if(y==2)'}
-    Accepts two optional constructor arguments in addition to the keyword string:
-     - C{identChars} is a string of characters that would be valid identifier characters,
-          defaulting to all alphanumerics + "_" and "$"
-     - C{caseless} allows case-insensitive matching, default is C{False}.
-       
-    Example::
-        Keyword("start").parseString("start")  # -> ['start']
-        Keyword("start").parseString("starting")  # -> Exception
-
-    For case-insensitive matching, use L{CaselessKeyword}.
-    """
-    DEFAULT_KEYWORD_CHARS = alphanums+"_$"
-
-    def __init__( self, matchString, identChars=None, caseless=False ):
-        super(Keyword,self).__init__()
-        if identChars is None:
-            identChars = Keyword.DEFAULT_KEYWORD_CHARS
-        self.match = matchString
-        self.matchLen = len(matchString)
-        try:
-            self.firstMatchChar = matchString[0]
-        except IndexError:
-            warnings.warn("null string passed to Keyword; use Empty() instead",
-                            SyntaxWarning, stacklevel=2)
-        self.name = '"%s"' % self.match
-        self.errmsg = "Expected " + self.name
-        self.mayReturnEmpty = False
-        self.mayIndexError = False
-        self.caseless = caseless
-        if caseless:
-            self.caselessmatch = matchString.upper()
-            identChars = identChars.upper()
-        self.identChars = set(identChars)
-
-    def parseImpl( self, instring, loc, doActions=True ):
-        if self.caseless:
-            if ( (instring[ loc:loc+self.matchLen ].upper() == self.caselessmatch) and
-                 (loc >= len(instring)-self.matchLen or instring[loc+self.matchLen].upper() not in self.identChars) and
-                 (loc == 0 or instring[loc-1].upper() not in self.identChars) ):
-                return loc+self.matchLen, self.match
-        else:
-            if (instring[loc] == self.firstMatchChar and
-                (self.matchLen==1 or instring.startswith(self.match,loc)) and
-                (loc >= len(instring)-self.matchLen or instring[loc+self.matchLen] not in self.identChars) and
-                (loc == 0 or instring[loc-1] not in self.identChars) ):
-                return loc+self.matchLen, self.match
-        raise ParseException(instring, loc, self.errmsg, self)
-
-    def copy(self):
-        c = super(Keyword,self).copy()
-        c.identChars = Keyword.DEFAULT_KEYWORD_CHARS
-        return c
-
-    @staticmethod
-    def setDefaultKeywordChars( chars ):
-        """Overrides the default Keyword chars
-        """
-        Keyword.DEFAULT_KEYWORD_CHARS = chars
-
-class CaselessLiteral(Literal):
-    """
-    Token to match a specified string, ignoring case of letters.
-    Note: the matched results will always be in the case of the given
-    match string, NOT the case of the input text.
-
-    Example::
-        OneOrMore(CaselessLiteral("CMD")).parseString("cmd CMD Cmd10") # -> ['CMD', 'CMD', 'CMD']
-        
-    (Contrast with example for L{CaselessKeyword}.)
-    """
-    def __init__( self, matchString ):
-        super(CaselessLiteral,self).__init__( matchString.upper() )
-        # Preserve the defining literal.
-        self.returnString = matchString
-        self.name = "'%s'" % self.returnString
-        self.errmsg = "Expected " + self.name
-
-    def parseImpl( self, instring, loc, doActions=True ):
-        if instring[ loc:loc+self.matchLen ].upper() == self.match:
-            return loc+self.matchLen, self.returnString
-        raise ParseException(instring, loc, self.errmsg, self)
-
-class CaselessKeyword(Keyword):
-    """
-    Caseless version of L{Keyword}.
-
-    Example::
-        OneOrMore(CaselessKeyword("CMD")).parseString("cmd CMD Cmd10") # -> ['CMD', 'CMD']
-        
-    (Contrast with example for L{CaselessLiteral}.)
-    """
-    def __init__( self, matchString, identChars=None ):
-        super(CaselessKeyword,self).__init__( matchString, identChars, caseless=True )
-
-    def parseImpl( self, instring, loc, doActions=True ):
-        if ( (instring[ loc:loc+self.matchLen ].upper() == self.caselessmatch) and
-             (loc >= len(instring)-self.matchLen or instring[loc+self.matchLen].upper() not in self.identChars) ):
-            return loc+self.matchLen, self.match
-        raise ParseException(instring, loc, self.errmsg, self)
-
-class CloseMatch(Token):
-    """
-    A variation on L{Literal} which matches "close" matches, that is, 
-    strings with at most 'n' mismatching characters. C{CloseMatch} takes parameters:
-     - C{match_string} - string to be matched
-     - C{maxMismatches} - (C{default=1}) maximum number of mismatches allowed to count as a match
-    
-    The results from a successful parse will contain the matched text from the input string and the following named results:
-     - C{mismatches} - a list of the positions within the match_string where mismatches were found
-     - C{original} - the original match_string used to compare against the input string
-    
-    If C{mismatches} is an empty list, then the match was an exact match.
-    
-    Example::
-        patt = CloseMatch("ATCATCGAATGGA")
-        patt.parseString("ATCATCGAAXGGA") # -> (['ATCATCGAAXGGA'], {'mismatches': [[9]], 'original': ['ATCATCGAATGGA']})
-        patt.parseString("ATCAXCGAAXGGA") # -> Exception: Expected 'ATCATCGAATGGA' (with up to 1 mismatches) (at char 0), (line:1, col:1)
-
-        # exact match
-        patt.parseString("ATCATCGAATGGA") # -> (['ATCATCGAATGGA'], {'mismatches': [[]], 'original': ['ATCATCGAATGGA']})
-
-        # close match allowing up to 2 mismatches
-        patt = CloseMatch("ATCATCGAATGGA", maxMismatches=2)
-        patt.parseString("ATCAXCGAAXGGA") # -> (['ATCAXCGAAXGGA'], {'mismatches': [[4, 9]], 'original': ['ATCATCGAATGGA']})
-    """
-    def __init__(self, match_string, maxMismatches=1):
-        super(CloseMatch,self).__init__()
-        self.name = match_string
-        self.match_string = match_string
-        self.maxMismatches = maxMismatches
-        self.errmsg = "Expected %r (with up to %d mismatches)" % (self.match_string, self.maxMismatches)
-        self.mayIndexError = False
-        self.mayReturnEmpty = False
-
-    def parseImpl( self, instring, loc, doActions=True ):
-        start = loc
-        instrlen = len(instring)
-        maxloc = start + len(self.match_string)
-
-        if maxloc <= instrlen:
-            match_string = self.match_string
-            match_stringloc = 0
-            mismatches = []
-            maxMismatches = self.maxMismatches
-
-            for match_stringloc,s_m in enumerate(zip(instring[loc:maxloc], self.match_string)):
-                src,mat = s_m
-                if src != mat:
-                    mismatches.append(match_stringloc)
-                    if len(mismatches) > maxMismatches:
-                        break
-            else:
-                loc = match_stringloc + 1
-                results = ParseResults([instring[start:loc]])
-                results['original'] = self.match_string
-                results['mismatches'] = mismatches
-                return loc, results
-
-        raise ParseException(instring, loc, self.errmsg, self)
-
-
-class Word(Token):
-    """
-    Token for matching words composed of allowed character sets.
-    Defined with string containing all allowed initial characters,
-    an optional string containing allowed body characters (if omitted,
-    defaults to the initial character set), and an optional minimum,
-    maximum, and/or exact length.  The default value for C{min} is 1 (a
-    minimum value < 1 is not valid); the default values for C{max} and C{exact}
-    are 0, meaning no maximum or exact length restriction. An optional
-    C{excludeChars} parameter can list characters that might be found in 
-    the input C{bodyChars} string; useful to define a word of all printables
-    except for one or two characters, for instance.
-    
-    L{srange} is useful for defining custom character set strings for defining 
-    C{Word} expressions, using range notation from regular expression character sets.
-    
-    A common mistake is to use C{Word} to match a specific literal string, as in 
-    C{Word("Address")}. Remember that C{Word} uses the string argument to define
-    I{sets} of matchable characters. This expression would match "Add", "AAA",
-    "dAred", or any other word made up of the characters 'A', 'd', 'r', 'e', and 's'.
-    To match an exact literal string, use L{Literal} or L{Keyword}.
-
-    pyparsing includes helper strings for building Words:
-     - L{alphas}
-     - L{nums}
-     - L{alphanums}
-     - L{hexnums}
-     - L{alphas8bit} (alphabetic characters in ASCII range 128-255 - accented, tilded, umlauted, etc.)
-     - L{punc8bit} (non-alphabetic characters in ASCII range 128-255 - currency, symbols, superscripts, diacriticals, etc.)
-     - L{printables} (any non-whitespace character)
-
-    Example::
-        # a word composed of digits
-        integer = Word(nums) # equivalent to Word("0123456789") or Word(srange("0-9"))
-        
-        # a word with a leading capital, and zero or more lowercase
-        capital_word = Word(alphas.upper(), alphas.lower())
-
-        # hostnames are alphanumeric, with leading alpha, and '-'
-        hostname = Word(alphas, alphanums+'-')
-        
-        # roman numeral (not a strict parser, accepts invalid mix of characters)
-        roman = Word("IVXLCDM")
-        
-        # any string of non-whitespace characters, except for ','
-        csv_value = Word(printables, excludeChars=",")
-    """
-    def __init__( self, initChars, bodyChars=None, min=1, max=0, exact=0, asKeyword=False, excludeChars=None ):
-        super(Word,self).__init__()
-        if excludeChars:
-            initChars = ''.join(c for c in initChars if c not in excludeChars)
-            if bodyChars:
-                bodyChars = ''.join(c for c in bodyChars if c not in excludeChars)
-        self.initCharsOrig = initChars
-        self.initChars = set(initChars)
-        if bodyChars :
-            self.bodyCharsOrig = bodyChars
-            self.bodyChars = set(bodyChars)
-        else:
-            self.bodyCharsOrig = initChars
-            self.bodyChars = set(initChars)
-
-        self.maxSpecified = max > 0
-
-        if min < 1:
-            raise ValueError("cannot specify a minimum length < 1; use Optional(Word()) if zero-length word is permitted")
-
-        self.minLen = min
-
-        if max > 0:
-            self.maxLen = max
-        else:
-            self.maxLen = _MAX_INT
-
-        if exact > 0:
-            self.maxLen = exact
-            self.minLen = exact
-
-        self.name = _ustr(self)
-        self.errmsg = "Expected " + self.name
-        self.mayIndexError = False
-        self.asKeyword = asKeyword
-
-        if ' ' not in self.initCharsOrig+self.bodyCharsOrig and (min==1 and max==0 and exact==0):
-            if self.bodyCharsOrig == self.initCharsOrig:
-                self.reString = "[%s]+" % _escapeRegexRangeChars(self.initCharsOrig)
-            elif len(self.initCharsOrig) == 1:
-                self.reString = "%s[%s]*" % \
-                                      (re.escape(self.initCharsOrig),
-                                      _escapeRegexRangeChars(self.bodyCharsOrig),)
-            else:
-                self.reString = "[%s][%s]*" % \
-                                      (_escapeRegexRangeChars(self.initCharsOrig),
-                                      _escapeRegexRangeChars(self.bodyCharsOrig),)
-            if self.asKeyword:
-                self.reString = r"\b"+self.reString+r"\b"
-            try:
-                self.re = re.compile( self.reString )
-            except Exception:
-                self.re = None
-
-    def parseImpl( self, instring, loc, doActions=True ):
-        if self.re:
-            result = self.re.match(instring,loc)
-            if not result:
-                raise ParseException(instring, loc, self.errmsg, self)
-
-            loc = result.end()
-            return loc, result.group()
-
-        if not(instring[ loc ] in self.initChars):
-            raise ParseException(instring, loc, self.errmsg, self)
-
-        start = loc
-        loc += 1
-        instrlen = len(instring)
-        bodychars = self.bodyChars
-        maxloc = start + self.maxLen
-        maxloc = min( maxloc, instrlen )
-        while loc < maxloc and instring[loc] in bodychars:
-            loc += 1
-
-        throwException = False
-        if loc - start < self.minLen:
-            throwException = True
-        if self.maxSpecified and loc < instrlen and instring[loc] in bodychars:
-            throwException = True
-        if self.asKeyword:
-            if (start>0 and instring[start-1] in bodychars) or (loc4:
-                    return s[:4]+"..."
-                else:
-                    return s
-
-            if ( self.initCharsOrig != self.bodyCharsOrig ):
-                self.strRepr = "W:(%s,%s)" % ( charsAsStr(self.initCharsOrig), charsAsStr(self.bodyCharsOrig) )
-            else:
-                self.strRepr = "W:(%s)" % charsAsStr(self.initCharsOrig)
-
-        return self.strRepr
-
-
-class Regex(Token):
-    r"""
-    Token for matching strings that match a given regular expression.
-    Defined with string specifying the regular expression in a form recognized by the inbuilt Python re module.
-    If the given regex contains named groups (defined using C{(?P...)}), these will be preserved as 
-    named parse results.
-
-    Example::
-        realnum = Regex(r"[+-]?\d+\.\d*")
-        date = Regex(r'(?P\d{4})-(?P\d\d?)-(?P\d\d?)')
-        # ref: http://stackoverflow.com/questions/267399/how-do-you-match-only-valid-roman-numerals-with-a-regular-expression
-        roman = Regex(r"M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})")
-    """
-    compiledREtype = type(re.compile("[A-Z]"))
-    def __init__( self, pattern, flags=0):
-        """The parameters C{pattern} and C{flags} are passed to the C{re.compile()} function as-is. See the Python C{re} module for an explanation of the acceptable patterns and flags."""
-        super(Regex,self).__init__()
-
-        if isinstance(pattern, basestring):
-            if not pattern:
-                warnings.warn("null string passed to Regex; use Empty() instead",
-                        SyntaxWarning, stacklevel=2)
-
-            self.pattern = pattern
-            self.flags = flags
-
-            try:
-                self.re = re.compile(self.pattern, self.flags)
-                self.reString = self.pattern
-            except sre_constants.error:
-                warnings.warn("invalid pattern (%s) passed to Regex" % pattern,
-                    SyntaxWarning, stacklevel=2)
-                raise
-
-        elif isinstance(pattern, Regex.compiledREtype):
-            self.re = pattern
-            self.pattern = \
-            self.reString = str(pattern)
-            self.flags = flags
-            
-        else:
-            raise ValueError("Regex may only be constructed with a string or a compiled RE object")
-
-        self.name = _ustr(self)
-        self.errmsg = "Expected " + self.name
-        self.mayIndexError = False
-        self.mayReturnEmpty = True
-
-    def parseImpl( self, instring, loc, doActions=True ):
-        result = self.re.match(instring,loc)
-        if not result:
-            raise ParseException(instring, loc, self.errmsg, self)
-
-        loc = result.end()
-        d = result.groupdict()
-        ret = ParseResults(result.group())
-        if d:
-            for k in d:
-                ret[k] = d[k]
-        return loc,ret
-
-    def __str__( self ):
-        try:
-            return super(Regex,self).__str__()
-        except Exception:
-            pass
-
-        if self.strRepr is None:
-            self.strRepr = "Re:(%s)" % repr(self.pattern)
-
-        return self.strRepr
-
-
-class QuotedString(Token):
-    r"""
-    Token for matching strings that are delimited by quoting characters.
-    
-    Defined with the following parameters:
-        - quoteChar - string of one or more characters defining the quote delimiting string
-        - escChar - character to escape quotes, typically backslash (default=C{None})
-        - escQuote - special quote sequence to escape an embedded quote string (such as SQL's "" to escape an embedded ") (default=C{None})
-        - multiline - boolean indicating whether quotes can span multiple lines (default=C{False})
-        - unquoteResults - boolean indicating whether the matched text should be unquoted (default=C{True})
-        - endQuoteChar - string of one or more characters defining the end of the quote delimited string (default=C{None} => same as quoteChar)
-        - convertWhitespaceEscapes - convert escaped whitespace (C{'\t'}, C{'\n'}, etc.) to actual whitespace (default=C{True})
-
-    Example::
-        qs = QuotedString('"')
-        print(qs.searchString('lsjdf "This is the quote" sldjf'))
-        complex_qs = QuotedString('{{', endQuoteChar='}}')
-        print(complex_qs.searchString('lsjdf {{This is the "quote"}} sldjf'))
-        sql_qs = QuotedString('"', escQuote='""')
-        print(sql_qs.searchString('lsjdf "This is the quote with ""embedded"" quotes" sldjf'))
-    prints::
-        [['This is the quote']]
-        [['This is the "quote"']]
-        [['This is the quote with "embedded" quotes']]
-    """
-    def __init__( self, quoteChar, escChar=None, escQuote=None, multiline=False, unquoteResults=True, endQuoteChar=None, convertWhitespaceEscapes=True):
-        super(QuotedString,self).__init__()
-
-        # remove white space from quote chars - wont work anyway
-        quoteChar = quoteChar.strip()
-        if not quoteChar:
-            warnings.warn("quoteChar cannot be the empty string",SyntaxWarning,stacklevel=2)
-            raise SyntaxError()
-
-        if endQuoteChar is None:
-            endQuoteChar = quoteChar
-        else:
-            endQuoteChar = endQuoteChar.strip()
-            if not endQuoteChar:
-                warnings.warn("endQuoteChar cannot be the empty string",SyntaxWarning,stacklevel=2)
-                raise SyntaxError()
-
-        self.quoteChar = quoteChar
-        self.quoteCharLen = len(quoteChar)
-        self.firstQuoteChar = quoteChar[0]
-        self.endQuoteChar = endQuoteChar
-        self.endQuoteCharLen = len(endQuoteChar)
-        self.escChar = escChar
-        self.escQuote = escQuote
-        self.unquoteResults = unquoteResults
-        self.convertWhitespaceEscapes = convertWhitespaceEscapes
-
-        if multiline:
-            self.flags = re.MULTILINE | re.DOTALL
-            self.pattern = r'%s(?:[^%s%s]' % \
-                ( re.escape(self.quoteChar),
-                  _escapeRegexRangeChars(self.endQuoteChar[0]),
-                  (escChar is not None and _escapeRegexRangeChars(escChar) or '') )
-        else:
-            self.flags = 0
-            self.pattern = r'%s(?:[^%s\n\r%s]' % \
-                ( re.escape(self.quoteChar),
-                  _escapeRegexRangeChars(self.endQuoteChar[0]),
-                  (escChar is not None and _escapeRegexRangeChars(escChar) or '') )
-        if len(self.endQuoteChar) > 1:
-            self.pattern += (
-                '|(?:' + ')|(?:'.join("%s[^%s]" % (re.escape(self.endQuoteChar[:i]),
-                                               _escapeRegexRangeChars(self.endQuoteChar[i]))
-                                    for i in range(len(self.endQuoteChar)-1,0,-1)) + ')'
-                )
-        if escQuote:
-            self.pattern += (r'|(?:%s)' % re.escape(escQuote))
-        if escChar:
-            self.pattern += (r'|(?:%s.)' % re.escape(escChar))
-            self.escCharReplacePattern = re.escape(self.escChar)+"(.)"
-        self.pattern += (r')*%s' % re.escape(self.endQuoteChar))
-
-        try:
-            self.re = re.compile(self.pattern, self.flags)
-            self.reString = self.pattern
-        except sre_constants.error:
-            warnings.warn("invalid pattern (%s) passed to Regex" % self.pattern,
-                SyntaxWarning, stacklevel=2)
-            raise
-
-        self.name = _ustr(self)
-        self.errmsg = "Expected " + self.name
-        self.mayIndexError = False
-        self.mayReturnEmpty = True
-
-    def parseImpl( self, instring, loc, doActions=True ):
-        result = instring[loc] == self.firstQuoteChar and self.re.match(instring,loc) or None
-        if not result:
-            raise ParseException(instring, loc, self.errmsg, self)
-
-        loc = result.end()
-        ret = result.group()
-
-        if self.unquoteResults:
-
-            # strip off quotes
-            ret = ret[self.quoteCharLen:-self.endQuoteCharLen]
-
-            if isinstance(ret,basestring):
-                # replace escaped whitespace
-                if '\\' in ret and self.convertWhitespaceEscapes:
-                    ws_map = {
-                        r'\t' : '\t',
-                        r'\n' : '\n',
-                        r'\f' : '\f',
-                        r'\r' : '\r',
-                    }
-                    for wslit,wschar in ws_map.items():
-                        ret = ret.replace(wslit, wschar)
-
-                # replace escaped characters
-                if self.escChar:
-                    ret = re.sub(self.escCharReplacePattern, r"\g<1>", ret)
-
-                # replace escaped quotes
-                if self.escQuote:
-                    ret = ret.replace(self.escQuote, self.endQuoteChar)
-
-        return loc, ret
-
-    def __str__( self ):
-        try:
-            return super(QuotedString,self).__str__()
-        except Exception:
-            pass
-
-        if self.strRepr is None:
-            self.strRepr = "quoted string, starting with %s ending with %s" % (self.quoteChar, self.endQuoteChar)
-
-        return self.strRepr
-
-
-class CharsNotIn(Token):
-    """
-    Token for matching words composed of characters I{not} in a given set (will
-    include whitespace in matched characters if not listed in the provided exclusion set - see example).
-    Defined with string containing all disallowed characters, and an optional
-    minimum, maximum, and/or exact length.  The default value for C{min} is 1 (a
-    minimum value < 1 is not valid); the default values for C{max} and C{exact}
-    are 0, meaning no maximum or exact length restriction.
-
-    Example::
-        # define a comma-separated-value as anything that is not a ','
-        csv_value = CharsNotIn(',')
-        print(delimitedList(csv_value).parseString("dkls,lsdkjf,s12 34,@!#,213"))
-    prints::
-        ['dkls', 'lsdkjf', 's12 34', '@!#', '213']
-    """
-    def __init__( self, notChars, min=1, max=0, exact=0 ):
-        super(CharsNotIn,self).__init__()
-        self.skipWhitespace = False
-        self.notChars = notChars
-
-        if min < 1:
-            raise ValueError("cannot specify a minimum length < 1; use Optional(CharsNotIn()) if zero-length char group is permitted")
-
-        self.minLen = min
-
-        if max > 0:
-            self.maxLen = max
-        else:
-            self.maxLen = _MAX_INT
-
-        if exact > 0:
-            self.maxLen = exact
-            self.minLen = exact
-
-        self.name = _ustr(self)
-        self.errmsg = "Expected " + self.name
-        self.mayReturnEmpty = ( self.minLen == 0 )
-        self.mayIndexError = False
-
-    def parseImpl( self, instring, loc, doActions=True ):
-        if instring[loc] in self.notChars:
-            raise ParseException(instring, loc, self.errmsg, self)
-
-        start = loc
-        loc += 1
-        notchars = self.notChars
-        maxlen = min( start+self.maxLen, len(instring) )
-        while loc < maxlen and \
-              (instring[loc] not in notchars):
-            loc += 1
-
-        if loc - start < self.minLen:
-            raise ParseException(instring, loc, self.errmsg, self)
-
-        return loc, instring[start:loc]
-
-    def __str__( self ):
-        try:
-            return super(CharsNotIn, self).__str__()
-        except Exception:
-            pass
-
-        if self.strRepr is None:
-            if len(self.notChars) > 4:
-                self.strRepr = "!W:(%s...)" % self.notChars[:4]
-            else:
-                self.strRepr = "!W:(%s)" % self.notChars
-
-        return self.strRepr
-
-class White(Token):
-    """
-    Special matching class for matching whitespace.  Normally, whitespace is ignored
-    by pyparsing grammars.  This class is included when some whitespace structures
-    are significant.  Define with a string containing the whitespace characters to be
-    matched; default is C{" \\t\\r\\n"}.  Also takes optional C{min}, C{max}, and C{exact} arguments,
-    as defined for the C{L{Word}} class.
-    """
-    whiteStrs = {
-        " " : "",
-        "\t": "",
-        "\n": "",
-        "\r": "",
-        "\f": "",
-        }
-    def __init__(self, ws=" \t\r\n", min=1, max=0, exact=0):
-        super(White,self).__init__()
-        self.matchWhite = ws
-        self.setWhitespaceChars( "".join(c for c in self.whiteChars if c not in self.matchWhite) )
-        #~ self.leaveWhitespace()
-        self.name = ("".join(White.whiteStrs[c] for c in self.matchWhite))
-        self.mayReturnEmpty = True
-        self.errmsg = "Expected " + self.name
-
-        self.minLen = min
-
-        if max > 0:
-            self.maxLen = max
-        else:
-            self.maxLen = _MAX_INT
-
-        if exact > 0:
-            self.maxLen = exact
-            self.minLen = exact
-
-    def parseImpl( self, instring, loc, doActions=True ):
-        if not(instring[ loc ] in self.matchWhite):
-            raise ParseException(instring, loc, self.errmsg, self)
-        start = loc
-        loc += 1
-        maxloc = start + self.maxLen
-        maxloc = min( maxloc, len(instring) )
-        while loc < maxloc and instring[loc] in self.matchWhite:
-            loc += 1
-
-        if loc - start < self.minLen:
-            raise ParseException(instring, loc, self.errmsg, self)
-
-        return loc, instring[start:loc]
-
-
-class _PositionToken(Token):
-    def __init__( self ):
-        super(_PositionToken,self).__init__()
-        self.name=self.__class__.__name__
-        self.mayReturnEmpty = True
-        self.mayIndexError = False
-
-class GoToColumn(_PositionToken):
-    """
-    Token to advance to a specific column of input text; useful for tabular report scraping.
-    """
-    def __init__( self, colno ):
-        super(GoToColumn,self).__init__()
-        self.col = colno
-
-    def preParse( self, instring, loc ):
-        if col(loc,instring) != self.col:
-            instrlen = len(instring)
-            if self.ignoreExprs:
-                loc = self._skipIgnorables( instring, loc )
-            while loc < instrlen and instring[loc].isspace() and col( loc, instring ) != self.col :
-                loc += 1
-        return loc
-
-    def parseImpl( self, instring, loc, doActions=True ):
-        thiscol = col( loc, instring )
-        if thiscol > self.col:
-            raise ParseException( instring, loc, "Text not in expected column", self )
-        newloc = loc + self.col - thiscol
-        ret = instring[ loc: newloc ]
-        return newloc, ret
-
-
-class LineStart(_PositionToken):
-    """
-    Matches if current position is at the beginning of a line within the parse string
-    
-    Example::
-    
-        test = '''\
-        AAA this line
-        AAA and this line
-          AAA but not this one
-        B AAA and definitely not this one
-        '''
-
-        for t in (LineStart() + 'AAA' + restOfLine).searchString(test):
-            print(t)
-    
-    Prints::
-        ['AAA', ' this line']
-        ['AAA', ' and this line']    
-
-    """
-    def __init__( self ):
-        super(LineStart,self).__init__()
-        self.errmsg = "Expected start of line"
-
-    def parseImpl( self, instring, loc, doActions=True ):
-        if col(loc, instring) == 1:
-            return loc, []
-        raise ParseException(instring, loc, self.errmsg, self)
-
-class LineEnd(_PositionToken):
-    """
-    Matches if current position is at the end of a line within the parse string
-    """
-    def __init__( self ):
-        super(LineEnd,self).__init__()
-        self.setWhitespaceChars( ParserElement.DEFAULT_WHITE_CHARS.replace("\n","") )
-        self.errmsg = "Expected end of line"
-
-    def parseImpl( self, instring, loc, doActions=True ):
-        if loc len(instring):
-            return loc, []
-        else:
-            raise ParseException(instring, loc, self.errmsg, self)
-
-class WordStart(_PositionToken):
-    """
-    Matches if the current position is at the beginning of a Word, and
-    is not preceded by any character in a given set of C{wordChars}
-    (default=C{printables}). To emulate the C{\b} behavior of regular expressions,
-    use C{WordStart(alphanums)}. C{WordStart} will also match at the beginning of
-    the string being parsed, or at the beginning of a line.
-    """
-    def __init__(self, wordChars = printables):
-        super(WordStart,self).__init__()
-        self.wordChars = set(wordChars)
-        self.errmsg = "Not at the start of a word"
-
-    def parseImpl(self, instring, loc, doActions=True ):
-        if loc != 0:
-            if (instring[loc-1] in self.wordChars or
-                instring[loc] not in self.wordChars):
-                raise ParseException(instring, loc, self.errmsg, self)
-        return loc, []
-
-class WordEnd(_PositionToken):
-    """
-    Matches if the current position is at the end of a Word, and
-    is not followed by any character in a given set of C{wordChars}
-    (default=C{printables}). To emulate the C{\b} behavior of regular expressions,
-    use C{WordEnd(alphanums)}. C{WordEnd} will also match at the end of
-    the string being parsed, or at the end of a line.
-    """
-    def __init__(self, wordChars = printables):
-        super(WordEnd,self).__init__()
-        self.wordChars = set(wordChars)
-        self.skipWhitespace = False
-        self.errmsg = "Not at the end of a word"
-
-    def parseImpl(self, instring, loc, doActions=True ):
-        instrlen = len(instring)
-        if instrlen>0 and loc maxExcLoc:
-                    maxException = err
-                    maxExcLoc = err.loc
-            except IndexError:
-                if len(instring) > maxExcLoc:
-                    maxException = ParseException(instring,len(instring),e.errmsg,self)
-                    maxExcLoc = len(instring)
-            else:
-                # save match among all matches, to retry longest to shortest
-                matches.append((loc2, e))
-
-        if matches:
-            matches.sort(key=lambda x: -x[0])
-            for _,e in matches:
-                try:
-                    return e._parse( instring, loc, doActions )
-                except ParseException as err:
-                    err.__traceback__ = None
-                    if err.loc > maxExcLoc:
-                        maxException = err
-                        maxExcLoc = err.loc
-
-        if maxException is not None:
-            maxException.msg = self.errmsg
-            raise maxException
-        else:
-            raise ParseException(instring, loc, "no defined alternatives to match", self)
-
-
-    def __ixor__(self, other ):
-        if isinstance( other, basestring ):
-            other = ParserElement._literalStringClass( other )
-        return self.append( other ) #Or( [ self, other ] )
-
-    def __str__( self ):
-        if hasattr(self,"name"):
-            return self.name
-
-        if self.strRepr is None:
-            self.strRepr = "{" + " ^ ".join(_ustr(e) for e in self.exprs) + "}"
-
-        return self.strRepr
-
-    def checkRecursion( self, parseElementList ):
-        subRecCheckList = parseElementList[:] + [ self ]
-        for e in self.exprs:
-            e.checkRecursion( subRecCheckList )
-
-
-class MatchFirst(ParseExpression):
-    """
-    Requires that at least one C{ParseExpression} is found.
-    If two expressions match, the first one listed is the one that will match.
-    May be constructed using the C{'|'} operator.
-
-    Example::
-        # construct MatchFirst using '|' operator
-        
-        # watch the order of expressions to match
-        number = Word(nums) | Combine(Word(nums) + '.' + Word(nums))
-        print(number.searchString("123 3.1416 789")) #  Fail! -> [['123'], ['3'], ['1416'], ['789']]
-
-        # put more selective expression first
-        number = Combine(Word(nums) + '.' + Word(nums)) | Word(nums)
-        print(number.searchString("123 3.1416 789")) #  Better -> [['123'], ['3.1416'], ['789']]
-    """
-    def __init__( self, exprs, savelist = False ):
-        super(MatchFirst,self).__init__(exprs, savelist)
-        if self.exprs:
-            self.mayReturnEmpty = any(e.mayReturnEmpty for e in self.exprs)
-        else:
-            self.mayReturnEmpty = True
-
-    def parseImpl( self, instring, loc, doActions=True ):
-        maxExcLoc = -1
-        maxException = None
-        for e in self.exprs:
-            try:
-                ret = e._parse( instring, loc, doActions )
-                return ret
-            except ParseException as err:
-                if err.loc > maxExcLoc:
-                    maxException = err
-                    maxExcLoc = err.loc
-            except IndexError:
-                if len(instring) > maxExcLoc:
-                    maxException = ParseException(instring,len(instring),e.errmsg,self)
-                    maxExcLoc = len(instring)
-
-        # only got here if no expression matched, raise exception for match that made it the furthest
-        else:
-            if maxException is not None:
-                maxException.msg = self.errmsg
-                raise maxException
-            else:
-                raise ParseException(instring, loc, "no defined alternatives to match", self)
-
-    def __ior__(self, other ):
-        if isinstance( other, basestring ):
-            other = ParserElement._literalStringClass( other )
-        return self.append( other ) #MatchFirst( [ self, other ] )
-
-    def __str__( self ):
-        if hasattr(self,"name"):
-            return self.name
-
-        if self.strRepr is None:
-            self.strRepr = "{" + " | ".join(_ustr(e) for e in self.exprs) + "}"
-
-        return self.strRepr
-
-    def checkRecursion( self, parseElementList ):
-        subRecCheckList = parseElementList[:] + [ self ]
-        for e in self.exprs:
-            e.checkRecursion( subRecCheckList )
-
-
-class Each(ParseExpression):
-    """
-    Requires all given C{ParseExpression}s to be found, but in any order.
-    Expressions may be separated by whitespace.
-    May be constructed using the C{'&'} operator.
-
-    Example::
-        color = oneOf("RED ORANGE YELLOW GREEN BLUE PURPLE BLACK WHITE BROWN")
-        shape_type = oneOf("SQUARE CIRCLE TRIANGLE STAR HEXAGON OCTAGON")
-        integer = Word(nums)
-        shape_attr = "shape:" + shape_type("shape")
-        posn_attr = "posn:" + Group(integer("x") + ',' + integer("y"))("posn")
-        color_attr = "color:" + color("color")
-        size_attr = "size:" + integer("size")
-
-        # use Each (using operator '&') to accept attributes in any order 
-        # (shape and posn are required, color and size are optional)
-        shape_spec = shape_attr & posn_attr & Optional(color_attr) & Optional(size_attr)
-
-        shape_spec.runTests('''
-            shape: SQUARE color: BLACK posn: 100, 120
-            shape: CIRCLE size: 50 color: BLUE posn: 50,80
-            color:GREEN size:20 shape:TRIANGLE posn:20,40
-            '''
-            )
-    prints::
-        shape: SQUARE color: BLACK posn: 100, 120
-        ['shape:', 'SQUARE', 'color:', 'BLACK', 'posn:', ['100', ',', '120']]
-        - color: BLACK
-        - posn: ['100', ',', '120']
-          - x: 100
-          - y: 120
-        - shape: SQUARE
-
-
-        shape: CIRCLE size: 50 color: BLUE posn: 50,80
-        ['shape:', 'CIRCLE', 'size:', '50', 'color:', 'BLUE', 'posn:', ['50', ',', '80']]
-        - color: BLUE
-        - posn: ['50', ',', '80']
-          - x: 50
-          - y: 80
-        - shape: CIRCLE
-        - size: 50
-
-
-        color: GREEN size: 20 shape: TRIANGLE posn: 20,40
-        ['color:', 'GREEN', 'size:', '20', 'shape:', 'TRIANGLE', 'posn:', ['20', ',', '40']]
-        - color: GREEN
-        - posn: ['20', ',', '40']
-          - x: 20
-          - y: 40
-        - shape: TRIANGLE
-        - size: 20
-    """
-    def __init__( self, exprs, savelist = True ):
-        super(Each,self).__init__(exprs, savelist)
-        self.mayReturnEmpty = all(e.mayReturnEmpty for e in self.exprs)
-        self.skipWhitespace = True
-        self.initExprGroups = True
-
-    def parseImpl( self, instring, loc, doActions=True ):
-        if self.initExprGroups:
-            self.opt1map = dict((id(e.expr),e) for e in self.exprs if isinstance(e,Optional))
-            opt1 = [ e.expr for e in self.exprs if isinstance(e,Optional) ]
-            opt2 = [ e for e in self.exprs if e.mayReturnEmpty and not isinstance(e,Optional)]
-            self.optionals = opt1 + opt2
-            self.multioptionals = [ e.expr for e in self.exprs if isinstance(e,ZeroOrMore) ]
-            self.multirequired = [ e.expr for e in self.exprs if isinstance(e,OneOrMore) ]
-            self.required = [ e for e in self.exprs if not isinstance(e,(Optional,ZeroOrMore,OneOrMore)) ]
-            self.required += self.multirequired
-            self.initExprGroups = False
-        tmpLoc = loc
-        tmpReqd = self.required[:]
-        tmpOpt  = self.optionals[:]
-        matchOrder = []
-
-        keepMatching = True
-        while keepMatching:
-            tmpExprs = tmpReqd + tmpOpt + self.multioptionals + self.multirequired
-            failed = []
-            for e in tmpExprs:
-                try:
-                    tmpLoc = e.tryParse( instring, tmpLoc )
-                except ParseException:
-                    failed.append(e)
-                else:
-                    matchOrder.append(self.opt1map.get(id(e),e))
-                    if e in tmpReqd:
-                        tmpReqd.remove(e)
-                    elif e in tmpOpt:
-                        tmpOpt.remove(e)
-            if len(failed) == len(tmpExprs):
-                keepMatching = False
-
-        if tmpReqd:
-            missing = ", ".join(_ustr(e) for e in tmpReqd)
-            raise ParseException(instring,loc,"Missing one or more required elements (%s)" % missing )
-
-        # add any unmatched Optionals, in case they have default values defined
-        matchOrder += [e for e in self.exprs if isinstance(e,Optional) and e.expr in tmpOpt]
-
-        resultlist = []
-        for e in matchOrder:
-            loc,results = e._parse(instring,loc,doActions)
-            resultlist.append(results)
-
-        finalResults = sum(resultlist, ParseResults([]))
-        return loc, finalResults
-
-    def __str__( self ):
-        if hasattr(self,"name"):
-            return self.name
-
-        if self.strRepr is None:
-            self.strRepr = "{" + " & ".join(_ustr(e) for e in self.exprs) + "}"
-
-        return self.strRepr
-
-    def checkRecursion( self, parseElementList ):
-        subRecCheckList = parseElementList[:] + [ self ]
-        for e in self.exprs:
-            e.checkRecursion( subRecCheckList )
-
-
-class ParseElementEnhance(ParserElement):
-    """
-    Abstract subclass of C{ParserElement}, for combining and post-processing parsed tokens.
-    """
-    def __init__( self, expr, savelist=False ):
-        super(ParseElementEnhance,self).__init__(savelist)
-        if isinstance( expr, basestring ):
-            if issubclass(ParserElement._literalStringClass, Token):
-                expr = ParserElement._literalStringClass(expr)
-            else:
-                expr = ParserElement._literalStringClass(Literal(expr))
-        self.expr = expr
-        self.strRepr = None
-        if expr is not None:
-            self.mayIndexError = expr.mayIndexError
-            self.mayReturnEmpty = expr.mayReturnEmpty
-            self.setWhitespaceChars( expr.whiteChars )
-            self.skipWhitespace = expr.skipWhitespace
-            self.saveAsList = expr.saveAsList
-            self.callPreparse = expr.callPreparse
-            self.ignoreExprs.extend(expr.ignoreExprs)
-
-    def parseImpl( self, instring, loc, doActions=True ):
-        if self.expr is not None:
-            return self.expr._parse( instring, loc, doActions, callPreParse=False )
-        else:
-            raise ParseException("",loc,self.errmsg,self)
-
-    def leaveWhitespace( self ):
-        self.skipWhitespace = False
-        self.expr = self.expr.copy()
-        if self.expr is not None:
-            self.expr.leaveWhitespace()
-        return self
-
-    def ignore( self, other ):
-        if isinstance( other, Suppress ):
-            if other not in self.ignoreExprs:
-                super( ParseElementEnhance, self).ignore( other )
-                if self.expr is not None:
-                    self.expr.ignore( self.ignoreExprs[-1] )
-        else:
-            super( ParseElementEnhance, self).ignore( other )
-            if self.expr is not None:
-                self.expr.ignore( self.ignoreExprs[-1] )
-        return self
-
-    def streamline( self ):
-        super(ParseElementEnhance,self).streamline()
-        if self.expr is not None:
-            self.expr.streamline()
-        return self
-
-    def checkRecursion( self, parseElementList ):
-        if self in parseElementList:
-            raise RecursiveGrammarException( parseElementList+[self] )
-        subRecCheckList = parseElementList[:] + [ self ]
-        if self.expr is not None:
-            self.expr.checkRecursion( subRecCheckList )
-
-    def validate( self, validateTrace=[] ):
-        tmp = validateTrace[:]+[self]
-        if self.expr is not None:
-            self.expr.validate(tmp)
-        self.checkRecursion( [] )
-
-    def __str__( self ):
-        try:
-            return super(ParseElementEnhance,self).__str__()
-        except Exception:
-            pass
-
-        if self.strRepr is None and self.expr is not None:
-            self.strRepr = "%s:(%s)" % ( self.__class__.__name__, _ustr(self.expr) )
-        return self.strRepr
-
-
-class FollowedBy(ParseElementEnhance):
-    """
-    Lookahead matching of the given parse expression.  C{FollowedBy}
-    does I{not} advance the parsing position within the input string, it only
-    verifies that the specified parse expression matches at the current
-    position.  C{FollowedBy} always returns a null token list.
-
-    Example::
-        # use FollowedBy to match a label only if it is followed by a ':'
-        data_word = Word(alphas)
-        label = data_word + FollowedBy(':')
-        attr_expr = Group(label + Suppress(':') + OneOrMore(data_word, stopOn=label).setParseAction(' '.join))
-        
-        OneOrMore(attr_expr).parseString("shape: SQUARE color: BLACK posn: upper left").pprint()
-    prints::
-        [['shape', 'SQUARE'], ['color', 'BLACK'], ['posn', 'upper left']]
-    """
-    def __init__( self, expr ):
-        super(FollowedBy,self).__init__(expr)
-        self.mayReturnEmpty = True
-
-    def parseImpl( self, instring, loc, doActions=True ):
-        self.expr.tryParse( instring, loc )
-        return loc, []
-
-
-class NotAny(ParseElementEnhance):
-    """
-    Lookahead to disallow matching with the given parse expression.  C{NotAny}
-    does I{not} advance the parsing position within the input string, it only
-    verifies that the specified parse expression does I{not} match at the current
-    position.  Also, C{NotAny} does I{not} skip over leading whitespace. C{NotAny}
-    always returns a null token list.  May be constructed using the '~' operator.
-
-    Example::
-        
-    """
-    def __init__( self, expr ):
-        super(NotAny,self).__init__(expr)
-        #~ self.leaveWhitespace()
-        self.skipWhitespace = False  # do NOT use self.leaveWhitespace(), don't want to propagate to exprs
-        self.mayReturnEmpty = True
-        self.errmsg = "Found unwanted token, "+_ustr(self.expr)
-
-    def parseImpl( self, instring, loc, doActions=True ):
-        if self.expr.canParseNext(instring, loc):
-            raise ParseException(instring, loc, self.errmsg, self)
-        return loc, []
-
-    def __str__( self ):
-        if hasattr(self,"name"):
-            return self.name
-
-        if self.strRepr is None:
-            self.strRepr = "~{" + _ustr(self.expr) + "}"
-
-        return self.strRepr
-
-class _MultipleMatch(ParseElementEnhance):
-    def __init__( self, expr, stopOn=None):
-        super(_MultipleMatch, self).__init__(expr)
-        self.saveAsList = True
-        ender = stopOn
-        if isinstance(ender, basestring):
-            ender = ParserElement._literalStringClass(ender)
-        self.not_ender = ~ender if ender is not None else None
-
-    def parseImpl( self, instring, loc, doActions=True ):
-        self_expr_parse = self.expr._parse
-        self_skip_ignorables = self._skipIgnorables
-        check_ender = self.not_ender is not None
-        if check_ender:
-            try_not_ender = self.not_ender.tryParse
-        
-        # must be at least one (but first see if we are the stopOn sentinel;
-        # if so, fail)
-        if check_ender:
-            try_not_ender(instring, loc)
-        loc, tokens = self_expr_parse( instring, loc, doActions, callPreParse=False )
-        try:
-            hasIgnoreExprs = (not not self.ignoreExprs)
-            while 1:
-                if check_ender:
-                    try_not_ender(instring, loc)
-                if hasIgnoreExprs:
-                    preloc = self_skip_ignorables( instring, loc )
-                else:
-                    preloc = loc
-                loc, tmptokens = self_expr_parse( instring, preloc, doActions )
-                if tmptokens or tmptokens.haskeys():
-                    tokens += tmptokens
-        except (ParseException,IndexError):
-            pass
-
-        return loc, tokens
-        
-class OneOrMore(_MultipleMatch):
-    """
-    Repetition of one or more of the given expression.
-    
-    Parameters:
-     - expr - expression that must match one or more times
-     - stopOn - (default=C{None}) - expression for a terminating sentinel
-          (only required if the sentinel would ordinarily match the repetition 
-          expression)          
-
-    Example::
-        data_word = Word(alphas)
-        label = data_word + FollowedBy(':')
-        attr_expr = Group(label + Suppress(':') + OneOrMore(data_word).setParseAction(' '.join))
-
-        text = "shape: SQUARE posn: upper left color: BLACK"
-        OneOrMore(attr_expr).parseString(text).pprint()  # Fail! read 'color' as data instead of next label -> [['shape', 'SQUARE color']]
-
-        # use stopOn attribute for OneOrMore to avoid reading label string as part of the data
-        attr_expr = Group(label + Suppress(':') + OneOrMore(data_word, stopOn=label).setParseAction(' '.join))
-        OneOrMore(attr_expr).parseString(text).pprint() # Better -> [['shape', 'SQUARE'], ['posn', 'upper left'], ['color', 'BLACK']]
-        
-        # could also be written as
-        (attr_expr * (1,)).parseString(text).pprint()
-    """
-
-    def __str__( self ):
-        if hasattr(self,"name"):
-            return self.name
-
-        if self.strRepr is None:
-            self.strRepr = "{" + _ustr(self.expr) + "}..."
-
-        return self.strRepr
-
-class ZeroOrMore(_MultipleMatch):
-    """
-    Optional repetition of zero or more of the given expression.
-    
-    Parameters:
-     - expr - expression that must match zero or more times
-     - stopOn - (default=C{None}) - expression for a terminating sentinel
-          (only required if the sentinel would ordinarily match the repetition 
-          expression)          
-
-    Example: similar to L{OneOrMore}
-    """
-    def __init__( self, expr, stopOn=None):
-        super(ZeroOrMore,self).__init__(expr, stopOn=stopOn)
-        self.mayReturnEmpty = True
-        
-    def parseImpl( self, instring, loc, doActions=True ):
-        try:
-            return super(ZeroOrMore, self).parseImpl(instring, loc, doActions)
-        except (ParseException,IndexError):
-            return loc, []
-
-    def __str__( self ):
-        if hasattr(self,"name"):
-            return self.name
-
-        if self.strRepr is None:
-            self.strRepr = "[" + _ustr(self.expr) + "]..."
-
-        return self.strRepr
-
-class _NullToken(object):
-    def __bool__(self):
-        return False
-    __nonzero__ = __bool__
-    def __str__(self):
-        return ""
-
-_optionalNotMatched = _NullToken()
-class Optional(ParseElementEnhance):
-    """
-    Optional matching of the given expression.
-
-    Parameters:
-     - expr - expression that must match zero or more times
-     - default (optional) - value to be returned if the optional expression is not found.
-
-    Example::
-        # US postal code can be a 5-digit zip, plus optional 4-digit qualifier
-        zip = Combine(Word(nums, exact=5) + Optional('-' + Word(nums, exact=4)))
-        zip.runTests('''
-            # traditional ZIP code
-            12345
-            
-            # ZIP+4 form
-            12101-0001
-            
-            # invalid ZIP
-            98765-
-            ''')
-    prints::
-        # traditional ZIP code
-        12345
-        ['12345']
-
-        # ZIP+4 form
-        12101-0001
-        ['12101-0001']
-
-        # invalid ZIP
-        98765-
-             ^
-        FAIL: Expected end of text (at char 5), (line:1, col:6)
-    """
-    def __init__( self, expr, default=_optionalNotMatched ):
-        super(Optional,self).__init__( expr, savelist=False )
-        self.saveAsList = self.expr.saveAsList
-        self.defaultValue = default
-        self.mayReturnEmpty = True
-
-    def parseImpl( self, instring, loc, doActions=True ):
-        try:
-            loc, tokens = self.expr._parse( instring, loc, doActions, callPreParse=False )
-        except (ParseException,IndexError):
-            if self.defaultValue is not _optionalNotMatched:
-                if self.expr.resultsName:
-                    tokens = ParseResults([ self.defaultValue ])
-                    tokens[self.expr.resultsName] = self.defaultValue
-                else:
-                    tokens = [ self.defaultValue ]
-            else:
-                tokens = []
-        return loc, tokens
-
-    def __str__( self ):
-        if hasattr(self,"name"):
-            return self.name
-
-        if self.strRepr is None:
-            self.strRepr = "[" + _ustr(self.expr) + "]"
-
-        return self.strRepr
-
-class SkipTo(ParseElementEnhance):
-    """
-    Token for skipping over all undefined text until the matched expression is found.
-
-    Parameters:
-     - expr - target expression marking the end of the data to be skipped
-     - include - (default=C{False}) if True, the target expression is also parsed 
-          (the skipped text and target expression are returned as a 2-element list).
-     - ignore - (default=C{None}) used to define grammars (typically quoted strings and 
-          comments) that might contain false matches to the target expression
-     - failOn - (default=C{None}) define expressions that are not allowed to be 
-          included in the skipped test; if found before the target expression is found, 
-          the SkipTo is not a match
-
-    Example::
-        report = '''
-            Outstanding Issues Report - 1 Jan 2000
-
-               # | Severity | Description                               |  Days Open
-            -----+----------+-------------------------------------------+-----------
-             101 | Critical | Intermittent system crash                 |          6
-              94 | Cosmetic | Spelling error on Login ('log|n')         |         14
-              79 | Minor    | System slow when running too many reports |         47
-            '''
-        integer = Word(nums)
-        SEP = Suppress('|')
-        # use SkipTo to simply match everything up until the next SEP
-        # - ignore quoted strings, so that a '|' character inside a quoted string does not match
-        # - parse action will call token.strip() for each matched token, i.e., the description body
-        string_data = SkipTo(SEP, ignore=quotedString)
-        string_data.setParseAction(tokenMap(str.strip))
-        ticket_expr = (integer("issue_num") + SEP 
-                      + string_data("sev") + SEP 
-                      + string_data("desc") + SEP 
-                      + integer("days_open"))
-        
-        for tkt in ticket_expr.searchString(report):
-            print tkt.dump()
-    prints::
-        ['101', 'Critical', 'Intermittent system crash', '6']
-        - days_open: 6
-        - desc: Intermittent system crash
-        - issue_num: 101
-        - sev: Critical
-        ['94', 'Cosmetic', "Spelling error on Login ('log|n')", '14']
-        - days_open: 14
-        - desc: Spelling error on Login ('log|n')
-        - issue_num: 94
-        - sev: Cosmetic
-        ['79', 'Minor', 'System slow when running too many reports', '47']
-        - days_open: 47
-        - desc: System slow when running too many reports
-        - issue_num: 79
-        - sev: Minor
-    """
-    def __init__( self, other, include=False, ignore=None, failOn=None ):
-        super( SkipTo, self ).__init__( other )
-        self.ignoreExpr = ignore
-        self.mayReturnEmpty = True
-        self.mayIndexError = False
-        self.includeMatch = include
-        self.asList = False
-        if isinstance(failOn, basestring):
-            self.failOn = ParserElement._literalStringClass(failOn)
-        else:
-            self.failOn = failOn
-        self.errmsg = "No match found for "+_ustr(self.expr)
-
-    def parseImpl( self, instring, loc, doActions=True ):
-        startloc = loc
-        instrlen = len(instring)
-        expr = self.expr
-        expr_parse = self.expr._parse
-        self_failOn_canParseNext = self.failOn.canParseNext if self.failOn is not None else None
-        self_ignoreExpr_tryParse = self.ignoreExpr.tryParse if self.ignoreExpr is not None else None
-        
-        tmploc = loc
-        while tmploc <= instrlen:
-            if self_failOn_canParseNext is not None:
-                # break if failOn expression matches
-                if self_failOn_canParseNext(instring, tmploc):
-                    break
-                    
-            if self_ignoreExpr_tryParse is not None:
-                # advance past ignore expressions
-                while 1:
-                    try:
-                        tmploc = self_ignoreExpr_tryParse(instring, tmploc)
-                    except ParseBaseException:
-                        break
-            
-            try:
-                expr_parse(instring, tmploc, doActions=False, callPreParse=False)
-            except (ParseException, IndexError):
-                # no match, advance loc in string
-                tmploc += 1
-            else:
-                # matched skipto expr, done
-                break
-
-        else:
-            # ran off the end of the input string without matching skipto expr, fail
-            raise ParseException(instring, loc, self.errmsg, self)
-
-        # build up return values
-        loc = tmploc
-        skiptext = instring[startloc:loc]
-        skipresult = ParseResults(skiptext)
-        
-        if self.includeMatch:
-            loc, mat = expr_parse(instring,loc,doActions,callPreParse=False)
-            skipresult += mat
-
-        return loc, skipresult
-
-class Forward(ParseElementEnhance):
-    """
-    Forward declaration of an expression to be defined later -
-    used for recursive grammars, such as algebraic infix notation.
-    When the expression is known, it is assigned to the C{Forward} variable using the '<<' operator.
-
-    Note: take care when assigning to C{Forward} not to overlook precedence of operators.
-    Specifically, '|' has a lower precedence than '<<', so that::
-        fwdExpr << a | b | c
-    will actually be evaluated as::
-        (fwdExpr << a) | b | c
-    thereby leaving b and c out as parseable alternatives.  It is recommended that you
-    explicitly group the values inserted into the C{Forward}::
-        fwdExpr << (a | b | c)
-    Converting to use the '<<=' operator instead will avoid this problem.
-
-    See L{ParseResults.pprint} for an example of a recursive parser created using
-    C{Forward}.
-    """
-    def __init__( self, other=None ):
-        super(Forward,self).__init__( other, savelist=False )
-
-    def __lshift__( self, other ):
-        if isinstance( other, basestring ):
-            other = ParserElement._literalStringClass(other)
-        self.expr = other
-        self.strRepr = None
-        self.mayIndexError = self.expr.mayIndexError
-        self.mayReturnEmpty = self.expr.mayReturnEmpty
-        self.setWhitespaceChars( self.expr.whiteChars )
-        self.skipWhitespace = self.expr.skipWhitespace
-        self.saveAsList = self.expr.saveAsList
-        self.ignoreExprs.extend(self.expr.ignoreExprs)
-        return self
-        
-    def __ilshift__(self, other):
-        return self << other
-    
-    def leaveWhitespace( self ):
-        self.skipWhitespace = False
-        return self
-
-    def streamline( self ):
-        if not self.streamlined:
-            self.streamlined = True
-            if self.expr is not None:
-                self.expr.streamline()
-        return self
-
-    def validate( self, validateTrace=[] ):
-        if self not in validateTrace:
-            tmp = validateTrace[:]+[self]
-            if self.expr is not None:
-                self.expr.validate(tmp)
-        self.checkRecursion([])
-
-    def __str__( self ):
-        if hasattr(self,"name"):
-            return self.name
-        return self.__class__.__name__ + ": ..."
-
-        # stubbed out for now - creates awful memory and perf issues
-        self._revertClass = self.__class__
-        self.__class__ = _ForwardNoRecurse
-        try:
-            if self.expr is not None:
-                retString = _ustr(self.expr)
-            else:
-                retString = "None"
-        finally:
-            self.__class__ = self._revertClass
-        return self.__class__.__name__ + ": " + retString
-
-    def copy(self):
-        if self.expr is not None:
-            return super(Forward,self).copy()
-        else:
-            ret = Forward()
-            ret <<= self
-            return ret
-
-class _ForwardNoRecurse(Forward):
-    def __str__( self ):
-        return "..."
-
-class TokenConverter(ParseElementEnhance):
-    """
-    Abstract subclass of C{ParseExpression}, for converting parsed results.
-    """
-    def __init__( self, expr, savelist=False ):
-        super(TokenConverter,self).__init__( expr )#, savelist )
-        self.saveAsList = False
-
-class Combine(TokenConverter):
-    """
-    Converter to concatenate all matching tokens to a single string.
-    By default, the matching patterns must also be contiguous in the input string;
-    this can be disabled by specifying C{'adjacent=False'} in the constructor.
-
-    Example::
-        real = Word(nums) + '.' + Word(nums)
-        print(real.parseString('3.1416')) # -> ['3', '.', '1416']
-        # will also erroneously match the following
-        print(real.parseString('3. 1416')) # -> ['3', '.', '1416']
-
-        real = Combine(Word(nums) + '.' + Word(nums))
-        print(real.parseString('3.1416')) # -> ['3.1416']
-        # no match when there are internal spaces
-        print(real.parseString('3. 1416')) # -> Exception: Expected W:(0123...)
-    """
-    def __init__( self, expr, joinString="", adjacent=True ):
-        super(Combine,self).__init__( expr )
-        # suppress whitespace-stripping in contained parse expressions, but re-enable it on the Combine itself
-        if adjacent:
-            self.leaveWhitespace()
-        self.adjacent = adjacent
-        self.skipWhitespace = True
-        self.joinString = joinString
-        self.callPreparse = True
-
-    def ignore( self, other ):
-        if self.adjacent:
-            ParserElement.ignore(self, other)
-        else:
-            super( Combine, self).ignore( other )
-        return self
-
-    def postParse( self, instring, loc, tokenlist ):
-        retToks = tokenlist.copy()
-        del retToks[:]
-        retToks += ParseResults([ "".join(tokenlist._asStringList(self.joinString)) ], modal=self.modalResults)
-
-        if self.resultsName and retToks.haskeys():
-            return [ retToks ]
-        else:
-            return retToks
-
-class Group(TokenConverter):
-    """
-    Converter to return the matched tokens as a list - useful for returning tokens of C{L{ZeroOrMore}} and C{L{OneOrMore}} expressions.
-
-    Example::
-        ident = Word(alphas)
-        num = Word(nums)
-        term = ident | num
-        func = ident + Optional(delimitedList(term))
-        print(func.parseString("fn a,b,100"))  # -> ['fn', 'a', 'b', '100']
-
-        func = ident + Group(Optional(delimitedList(term)))
-        print(func.parseString("fn a,b,100"))  # -> ['fn', ['a', 'b', '100']]
-    """
-    def __init__( self, expr ):
-        super(Group,self).__init__( expr )
-        self.saveAsList = True
-
-    def postParse( self, instring, loc, tokenlist ):
-        return [ tokenlist ]
-
-class Dict(TokenConverter):
-    """
-    Converter to return a repetitive expression as a list, but also as a dictionary.
-    Each element can also be referenced using the first token in the expression as its key.
-    Useful for tabular report scraping when the first column can be used as a item key.
-
-    Example::
-        data_word = Word(alphas)
-        label = data_word + FollowedBy(':')
-        attr_expr = Group(label + Suppress(':') + OneOrMore(data_word).setParseAction(' '.join))
-
-        text = "shape: SQUARE posn: upper left color: light blue texture: burlap"
-        attr_expr = (label + Suppress(':') + OneOrMore(data_word, stopOn=label).setParseAction(' '.join))
-        
-        # print attributes as plain groups
-        print(OneOrMore(attr_expr).parseString(text).dump())
-        
-        # instead of OneOrMore(expr), parse using Dict(OneOrMore(Group(expr))) - Dict will auto-assign names
-        result = Dict(OneOrMore(Group(attr_expr))).parseString(text)
-        print(result.dump())
-        
-        # access named fields as dict entries, or output as dict
-        print(result['shape'])        
-        print(result.asDict())
-    prints::
-        ['shape', 'SQUARE', 'posn', 'upper left', 'color', 'light blue', 'texture', 'burlap']
-
-        [['shape', 'SQUARE'], ['posn', 'upper left'], ['color', 'light blue'], ['texture', 'burlap']]
-        - color: light blue
-        - posn: upper left
-        - shape: SQUARE
-        - texture: burlap
-        SQUARE
-        {'color': 'light blue', 'posn': 'upper left', 'texture': 'burlap', 'shape': 'SQUARE'}
-    See more examples at L{ParseResults} of accessing fields by results name.
-    """
-    def __init__( self, expr ):
-        super(Dict,self).__init__( expr )
-        self.saveAsList = True
-
-    def postParse( self, instring, loc, tokenlist ):
-        for i,tok in enumerate(tokenlist):
-            if len(tok) == 0:
-                continue
-            ikey = tok[0]
-            if isinstance(ikey,int):
-                ikey = _ustr(tok[0]).strip()
-            if len(tok)==1:
-                tokenlist[ikey] = _ParseResultsWithOffset("",i)
-            elif len(tok)==2 and not isinstance(tok[1],ParseResults):
-                tokenlist[ikey] = _ParseResultsWithOffset(tok[1],i)
-            else:
-                dictvalue = tok.copy() #ParseResults(i)
-                del dictvalue[0]
-                if len(dictvalue)!= 1 or (isinstance(dictvalue,ParseResults) and dictvalue.haskeys()):
-                    tokenlist[ikey] = _ParseResultsWithOffset(dictvalue,i)
-                else:
-                    tokenlist[ikey] = _ParseResultsWithOffset(dictvalue[0],i)
-
-        if self.resultsName:
-            return [ tokenlist ]
-        else:
-            return tokenlist
-
-
-class Suppress(TokenConverter):
-    """
-    Converter for ignoring the results of a parsed expression.
-
-    Example::
-        source = "a, b, c,d"
-        wd = Word(alphas)
-        wd_list1 = wd + ZeroOrMore(',' + wd)
-        print(wd_list1.parseString(source))
-
-        # often, delimiters that are useful during parsing are just in the
-        # way afterward - use Suppress to keep them out of the parsed output
-        wd_list2 = wd + ZeroOrMore(Suppress(',') + wd)
-        print(wd_list2.parseString(source))
-    prints::
-        ['a', ',', 'b', ',', 'c', ',', 'd']
-        ['a', 'b', 'c', 'd']
-    (See also L{delimitedList}.)
-    """
-    def postParse( self, instring, loc, tokenlist ):
-        return []
-
-    def suppress( self ):
-        return self
-
-
-class OnlyOnce(object):
-    """
-    Wrapper for parse actions, to ensure they are only called once.
-    """
-    def __init__(self, methodCall):
-        self.callable = _trim_arity(methodCall)
-        self.called = False
-    def __call__(self,s,l,t):
-        if not self.called:
-            results = self.callable(s,l,t)
-            self.called = True
-            return results
-        raise ParseException(s,l,"")
-    def reset(self):
-        self.called = False
-
-def traceParseAction(f):
-    """
-    Decorator for debugging parse actions. 
-    
-    When the parse action is called, this decorator will print C{">> entering I{method-name}(line:I{current_source_line}, I{parse_location}, I{matched_tokens})".}
-    When the parse action completes, the decorator will print C{"<<"} followed by the returned value, or any exception that the parse action raised.
-
-    Example::
-        wd = Word(alphas)
-
-        @traceParseAction
-        def remove_duplicate_chars(tokens):
-            return ''.join(sorted(set(''.join(tokens))))
-
-        wds = OneOrMore(wd).setParseAction(remove_duplicate_chars)
-        print(wds.parseString("slkdjs sld sldd sdlf sdljf"))
-    prints::
-        >>entering remove_duplicate_chars(line: 'slkdjs sld sldd sdlf sdljf', 0, (['slkdjs', 'sld', 'sldd', 'sdlf', 'sdljf'], {}))
-        <3:
-            thisFunc = paArgs[0].__class__.__name__ + '.' + thisFunc
-        sys.stderr.write( ">>entering %s(line: '%s', %d, %r)\n" % (thisFunc,line(l,s),l,t) )
-        try:
-            ret = f(*paArgs)
-        except Exception as exc:
-            sys.stderr.write( "< ['aa', 'bb', 'cc']
-        delimitedList(Word(hexnums), delim=':', combine=True).parseString("AA:BB:CC:DD:EE") # -> ['AA:BB:CC:DD:EE']
-    """
-    dlName = _ustr(expr)+" ["+_ustr(delim)+" "+_ustr(expr)+"]..."
-    if combine:
-        return Combine( expr + ZeroOrMore( delim + expr ) ).setName(dlName)
-    else:
-        return ( expr + ZeroOrMore( Suppress( delim ) + expr ) ).setName(dlName)
-
-def countedArray( expr, intExpr=None ):
-    """
-    Helper to define a counted list of expressions.
-    This helper defines a pattern of the form::
-        integer expr expr expr...
-    where the leading integer tells how many expr expressions follow.
-    The matched tokens returns the array of expr tokens as a list - the leading count token is suppressed.
-    
-    If C{intExpr} is specified, it should be a pyparsing expression that produces an integer value.
-
-    Example::
-        countedArray(Word(alphas)).parseString('2 ab cd ef')  # -> ['ab', 'cd']
-
-        # in this parser, the leading integer value is given in binary,
-        # '10' indicating that 2 values are in the array
-        binaryConstant = Word('01').setParseAction(lambda t: int(t[0], 2))
-        countedArray(Word(alphas), intExpr=binaryConstant).parseString('10 ab cd ef')  # -> ['ab', 'cd']
-    """
-    arrayExpr = Forward()
-    def countFieldParseAction(s,l,t):
-        n = t[0]
-        arrayExpr << (n and Group(And([expr]*n)) or Group(empty))
-        return []
-    if intExpr is None:
-        intExpr = Word(nums).setParseAction(lambda t:int(t[0]))
-    else:
-        intExpr = intExpr.copy()
-    intExpr.setName("arrayLen")
-    intExpr.addParseAction(countFieldParseAction, callDuringTry=True)
-    return ( intExpr + arrayExpr ).setName('(len) ' + _ustr(expr) + '...')
-
-def _flatten(L):
-    ret = []
-    for i in L:
-        if isinstance(i,list):
-            ret.extend(_flatten(i))
-        else:
-            ret.append(i)
-    return ret
-
-def matchPreviousLiteral(expr):
-    """
-    Helper to define an expression that is indirectly defined from
-    the tokens matched in a previous expression, that is, it looks
-    for a 'repeat' of a previous expression.  For example::
-        first = Word(nums)
-        second = matchPreviousLiteral(first)
-        matchExpr = first + ":" + second
-    will match C{"1:1"}, but not C{"1:2"}.  Because this matches a
-    previous literal, will also match the leading C{"1:1"} in C{"1:10"}.
-    If this is not desired, use C{matchPreviousExpr}.
-    Do I{not} use with packrat parsing enabled.
-    """
-    rep = Forward()
-    def copyTokenToRepeater(s,l,t):
-        if t:
-            if len(t) == 1:
-                rep << t[0]
-            else:
-                # flatten t tokens
-                tflat = _flatten(t.asList())
-                rep << And(Literal(tt) for tt in tflat)
-        else:
-            rep << Empty()
-    expr.addParseAction(copyTokenToRepeater, callDuringTry=True)
-    rep.setName('(prev) ' + _ustr(expr))
-    return rep
-
-def matchPreviousExpr(expr):
-    """
-    Helper to define an expression that is indirectly defined from
-    the tokens matched in a previous expression, that is, it looks
-    for a 'repeat' of a previous expression.  For example::
-        first = Word(nums)
-        second = matchPreviousExpr(first)
-        matchExpr = first + ":" + second
-    will match C{"1:1"}, but not C{"1:2"}.  Because this matches by
-    expressions, will I{not} match the leading C{"1:1"} in C{"1:10"};
-    the expressions are evaluated first, and then compared, so
-    C{"1"} is compared with C{"10"}.
-    Do I{not} use with packrat parsing enabled.
-    """
-    rep = Forward()
-    e2 = expr.copy()
-    rep <<= e2
-    def copyTokenToRepeater(s,l,t):
-        matchTokens = _flatten(t.asList())
-        def mustMatchTheseTokens(s,l,t):
-            theseTokens = _flatten(t.asList())
-            if  theseTokens != matchTokens:
-                raise ParseException("",0,"")
-        rep.setParseAction( mustMatchTheseTokens, callDuringTry=True )
-    expr.addParseAction(copyTokenToRepeater, callDuringTry=True)
-    rep.setName('(prev) ' + _ustr(expr))
-    return rep
-
-def _escapeRegexRangeChars(s):
-    #~  escape these chars: ^-]
-    for c in r"\^-]":
-        s = s.replace(c,_bslash+c)
-    s = s.replace("\n",r"\n")
-    s = s.replace("\t",r"\t")
-    return _ustr(s)
-
-def oneOf( strs, caseless=False, useRegex=True ):
-    """
-    Helper to quickly define a set of alternative Literals, and makes sure to do
-    longest-first testing when there is a conflict, regardless of the input order,
-    but returns a C{L{MatchFirst}} for best performance.
-
-    Parameters:
-     - strs - a string of space-delimited literals, or a collection of string literals
-     - caseless - (default=C{False}) - treat all literals as caseless
-     - useRegex - (default=C{True}) - as an optimization, will generate a Regex
-          object; otherwise, will generate a C{MatchFirst} object (if C{caseless=True}, or
-          if creating a C{Regex} raises an exception)
-
-    Example::
-        comp_oper = oneOf("< = > <= >= !=")
-        var = Word(alphas)
-        number = Word(nums)
-        term = var | number
-        comparison_expr = term + comp_oper + term
-        print(comparison_expr.searchString("B = 12  AA=23 B<=AA AA>12"))
-    prints::
-        [['B', '=', '12'], ['AA', '=', '23'], ['B', '<=', 'AA'], ['AA', '>', '12']]
-    """
-    if caseless:
-        isequal = ( lambda a,b: a.upper() == b.upper() )
-        masks = ( lambda a,b: b.upper().startswith(a.upper()) )
-        parseElementClass = CaselessLiteral
-    else:
-        isequal = ( lambda a,b: a == b )
-        masks = ( lambda a,b: b.startswith(a) )
-        parseElementClass = Literal
-
-    symbols = []
-    if isinstance(strs,basestring):
-        symbols = strs.split()
-    elif isinstance(strs, Iterable):
-        symbols = list(strs)
-    else:
-        warnings.warn("Invalid argument to oneOf, expected string or iterable",
-                SyntaxWarning, stacklevel=2)
-    if not symbols:
-        return NoMatch()
-
-    i = 0
-    while i < len(symbols)-1:
-        cur = symbols[i]
-        for j,other in enumerate(symbols[i+1:]):
-            if ( isequal(other, cur) ):
-                del symbols[i+j+1]
-                break
-            elif ( masks(cur, other) ):
-                del symbols[i+j+1]
-                symbols.insert(i,other)
-                cur = other
-                break
-        else:
-            i += 1
-
-    if not caseless and useRegex:
-        #~ print (strs,"->", "|".join( [ _escapeRegexChars(sym) for sym in symbols] ))
-        try:
-            if len(symbols)==len("".join(symbols)):
-                return Regex( "[%s]" % "".join(_escapeRegexRangeChars(sym) for sym in symbols) ).setName(' | '.join(symbols))
-            else:
-                return Regex( "|".join(re.escape(sym) for sym in symbols) ).setName(' | '.join(symbols))
-        except Exception:
-            warnings.warn("Exception creating Regex for oneOf, building MatchFirst",
-                    SyntaxWarning, stacklevel=2)
-
-
-    # last resort, just use MatchFirst
-    return MatchFirst(parseElementClass(sym) for sym in symbols).setName(' | '.join(symbols))
-
-def dictOf( key, value ):
-    """
-    Helper to easily and clearly define a dictionary by specifying the respective patterns
-    for the key and value.  Takes care of defining the C{L{Dict}}, C{L{ZeroOrMore}}, and C{L{Group}} tokens
-    in the proper order.  The key pattern can include delimiting markers or punctuation,
-    as long as they are suppressed, thereby leaving the significant key text.  The value
-    pattern can include named results, so that the C{Dict} results can include named token
-    fields.
-
-    Example::
-        text = "shape: SQUARE posn: upper left color: light blue texture: burlap"
-        attr_expr = (label + Suppress(':') + OneOrMore(data_word, stopOn=label).setParseAction(' '.join))
-        print(OneOrMore(attr_expr).parseString(text).dump())
-        
-        attr_label = label
-        attr_value = Suppress(':') + OneOrMore(data_word, stopOn=label).setParseAction(' '.join)
-
-        # similar to Dict, but simpler call format
-        result = dictOf(attr_label, attr_value).parseString(text)
-        print(result.dump())
-        print(result['shape'])
-        print(result.shape)  # object attribute access works too
-        print(result.asDict())
-    prints::
-        [['shape', 'SQUARE'], ['posn', 'upper left'], ['color', 'light blue'], ['texture', 'burlap']]
-        - color: light blue
-        - posn: upper left
-        - shape: SQUARE
-        - texture: burlap
-        SQUARE
-        SQUARE
-        {'color': 'light blue', 'shape': 'SQUARE', 'posn': 'upper left', 'texture': 'burlap'}
-    """
-    return Dict( ZeroOrMore( Group ( key + value ) ) )
-
-def originalTextFor(expr, asString=True):
-    """
-    Helper to return the original, untokenized text for a given expression.  Useful to
-    restore the parsed fields of an HTML start tag into the raw tag text itself, or to
-    revert separate tokens with intervening whitespace back to the original matching
-    input text. By default, returns astring containing the original parsed text.  
-       
-    If the optional C{asString} argument is passed as C{False}, then the return value is a 
-    C{L{ParseResults}} containing any results names that were originally matched, and a 
-    single token containing the original matched text from the input string.  So if 
-    the expression passed to C{L{originalTextFor}} contains expressions with defined
-    results names, you must set C{asString} to C{False} if you want to preserve those
-    results name values.
-
-    Example::
-        src = "this is test  bold text  normal text "
-        for tag in ("b","i"):
-            opener,closer = makeHTMLTags(tag)
-            patt = originalTextFor(opener + SkipTo(closer) + closer)
-            print(patt.searchString(src)[0])
-    prints::
-        [' bold text ']
-        ['text']
-    """
-    locMarker = Empty().setParseAction(lambda s,loc,t: loc)
-    endlocMarker = locMarker.copy()
-    endlocMarker.callPreparse = False
-    matchExpr = locMarker("_original_start") + expr + endlocMarker("_original_end")
-    if asString:
-        extractText = lambda s,l,t: s[t._original_start:t._original_end]
-    else:
-        def extractText(s,l,t):
-            t[:] = [s[t.pop('_original_start'):t.pop('_original_end')]]
-    matchExpr.setParseAction(extractText)
-    matchExpr.ignoreExprs = expr.ignoreExprs
-    return matchExpr
-
-def ungroup(expr): 
-    """
-    Helper to undo pyparsing's default grouping of And expressions, even
-    if all but one are non-empty.
-    """
-    return TokenConverter(expr).setParseAction(lambda t:t[0])
-
-def locatedExpr(expr):
-    """
-    Helper to decorate a returned token with its starting and ending locations in the input string.
-    This helper adds the following results names:
-     - locn_start = location where matched expression begins
-     - locn_end = location where matched expression ends
-     - value = the actual parsed results
-
-    Be careful if the input text contains C{} characters, you may want to call
-    C{L{ParserElement.parseWithTabs}}
-
-    Example::
-        wd = Word(alphas)
-        for match in locatedExpr(wd).searchString("ljsdf123lksdjjf123lkkjj1222"):
-            print(match)
-    prints::
-        [[0, 'ljsdf', 5]]
-        [[8, 'lksdjjf', 15]]
-        [[18, 'lkkjj', 23]]
-    """
-    locator = Empty().setParseAction(lambda s,l,t: l)
-    return Group(locator("locn_start") + expr("value") + locator.copy().leaveWhitespace()("locn_end"))
-
-
-# convenience constants for positional expressions
-empty       = Empty().setName("empty")
-lineStart   = LineStart().setName("lineStart")
-lineEnd     = LineEnd().setName("lineEnd")
-stringStart = StringStart().setName("stringStart")
-stringEnd   = StringEnd().setName("stringEnd")
-
-_escapedPunc = Word( _bslash, r"\[]-*.$+^?()~ ", exact=2 ).setParseAction(lambda s,l,t:t[0][1])
-_escapedHexChar = Regex(r"\\0?[xX][0-9a-fA-F]+").setParseAction(lambda s,l,t:unichr(int(t[0].lstrip(r'\0x'),16)))
-_escapedOctChar = Regex(r"\\0[0-7]+").setParseAction(lambda s,l,t:unichr(int(t[0][1:],8)))
-_singleChar = _escapedPunc | _escapedHexChar | _escapedOctChar | CharsNotIn(r'\]', exact=1)
-_charRange = Group(_singleChar + Suppress("-") + _singleChar)
-_reBracketExpr = Literal("[") + Optional("^").setResultsName("negate") + Group( OneOrMore( _charRange | _singleChar ) ).setResultsName("body") + "]"
-
-def srange(s):
-    r"""
-    Helper to easily define string ranges for use in Word construction.  Borrows
-    syntax from regexp '[]' string range definitions::
-        srange("[0-9]")   -> "0123456789"
-        srange("[a-z]")   -> "abcdefghijklmnopqrstuvwxyz"
-        srange("[a-z$_]") -> "abcdefghijklmnopqrstuvwxyz$_"
-    The input string must be enclosed in []'s, and the returned string is the expanded
-    character set joined into a single string.
-    The values enclosed in the []'s may be:
-     - a single character
-     - an escaped character with a leading backslash (such as C{\-} or C{\]})
-     - an escaped hex character with a leading C{'\x'} (C{\x21}, which is a C{'!'} character) 
-         (C{\0x##} is also supported for backwards compatibility) 
-     - an escaped octal character with a leading C{'\0'} (C{\041}, which is a C{'!'} character)
-     - a range of any of the above, separated by a dash (C{'a-z'}, etc.)
-     - any combination of the above (C{'aeiouy'}, C{'a-zA-Z0-9_$'}, etc.)
-    """
-    _expanded = lambda p: p if not isinstance(p,ParseResults) else ''.join(unichr(c) for c in range(ord(p[0]),ord(p[1])+1))
-    try:
-        return "".join(_expanded(part) for part in _reBracketExpr.parseString(s).body)
-    except Exception:
-        return ""
-
-def matchOnlyAtCol(n):
-    """
-    Helper method for defining parse actions that require matching at a specific
-    column in the input text.
-    """
-    def verifyCol(strg,locn,toks):
-        if col(locn,strg) != n:
-            raise ParseException(strg,locn,"matched token not at column %d" % n)
-    return verifyCol
-
-def replaceWith(replStr):
-    """
-    Helper method for common parse actions that simply return a literal value.  Especially
-    useful when used with C{L{transformString}()}.
-
-    Example::
-        num = Word(nums).setParseAction(lambda toks: int(toks[0]))
-        na = oneOf("N/A NA").setParseAction(replaceWith(math.nan))
-        term = na | num
-        
-        OneOrMore(term).parseString("324 234 N/A 234") # -> [324, 234, nan, 234]
-    """
-    return lambda s,l,t: [replStr]
-
-def removeQuotes(s,l,t):
-    """
-    Helper parse action for removing quotation marks from parsed quoted strings.
-
-    Example::
-        # by default, quotation marks are included in parsed results
-        quotedString.parseString("'Now is the Winter of our Discontent'") # -> ["'Now is the Winter of our Discontent'"]
-
-        # use removeQuotes to strip quotation marks from parsed results
-        quotedString.setParseAction(removeQuotes)
-        quotedString.parseString("'Now is the Winter of our Discontent'") # -> ["Now is the Winter of our Discontent"]
-    """
-    return t[0][1:-1]
-
-def tokenMap(func, *args):
-    """
-    Helper to define a parse action by mapping a function to all elements of a ParseResults list.If any additional 
-    args are passed, they are forwarded to the given function as additional arguments after
-    the token, as in C{hex_integer = Word(hexnums).setParseAction(tokenMap(int, 16))}, which will convert the
-    parsed data to an integer using base 16.
-
-    Example (compare the last to example in L{ParserElement.transformString}::
-        hex_ints = OneOrMore(Word(hexnums)).setParseAction(tokenMap(int, 16))
-        hex_ints.runTests('''
-            00 11 22 aa FF 0a 0d 1a
-            ''')
-        
-        upperword = Word(alphas).setParseAction(tokenMap(str.upper))
-        OneOrMore(upperword).runTests('''
-            my kingdom for a horse
-            ''')
-
-        wd = Word(alphas).setParseAction(tokenMap(str.title))
-        OneOrMore(wd).setParseAction(' '.join).runTests('''
-            now is the winter of our discontent made glorious summer by this sun of york
-            ''')
-    prints::
-        00 11 22 aa FF 0a 0d 1a
-        [0, 17, 34, 170, 255, 10, 13, 26]
-
-        my kingdom for a horse
-        ['MY', 'KINGDOM', 'FOR', 'A', 'HORSE']
-
-        now is the winter of our discontent made glorious summer by this sun of york
-        ['Now Is The Winter Of Our Discontent Made Glorious Summer By This Sun Of York']
-    """
-    def pa(s,l,t):
-        return [func(tokn, *args) for tokn in t]
-
-    try:
-        func_name = getattr(func, '__name__', 
-                            getattr(func, '__class__').__name__)
-    except Exception:
-        func_name = str(func)
-    pa.__name__ = func_name
-
-    return pa
-
-upcaseTokens = tokenMap(lambda t: _ustr(t).upper())
-"""(Deprecated) Helper parse action to convert tokens to upper case. Deprecated in favor of L{pyparsing_common.upcaseTokens}"""
-
-downcaseTokens = tokenMap(lambda t: _ustr(t).lower())
-"""(Deprecated) Helper parse action to convert tokens to lower case. Deprecated in favor of L{pyparsing_common.downcaseTokens}"""
-    
-def _makeTags(tagStr, xml):
-    """Internal helper to construct opening and closing tag expressions, given a tag name"""
-    if isinstance(tagStr,basestring):
-        resname = tagStr
-        tagStr = Keyword(tagStr, caseless=not xml)
-    else:
-        resname = tagStr.name
-
-    tagAttrName = Word(alphas,alphanums+"_-:")
-    if (xml):
-        tagAttrValue = dblQuotedString.copy().setParseAction( removeQuotes )
-        openTag = Suppress("<") + tagStr("tag") + \
-                Dict(ZeroOrMore(Group( tagAttrName + Suppress("=") + tagAttrValue ))) + \
-                Optional("/",default=[False]).setResultsName("empty").setParseAction(lambda s,l,t:t[0]=='/') + Suppress(">")
-    else:
-        printablesLessRAbrack = "".join(c for c in printables if c not in ">")
-        tagAttrValue = quotedString.copy().setParseAction( removeQuotes ) | Word(printablesLessRAbrack)
-        openTag = Suppress("<") + tagStr("tag") + \
-                Dict(ZeroOrMore(Group( tagAttrName.setParseAction(downcaseTokens) + \
-                Optional( Suppress("=") + tagAttrValue ) ))) + \
-                Optional("/",default=[False]).setResultsName("empty").setParseAction(lambda s,l,t:t[0]=='/') + Suppress(">")
-    closeTag = Combine(_L("")
-
-    openTag = openTag.setResultsName("start"+"".join(resname.replace(":"," ").title().split())).setName("<%s>" % resname)
-    closeTag = closeTag.setResultsName("end"+"".join(resname.replace(":"," ").title().split())).setName("" % resname)
-    openTag.tag = resname
-    closeTag.tag = resname
-    return openTag, closeTag
-
-def makeHTMLTags(tagStr):
-    """
-    Helper to construct opening and closing tag expressions for HTML, given a tag name. Matches
-    tags in either upper or lower case, attributes with namespaces and with quoted or unquoted values.
-
-    Example::
-        text = 'More info at the pyparsing wiki page'
-        # makeHTMLTags returns pyparsing expressions for the opening and closing tags as a 2-tuple
-        a,a_end = makeHTMLTags("A")
-        link_expr = a + SkipTo(a_end)("link_text") + a_end
-        
-        for link in link_expr.searchString(text):
-            # attributes in the  tag (like "href" shown here) are also accessible as named results
-            print(link.link_text, '->', link.href)
-    prints::
-        pyparsing -> http://pyparsing.wikispaces.com
-    """
-    return _makeTags( tagStr, False )
-
-def makeXMLTags(tagStr):
-    """
-    Helper to construct opening and closing tag expressions for XML, given a tag name. Matches
-    tags only in the given upper/lower case.
-
-    Example: similar to L{makeHTMLTags}
-    """
-    return _makeTags( tagStr, True )
-
-def withAttribute(*args,**attrDict):
-    """
-    Helper to create a validating parse action to be used with start tags created
-    with C{L{makeXMLTags}} or C{L{makeHTMLTags}}. Use C{withAttribute} to qualify a starting tag
-    with a required attribute value, to avoid false matches on common tags such as
-    C{} or C{
}. - - Call C{withAttribute} with a series of attribute names and values. Specify the list - of filter attributes names and values as: - - keyword arguments, as in C{(align="right")}, or - - as an explicit dict with C{**} operator, when an attribute name is also a Python - reserved word, as in C{**{"class":"Customer", "align":"right"}} - - a list of name-value tuples, as in ( ("ns1:class", "Customer"), ("ns2:align","right") ) - For attribute names with a namespace prefix, you must use the second form. Attribute - names are matched insensitive to upper/lower case. - - If just testing for C{class} (with or without a namespace), use C{L{withClass}}. - - To verify that the attribute exists, but without specifying a value, pass - C{withAttribute.ANY_VALUE} as the value. - - Example:: - html = ''' -
- Some text -
1 4 0 1 0
-
1,3 2,3 1,1
-
this has no type
-
- - ''' - div,div_end = makeHTMLTags("div") - - # only match div tag having a type attribute with value "grid" - div_grid = div().setParseAction(withAttribute(type="grid")) - grid_expr = div_grid + SkipTo(div | div_end)("body") - for grid_header in grid_expr.searchString(html): - print(grid_header.body) - - # construct a match with any div tag having a type attribute, regardless of the value - div_any_type = div().setParseAction(withAttribute(type=withAttribute.ANY_VALUE)) - div_expr = div_any_type + SkipTo(div | div_end)("body") - for div_header in div_expr.searchString(html): - print(div_header.body) - prints:: - 1 4 0 1 0 - - 1 4 0 1 0 - 1,3 2,3 1,1 - """ - if args: - attrs = args[:] - else: - attrs = attrDict.items() - attrs = [(k,v) for k,v in attrs] - def pa(s,l,tokens): - for attrName,attrValue in attrs: - if attrName not in tokens: - raise ParseException(s,l,"no matching attribute " + attrName) - if attrValue != withAttribute.ANY_VALUE and tokens[attrName] != attrValue: - raise ParseException(s,l,"attribute '%s' has value '%s', must be '%s'" % - (attrName, tokens[attrName], attrValue)) - return pa -withAttribute.ANY_VALUE = object() - -def withClass(classname, namespace=''): - """ - Simplified version of C{L{withAttribute}} when matching on a div class - made - difficult because C{class} is a reserved word in Python. - - Example:: - html = ''' -
- Some text -
1 4 0 1 0
-
1,3 2,3 1,1
-
this <div> has no class
-
- - ''' - div,div_end = makeHTMLTags("div") - div_grid = div().setParseAction(withClass("grid")) - - grid_expr = div_grid + SkipTo(div | div_end)("body") - for grid_header in grid_expr.searchString(html): - print(grid_header.body) - - div_any_type = div().setParseAction(withClass(withAttribute.ANY_VALUE)) - div_expr = div_any_type + SkipTo(div | div_end)("body") - for div_header in div_expr.searchString(html): - print(div_header.body) - prints:: - 1 4 0 1 0 - - 1 4 0 1 0 - 1,3 2,3 1,1 - """ - classattr = "%s:class" % namespace if namespace else "class" - return withAttribute(**{classattr : classname}) - -opAssoc = _Constants() -opAssoc.LEFT = object() -opAssoc.RIGHT = object() - -def infixNotation( baseExpr, opList, lpar=Suppress('('), rpar=Suppress(')') ): - """ - Helper method for constructing grammars of expressions made up of - operators working in a precedence hierarchy. Operators may be unary or - binary, left- or right-associative. Parse actions can also be attached - to operator expressions. The generated parser will also recognize the use - of parentheses to override operator precedences (see example below). - - Note: if you define a deep operator list, you may see performance issues - when using infixNotation. See L{ParserElement.enablePackrat} for a - mechanism to potentially improve your parser performance. - - Parameters: - - baseExpr - expression representing the most basic element for the nested - - opList - list of tuples, one for each operator precedence level in the - expression grammar; each tuple is of the form - (opExpr, numTerms, rightLeftAssoc, parseAction), where: - - opExpr is the pyparsing expression for the operator; - may also be a string, which will be converted to a Literal; - if numTerms is 3, opExpr is a tuple of two expressions, for the - two operators separating the 3 terms - - numTerms is the number of terms for this operator (must - be 1, 2, or 3) - - rightLeftAssoc is the indicator whether the operator is - right or left associative, using the pyparsing-defined - constants C{opAssoc.RIGHT} and C{opAssoc.LEFT}. - - parseAction is the parse action to be associated with - expressions matching this operator expression (the - parse action tuple member may be omitted); if the parse action - is passed a tuple or list of functions, this is equivalent to - calling C{setParseAction(*fn)} (L{ParserElement.setParseAction}) - - lpar - expression for matching left-parentheses (default=C{Suppress('(')}) - - rpar - expression for matching right-parentheses (default=C{Suppress(')')}) - - Example:: - # simple example of four-function arithmetic with ints and variable names - integer = pyparsing_common.signed_integer - varname = pyparsing_common.identifier - - arith_expr = infixNotation(integer | varname, - [ - ('-', 1, opAssoc.RIGHT), - (oneOf('* /'), 2, opAssoc.LEFT), - (oneOf('+ -'), 2, opAssoc.LEFT), - ]) - - arith_expr.runTests(''' - 5+3*6 - (5+3)*6 - -2--11 - ''', fullDump=False) - prints:: - 5+3*6 - [[5, '+', [3, '*', 6]]] - - (5+3)*6 - [[[5, '+', 3], '*', 6]] - - -2--11 - [[['-', 2], '-', ['-', 11]]] - """ - ret = Forward() - lastExpr = baseExpr | ( lpar + ret + rpar ) - for i,operDef in enumerate(opList): - opExpr,arity,rightLeftAssoc,pa = (operDef + (None,))[:4] - termName = "%s term" % opExpr if arity < 3 else "%s%s term" % opExpr - if arity == 3: - if opExpr is None or len(opExpr) != 2: - raise ValueError("if numterms=3, opExpr must be a tuple or list of two expressions") - opExpr1, opExpr2 = opExpr - thisExpr = Forward().setName(termName) - if rightLeftAssoc == opAssoc.LEFT: - if arity == 1: - matchExpr = FollowedBy(lastExpr + opExpr) + Group( lastExpr + OneOrMore( opExpr ) ) - elif arity == 2: - if opExpr is not None: - matchExpr = FollowedBy(lastExpr + opExpr + lastExpr) + Group( lastExpr + OneOrMore( opExpr + lastExpr ) ) - else: - matchExpr = FollowedBy(lastExpr+lastExpr) + Group( lastExpr + OneOrMore(lastExpr) ) - elif arity == 3: - matchExpr = FollowedBy(lastExpr + opExpr1 + lastExpr + opExpr2 + lastExpr) + \ - Group( lastExpr + opExpr1 + lastExpr + opExpr2 + lastExpr ) - else: - raise ValueError("operator must be unary (1), binary (2), or ternary (3)") - elif rightLeftAssoc == opAssoc.RIGHT: - if arity == 1: - # try to avoid LR with this extra test - if not isinstance(opExpr, Optional): - opExpr = Optional(opExpr) - matchExpr = FollowedBy(opExpr.expr + thisExpr) + Group( opExpr + thisExpr ) - elif arity == 2: - if opExpr is not None: - matchExpr = FollowedBy(lastExpr + opExpr + thisExpr) + Group( lastExpr + OneOrMore( opExpr + thisExpr ) ) - else: - matchExpr = FollowedBy(lastExpr + thisExpr) + Group( lastExpr + OneOrMore( thisExpr ) ) - elif arity == 3: - matchExpr = FollowedBy(lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr) + \ - Group( lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr ) - else: - raise ValueError("operator must be unary (1), binary (2), or ternary (3)") - else: - raise ValueError("operator must indicate right or left associativity") - if pa: - if isinstance(pa, (tuple, list)): - matchExpr.setParseAction(*pa) - else: - matchExpr.setParseAction(pa) - thisExpr <<= ( matchExpr.setName(termName) | lastExpr ) - lastExpr = thisExpr - ret <<= lastExpr - return ret - -operatorPrecedence = infixNotation -"""(Deprecated) Former name of C{L{infixNotation}}, will be dropped in a future release.""" - -dblQuotedString = Combine(Regex(r'"(?:[^"\n\r\\]|(?:"")|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*')+'"').setName("string enclosed in double quotes") -sglQuotedString = Combine(Regex(r"'(?:[^'\n\r\\]|(?:'')|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*")+"'").setName("string enclosed in single quotes") -quotedString = Combine(Regex(r'"(?:[^"\n\r\\]|(?:"")|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*')+'"'| - Regex(r"'(?:[^'\n\r\\]|(?:'')|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*")+"'").setName("quotedString using single or double quotes") -unicodeString = Combine(_L('u') + quotedString.copy()).setName("unicode string literal") - -def nestedExpr(opener="(", closer=")", content=None, ignoreExpr=quotedString.copy()): - """ - Helper method for defining nested lists enclosed in opening and closing - delimiters ("(" and ")" are the default). - - Parameters: - - opener - opening character for a nested list (default=C{"("}); can also be a pyparsing expression - - closer - closing character for a nested list (default=C{")"}); can also be a pyparsing expression - - content - expression for items within the nested lists (default=C{None}) - - ignoreExpr - expression for ignoring opening and closing delimiters (default=C{quotedString}) - - If an expression is not provided for the content argument, the nested - expression will capture all whitespace-delimited content between delimiters - as a list of separate values. - - Use the C{ignoreExpr} argument to define expressions that may contain - opening or closing characters that should not be treated as opening - or closing characters for nesting, such as quotedString or a comment - expression. Specify multiple expressions using an C{L{Or}} or C{L{MatchFirst}}. - The default is L{quotedString}, but if no expressions are to be ignored, - then pass C{None} for this argument. - - Example:: - data_type = oneOf("void int short long char float double") - decl_data_type = Combine(data_type + Optional(Word('*'))) - ident = Word(alphas+'_', alphanums+'_') - number = pyparsing_common.number - arg = Group(decl_data_type + ident) - LPAR,RPAR = map(Suppress, "()") - - code_body = nestedExpr('{', '}', ignoreExpr=(quotedString | cStyleComment)) - - c_function = (decl_data_type("type") - + ident("name") - + LPAR + Optional(delimitedList(arg), [])("args") + RPAR - + code_body("body")) - c_function.ignore(cStyleComment) - - source_code = ''' - int is_odd(int x) { - return (x%2); - } - - int dec_to_hex(char hchar) { - if (hchar >= '0' && hchar <= '9') { - return (ord(hchar)-ord('0')); - } else { - return (10+ord(hchar)-ord('A')); - } - } - ''' - for func in c_function.searchString(source_code): - print("%(name)s (%(type)s) args: %(args)s" % func) - - prints:: - is_odd (int) args: [['int', 'x']] - dec_to_hex (int) args: [['char', 'hchar']] - """ - if opener == closer: - raise ValueError("opening and closing strings cannot be the same") - if content is None: - if isinstance(opener,basestring) and isinstance(closer,basestring): - if len(opener) == 1 and len(closer)==1: - if ignoreExpr is not None: - content = (Combine(OneOrMore(~ignoreExpr + - CharsNotIn(opener+closer+ParserElement.DEFAULT_WHITE_CHARS,exact=1)) - ).setParseAction(lambda t:t[0].strip())) - else: - content = (empty.copy()+CharsNotIn(opener+closer+ParserElement.DEFAULT_WHITE_CHARS - ).setParseAction(lambda t:t[0].strip())) - else: - if ignoreExpr is not None: - content = (Combine(OneOrMore(~ignoreExpr + - ~Literal(opener) + ~Literal(closer) + - CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS,exact=1)) - ).setParseAction(lambda t:t[0].strip())) - else: - content = (Combine(OneOrMore(~Literal(opener) + ~Literal(closer) + - CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS,exact=1)) - ).setParseAction(lambda t:t[0].strip())) - else: - raise ValueError("opening and closing arguments must be strings if no content expression is given") - ret = Forward() - if ignoreExpr is not None: - ret <<= Group( Suppress(opener) + ZeroOrMore( ignoreExpr | ret | content ) + Suppress(closer) ) - else: - ret <<= Group( Suppress(opener) + ZeroOrMore( ret | content ) + Suppress(closer) ) - ret.setName('nested %s%s expression' % (opener,closer)) - return ret - -def indentedBlock(blockStatementExpr, indentStack, indent=True): - """ - Helper method for defining space-delimited indentation blocks, such as - those used to define block statements in Python source code. - - Parameters: - - blockStatementExpr - expression defining syntax of statement that - is repeated within the indented block - - indentStack - list created by caller to manage indentation stack - (multiple statementWithIndentedBlock expressions within a single grammar - should share a common indentStack) - - indent - boolean indicating whether block must be indented beyond the - the current level; set to False for block of left-most statements - (default=C{True}) - - A valid block must contain at least one C{blockStatement}. - - Example:: - data = ''' - def A(z): - A1 - B = 100 - G = A2 - A2 - A3 - B - def BB(a,b,c): - BB1 - def BBA(): - bba1 - bba2 - bba3 - C - D - def spam(x,y): - def eggs(z): - pass - ''' - - - indentStack = [1] - stmt = Forward() - - identifier = Word(alphas, alphanums) - funcDecl = ("def" + identifier + Group( "(" + Optional( delimitedList(identifier) ) + ")" ) + ":") - func_body = indentedBlock(stmt, indentStack) - funcDef = Group( funcDecl + func_body ) - - rvalue = Forward() - funcCall = Group(identifier + "(" + Optional(delimitedList(rvalue)) + ")") - rvalue << (funcCall | identifier | Word(nums)) - assignment = Group(identifier + "=" + rvalue) - stmt << ( funcDef | assignment | identifier ) - - module_body = OneOrMore(stmt) - - parseTree = module_body.parseString(data) - parseTree.pprint() - prints:: - [['def', - 'A', - ['(', 'z', ')'], - ':', - [['A1'], [['B', '=', '100']], [['G', '=', 'A2']], ['A2'], ['A3']]], - 'B', - ['def', - 'BB', - ['(', 'a', 'b', 'c', ')'], - ':', - [['BB1'], [['def', 'BBA', ['(', ')'], ':', [['bba1'], ['bba2'], ['bba3']]]]]], - 'C', - 'D', - ['def', - 'spam', - ['(', 'x', 'y', ')'], - ':', - [[['def', 'eggs', ['(', 'z', ')'], ':', [['pass']]]]]]] - """ - def checkPeerIndent(s,l,t): - if l >= len(s): return - curCol = col(l,s) - if curCol != indentStack[-1]: - if curCol > indentStack[-1]: - raise ParseFatalException(s,l,"illegal nesting") - raise ParseException(s,l,"not a peer entry") - - def checkSubIndent(s,l,t): - curCol = col(l,s) - if curCol > indentStack[-1]: - indentStack.append( curCol ) - else: - raise ParseException(s,l,"not a subentry") - - def checkUnindent(s,l,t): - if l >= len(s): return - curCol = col(l,s) - if not(indentStack and curCol < indentStack[-1] and curCol <= indentStack[-2]): - raise ParseException(s,l,"not an unindent") - indentStack.pop() - - NL = OneOrMore(LineEnd().setWhitespaceChars("\t ").suppress()) - INDENT = (Empty() + Empty().setParseAction(checkSubIndent)).setName('INDENT') - PEER = Empty().setParseAction(checkPeerIndent).setName('') - UNDENT = Empty().setParseAction(checkUnindent).setName('UNINDENT') - if indent: - smExpr = Group( Optional(NL) + - #~ FollowedBy(blockStatementExpr) + - INDENT + (OneOrMore( PEER + Group(blockStatementExpr) + Optional(NL) )) + UNDENT) - else: - smExpr = Group( Optional(NL) + - (OneOrMore( PEER + Group(blockStatementExpr) + Optional(NL) )) ) - blockStatementExpr.ignore(_bslash + LineEnd()) - return smExpr.setName('indented block') - -alphas8bit = srange(r"[\0xc0-\0xd6\0xd8-\0xf6\0xf8-\0xff]") -punc8bit = srange(r"[\0xa1-\0xbf\0xd7\0xf7]") - -anyOpenTag,anyCloseTag = makeHTMLTags(Word(alphas,alphanums+"_:").setName('any tag')) -_htmlEntityMap = dict(zip("gt lt amp nbsp quot apos".split(),'><& "\'')) -commonHTMLEntity = Regex('&(?P' + '|'.join(_htmlEntityMap.keys()) +");").setName("common HTML entity") -def replaceHTMLEntity(t): - """Helper parser action to replace common HTML entities with their special characters""" - return _htmlEntityMap.get(t.entity) - -# it's easy to get these comment structures wrong - they're very common, so may as well make them available -cStyleComment = Combine(Regex(r"/\*(?:[^*]|\*(?!/))*") + '*/').setName("C style comment") -"Comment of the form C{/* ... */}" - -htmlComment = Regex(r"").setName("HTML comment") -"Comment of the form C{}" - -restOfLine = Regex(r".*").leaveWhitespace().setName("rest of line") -dblSlashComment = Regex(r"//(?:\\\n|[^\n])*").setName("// comment") -"Comment of the form C{// ... (to end of line)}" - -cppStyleComment = Combine(Regex(r"/\*(?:[^*]|\*(?!/))*") + '*/'| dblSlashComment).setName("C++ style comment") -"Comment of either form C{L{cStyleComment}} or C{L{dblSlashComment}}" - -javaStyleComment = cppStyleComment -"Same as C{L{cppStyleComment}}" - -pythonStyleComment = Regex(r"#.*").setName("Python style comment") -"Comment of the form C{# ... (to end of line)}" - -_commasepitem = Combine(OneOrMore(Word(printables, excludeChars=',') + - Optional( Word(" \t") + - ~Literal(",") + ~LineEnd() ) ) ).streamline().setName("commaItem") -commaSeparatedList = delimitedList( Optional( quotedString.copy() | _commasepitem, default="") ).setName("commaSeparatedList") -"""(Deprecated) Predefined expression of 1 or more printable words or quoted strings, separated by commas. - This expression is deprecated in favor of L{pyparsing_common.comma_separated_list}.""" - -# some other useful expressions - using lower-case class name since we are really using this as a namespace -class pyparsing_common: - """ - Here are some common low-level expressions that may be useful in jump-starting parser development: - - numeric forms (L{integers}, L{reals}, L{scientific notation}) - - common L{programming identifiers} - - network addresses (L{MAC}, L{IPv4}, L{IPv6}) - - ISO8601 L{dates} and L{datetime} - - L{UUID} - - L{comma-separated list} - Parse actions: - - C{L{convertToInteger}} - - C{L{convertToFloat}} - - C{L{convertToDate}} - - C{L{convertToDatetime}} - - C{L{stripHTMLTags}} - - C{L{upcaseTokens}} - - C{L{downcaseTokens}} - - Example:: - pyparsing_common.number.runTests(''' - # any int or real number, returned as the appropriate type - 100 - -100 - +100 - 3.14159 - 6.02e23 - 1e-12 - ''') - - pyparsing_common.fnumber.runTests(''' - # any int or real number, returned as float - 100 - -100 - +100 - 3.14159 - 6.02e23 - 1e-12 - ''') - - pyparsing_common.hex_integer.runTests(''' - # hex numbers - 100 - FF - ''') - - pyparsing_common.fraction.runTests(''' - # fractions - 1/2 - -3/4 - ''') - - pyparsing_common.mixed_integer.runTests(''' - # mixed fractions - 1 - 1/2 - -3/4 - 1-3/4 - ''') - - import uuid - pyparsing_common.uuid.setParseAction(tokenMap(uuid.UUID)) - pyparsing_common.uuid.runTests(''' - # uuid - 12345678-1234-5678-1234-567812345678 - ''') - prints:: - # any int or real number, returned as the appropriate type - 100 - [100] - - -100 - [-100] - - +100 - [100] - - 3.14159 - [3.14159] - - 6.02e23 - [6.02e+23] - - 1e-12 - [1e-12] - - # any int or real number, returned as float - 100 - [100.0] - - -100 - [-100.0] - - +100 - [100.0] - - 3.14159 - [3.14159] - - 6.02e23 - [6.02e+23] - - 1e-12 - [1e-12] - - # hex numbers - 100 - [256] - - FF - [255] - - # fractions - 1/2 - [0.5] - - -3/4 - [-0.75] - - # mixed fractions - 1 - [1] - - 1/2 - [0.5] - - -3/4 - [-0.75] - - 1-3/4 - [1.75] - - # uuid - 12345678-1234-5678-1234-567812345678 - [UUID('12345678-1234-5678-1234-567812345678')] - """ - - convertToInteger = tokenMap(int) - """ - Parse action for converting parsed integers to Python int - """ - - convertToFloat = tokenMap(float) - """ - Parse action for converting parsed numbers to Python float - """ - - integer = Word(nums).setName("integer").setParseAction(convertToInteger) - """expression that parses an unsigned integer, returns an int""" - - hex_integer = Word(hexnums).setName("hex integer").setParseAction(tokenMap(int,16)) - """expression that parses a hexadecimal integer, returns an int""" - - signed_integer = Regex(r'[+-]?\d+').setName("signed integer").setParseAction(convertToInteger) - """expression that parses an integer with optional leading sign, returns an int""" - - fraction = (signed_integer().setParseAction(convertToFloat) + '/' + signed_integer().setParseAction(convertToFloat)).setName("fraction") - """fractional expression of an integer divided by an integer, returns a float""" - fraction.addParseAction(lambda t: t[0]/t[-1]) - - mixed_integer = (fraction | signed_integer + Optional(Optional('-').suppress() + fraction)).setName("fraction or mixed integer-fraction") - """mixed integer of the form 'integer - fraction', with optional leading integer, returns float""" - mixed_integer.addParseAction(sum) - - real = Regex(r'[+-]?\d+\.\d*').setName("real number").setParseAction(convertToFloat) - """expression that parses a floating point number and returns a float""" - - sci_real = Regex(r'[+-]?\d+([eE][+-]?\d+|\.\d*([eE][+-]?\d+)?)').setName("real number with scientific notation").setParseAction(convertToFloat) - """expression that parses a floating point number with optional scientific notation and returns a float""" - - # streamlining this expression makes the docs nicer-looking - number = (sci_real | real | signed_integer).streamline() - """any numeric expression, returns the corresponding Python type""" - - fnumber = Regex(r'[+-]?\d+\.?\d*([eE][+-]?\d+)?').setName("fnumber").setParseAction(convertToFloat) - """any int or real number, returned as float""" - - identifier = Word(alphas+'_', alphanums+'_').setName("identifier") - """typical code identifier (leading alpha or '_', followed by 0 or more alphas, nums, or '_')""" - - ipv4_address = Regex(r'(25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})(\.(25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})){3}').setName("IPv4 address") - "IPv4 address (C{0.0.0.0 - 255.255.255.255})" - - _ipv6_part = Regex(r'[0-9a-fA-F]{1,4}').setName("hex_integer") - _full_ipv6_address = (_ipv6_part + (':' + _ipv6_part)*7).setName("full IPv6 address") - _short_ipv6_address = (Optional(_ipv6_part + (':' + _ipv6_part)*(0,6)) + "::" + Optional(_ipv6_part + (':' + _ipv6_part)*(0,6))).setName("short IPv6 address") - _short_ipv6_address.addCondition(lambda t: sum(1 for tt in t if pyparsing_common._ipv6_part.matches(tt)) < 8) - _mixed_ipv6_address = ("::ffff:" + ipv4_address).setName("mixed IPv6 address") - ipv6_address = Combine((_full_ipv6_address | _mixed_ipv6_address | _short_ipv6_address).setName("IPv6 address")).setName("IPv6 address") - "IPv6 address (long, short, or mixed form)" - - mac_address = Regex(r'[0-9a-fA-F]{2}([:.-])[0-9a-fA-F]{2}(?:\1[0-9a-fA-F]{2}){4}').setName("MAC address") - "MAC address xx:xx:xx:xx:xx (may also have '-' or '.' delimiters)" - - @staticmethod - def convertToDate(fmt="%Y-%m-%d"): - """ - Helper to create a parse action for converting parsed date string to Python datetime.date - - Params - - - fmt - format to be passed to datetime.strptime (default=C{"%Y-%m-%d"}) - - Example:: - date_expr = pyparsing_common.iso8601_date.copy() - date_expr.setParseAction(pyparsing_common.convertToDate()) - print(date_expr.parseString("1999-12-31")) - prints:: - [datetime.date(1999, 12, 31)] - """ - def cvt_fn(s,l,t): - try: - return datetime.strptime(t[0], fmt).date() - except ValueError as ve: - raise ParseException(s, l, str(ve)) - return cvt_fn - - @staticmethod - def convertToDatetime(fmt="%Y-%m-%dT%H:%M:%S.%f"): - """ - Helper to create a parse action for converting parsed datetime string to Python datetime.datetime - - Params - - - fmt - format to be passed to datetime.strptime (default=C{"%Y-%m-%dT%H:%M:%S.%f"}) - - Example:: - dt_expr = pyparsing_common.iso8601_datetime.copy() - dt_expr.setParseAction(pyparsing_common.convertToDatetime()) - print(dt_expr.parseString("1999-12-31T23:59:59.999")) - prints:: - [datetime.datetime(1999, 12, 31, 23, 59, 59, 999000)] - """ - def cvt_fn(s,l,t): - try: - return datetime.strptime(t[0], fmt) - except ValueError as ve: - raise ParseException(s, l, str(ve)) - return cvt_fn - - iso8601_date = Regex(r'(?P\d{4})(?:-(?P\d\d)(?:-(?P\d\d))?)?').setName("ISO8601 date") - "ISO8601 date (C{yyyy-mm-dd})" - - iso8601_datetime = Regex(r'(?P\d{4})-(?P\d\d)-(?P\d\d)[T ](?P\d\d):(?P\d\d)(:(?P\d\d(\.\d*)?)?)?(?PZ|[+-]\d\d:?\d\d)?').setName("ISO8601 datetime") - "ISO8601 datetime (C{yyyy-mm-ddThh:mm:ss.s(Z|+-00:00)}) - trailing seconds, milliseconds, and timezone optional; accepts separating C{'T'} or C{' '}" - - uuid = Regex(r'[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}').setName("UUID") - "UUID (C{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx})" - - _html_stripper = anyOpenTag.suppress() | anyCloseTag.suppress() - @staticmethod - def stripHTMLTags(s, l, tokens): - """ - Parse action to remove HTML tags from web page HTML source - - Example:: - # strip HTML links from normal text - text = 'More info at the
pyparsing wiki page' - td,td_end = makeHTMLTags("TD") - table_text = td + SkipTo(td_end).setParseAction(pyparsing_common.stripHTMLTags)("body") + td_end - - print(table_text.parseString(text).body) # -> 'More info at the pyparsing wiki page' - """ - return pyparsing_common._html_stripper.transformString(tokens[0]) - - _commasepitem = Combine(OneOrMore(~Literal(",") + ~LineEnd() + Word(printables, excludeChars=',') - + Optional( White(" \t") ) ) ).streamline().setName("commaItem") - comma_separated_list = delimitedList( Optional( quotedString.copy() | _commasepitem, default="") ).setName("comma separated list") - """Predefined expression of 1 or more printable words or quoted strings, separated by commas.""" - - upcaseTokens = staticmethod(tokenMap(lambda t: _ustr(t).upper())) - """Parse action to convert tokens to upper case.""" - - downcaseTokens = staticmethod(tokenMap(lambda t: _ustr(t).lower())) - """Parse action to convert tokens to lower case.""" - - -if __name__ == "__main__": - - selectToken = CaselessLiteral("select") - fromToken = CaselessLiteral("from") - - ident = Word(alphas, alphanums + "_$") - - columnName = delimitedList(ident, ".", combine=True).setParseAction(upcaseTokens) - columnNameList = Group(delimitedList(columnName)).setName("columns") - columnSpec = ('*' | columnNameList) - - tableName = delimitedList(ident, ".", combine=True).setParseAction(upcaseTokens) - tableNameList = Group(delimitedList(tableName)).setName("tables") - - simpleSQL = selectToken("command") + columnSpec("columns") + fromToken + tableNameList("tables") - - # demo runTests method, including embedded comments in test string - simpleSQL.runTests(""" - # '*' as column list and dotted table name - select * from SYS.XYZZY - - # caseless match on "SELECT", and casts back to "select" - SELECT * from XYZZY, ABC - - # list of column names, and mixed case SELECT keyword - Select AA,BB,CC from Sys.dual - - # multiple tables - Select A, B, C from Sys.dual, Table2 - - # invalid SELECT keyword - should fail - Xelect A, B, C from Sys.dual - - # incomplete command - should fail - Select - - # invalid column name - should fail - Select ^^^ frox Sys.dual - - """) - - pyparsing_common.number.runTests(""" - 100 - -100 - +100 - 3.14159 - 6.02e23 - 1e-12 - """) - - # any int or real number, returned as float - pyparsing_common.fnumber.runTests(""" - 100 - -100 - +100 - 3.14159 - 6.02e23 - 1e-12 - """) - - pyparsing_common.hex_integer.runTests(""" - 100 - FF - """) - - import uuid - pyparsing_common.uuid.setParseAction(tokenMap(uuid.UUID)) - pyparsing_common.uuid.runTests(""" - 12345678-1234-5678-1234-567812345678 - """) diff --git a/libs/common/pkg_resources/_vendor/six.py b/libs/common/pkg_resources/_vendor/six.py deleted file mode 100644 index 190c0239..00000000 --- a/libs/common/pkg_resources/_vendor/six.py +++ /dev/null @@ -1,868 +0,0 @@ -"""Utilities for writing code that runs on Python 2 and 3""" - -# Copyright (c) 2010-2015 Benjamin Peterson -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from __future__ import absolute_import - -import functools -import itertools -import operator -import sys -import types - -__author__ = "Benjamin Peterson " -__version__ = "1.10.0" - - -# Useful for very coarse version differentiation. -PY2 = sys.version_info[0] == 2 -PY3 = sys.version_info[0] == 3 -PY34 = sys.version_info[0:2] >= (3, 4) - -if PY3: - string_types = str, - integer_types = int, - class_types = type, - text_type = str - binary_type = bytes - - MAXSIZE = sys.maxsize -else: - string_types = basestring, - integer_types = (int, long) - class_types = (type, types.ClassType) - text_type = unicode - binary_type = str - - if sys.platform.startswith("java"): - # Jython always uses 32 bits. - MAXSIZE = int((1 << 31) - 1) - else: - # It's possible to have sizeof(long) != sizeof(Py_ssize_t). - class X(object): - - def __len__(self): - return 1 << 31 - try: - len(X()) - except OverflowError: - # 32-bit - MAXSIZE = int((1 << 31) - 1) - else: - # 64-bit - MAXSIZE = int((1 << 63) - 1) - del X - - -def _add_doc(func, doc): - """Add documentation to a function.""" - func.__doc__ = doc - - -def _import_module(name): - """Import module, returning the module after the last dot.""" - __import__(name) - return sys.modules[name] - - -class _LazyDescr(object): - - def __init__(self, name): - self.name = name - - def __get__(self, obj, tp): - result = self._resolve() - setattr(obj, self.name, result) # Invokes __set__. - try: - # This is a bit ugly, but it avoids running this again by - # removing this descriptor. - delattr(obj.__class__, self.name) - except AttributeError: - pass - return result - - -class MovedModule(_LazyDescr): - - def __init__(self, name, old, new=None): - super(MovedModule, self).__init__(name) - if PY3: - if new is None: - new = name - self.mod = new - else: - self.mod = old - - def _resolve(self): - return _import_module(self.mod) - - def __getattr__(self, attr): - _module = self._resolve() - value = getattr(_module, attr) - setattr(self, attr, value) - return value - - -class _LazyModule(types.ModuleType): - - def __init__(self, name): - super(_LazyModule, self).__init__(name) - self.__doc__ = self.__class__.__doc__ - - def __dir__(self): - attrs = ["__doc__", "__name__"] - attrs += [attr.name for attr in self._moved_attributes] - return attrs - - # Subclasses should override this - _moved_attributes = [] - - -class MovedAttribute(_LazyDescr): - - def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): - super(MovedAttribute, self).__init__(name) - if PY3: - if new_mod is None: - new_mod = name - self.mod = new_mod - if new_attr is None: - if old_attr is None: - new_attr = name - else: - new_attr = old_attr - self.attr = new_attr - else: - self.mod = old_mod - if old_attr is None: - old_attr = name - self.attr = old_attr - - def _resolve(self): - module = _import_module(self.mod) - return getattr(module, self.attr) - - -class _SixMetaPathImporter(object): - - """ - A meta path importer to import six.moves and its submodules. - - This class implements a PEP302 finder and loader. It should be compatible - with Python 2.5 and all existing versions of Python3 - """ - - def __init__(self, six_module_name): - self.name = six_module_name - self.known_modules = {} - - def _add_module(self, mod, *fullnames): - for fullname in fullnames: - self.known_modules[self.name + "." + fullname] = mod - - def _get_module(self, fullname): - return self.known_modules[self.name + "." + fullname] - - def find_module(self, fullname, path=None): - if fullname in self.known_modules: - return self - return None - - def __get_module(self, fullname): - try: - return self.known_modules[fullname] - except KeyError: - raise ImportError("This loader does not know module " + fullname) - - def load_module(self, fullname): - try: - # in case of a reload - return sys.modules[fullname] - except KeyError: - pass - mod = self.__get_module(fullname) - if isinstance(mod, MovedModule): - mod = mod._resolve() - else: - mod.__loader__ = self - sys.modules[fullname] = mod - return mod - - def is_package(self, fullname): - """ - Return true, if the named module is a package. - - We need this method to get correct spec objects with - Python 3.4 (see PEP451) - """ - return hasattr(self.__get_module(fullname), "__path__") - - def get_code(self, fullname): - """Return None - - Required, if is_package is implemented""" - self.__get_module(fullname) # eventually raises ImportError - return None - get_source = get_code # same as get_code - -_importer = _SixMetaPathImporter(__name__) - - -class _MovedItems(_LazyModule): - - """Lazy loading of moved objects""" - __path__ = [] # mark as package - - -_moved_attributes = [ - MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), - MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), - MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"), - MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), - MovedAttribute("intern", "__builtin__", "sys"), - MovedAttribute("map", "itertools", "builtins", "imap", "map"), - MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"), - MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"), - MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), - MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"), - MovedAttribute("reduce", "__builtin__", "functools"), - MovedAttribute("shlex_quote", "pipes", "shlex", "quote"), - MovedAttribute("StringIO", "StringIO", "io"), - MovedAttribute("UserDict", "UserDict", "collections"), - MovedAttribute("UserList", "UserList", "collections"), - MovedAttribute("UserString", "UserString", "collections"), - MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), - MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), - MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"), - MovedModule("builtins", "__builtin__"), - MovedModule("configparser", "ConfigParser"), - MovedModule("copyreg", "copy_reg"), - MovedModule("dbm_gnu", "gdbm", "dbm.gnu"), - MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"), - MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), - MovedModule("http_cookies", "Cookie", "http.cookies"), - MovedModule("html_entities", "htmlentitydefs", "html.entities"), - MovedModule("html_parser", "HTMLParser", "html.parser"), - MovedModule("http_client", "httplib", "http.client"), - MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), - MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"), - MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), - MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), - MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), - MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), - MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), - MovedModule("cPickle", "cPickle", "pickle"), - MovedModule("queue", "Queue"), - MovedModule("reprlib", "repr"), - MovedModule("socketserver", "SocketServer"), - MovedModule("_thread", "thread", "_thread"), - MovedModule("tkinter", "Tkinter"), - MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), - MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), - MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), - MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), - MovedModule("tkinter_tix", "Tix", "tkinter.tix"), - MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"), - MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), - MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), - MovedModule("tkinter_colorchooser", "tkColorChooser", - "tkinter.colorchooser"), - MovedModule("tkinter_commondialog", "tkCommonDialog", - "tkinter.commondialog"), - MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), - MovedModule("tkinter_font", "tkFont", "tkinter.font"), - MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), - MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", - "tkinter.simpledialog"), - MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"), - MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"), - MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"), - MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), - MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"), - MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"), -] -# Add windows specific modules. -if sys.platform == "win32": - _moved_attributes += [ - MovedModule("winreg", "_winreg"), - ] - -for attr in _moved_attributes: - setattr(_MovedItems, attr.name, attr) - if isinstance(attr, MovedModule): - _importer._add_module(attr, "moves." + attr.name) -del attr - -_MovedItems._moved_attributes = _moved_attributes - -moves = _MovedItems(__name__ + ".moves") -_importer._add_module(moves, "moves") - - -class Module_six_moves_urllib_parse(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_parse""" - - -_urllib_parse_moved_attributes = [ - MovedAttribute("ParseResult", "urlparse", "urllib.parse"), - MovedAttribute("SplitResult", "urlparse", "urllib.parse"), - MovedAttribute("parse_qs", "urlparse", "urllib.parse"), - MovedAttribute("parse_qsl", "urlparse", "urllib.parse"), - MovedAttribute("urldefrag", "urlparse", "urllib.parse"), - MovedAttribute("urljoin", "urlparse", "urllib.parse"), - MovedAttribute("urlparse", "urlparse", "urllib.parse"), - MovedAttribute("urlsplit", "urlparse", "urllib.parse"), - MovedAttribute("urlunparse", "urlparse", "urllib.parse"), - MovedAttribute("urlunsplit", "urlparse", "urllib.parse"), - MovedAttribute("quote", "urllib", "urllib.parse"), - MovedAttribute("quote_plus", "urllib", "urllib.parse"), - MovedAttribute("unquote", "urllib", "urllib.parse"), - MovedAttribute("unquote_plus", "urllib", "urllib.parse"), - MovedAttribute("urlencode", "urllib", "urllib.parse"), - MovedAttribute("splitquery", "urllib", "urllib.parse"), - MovedAttribute("splittag", "urllib", "urllib.parse"), - MovedAttribute("splituser", "urllib", "urllib.parse"), - MovedAttribute("uses_fragment", "urlparse", "urllib.parse"), - MovedAttribute("uses_netloc", "urlparse", "urllib.parse"), - MovedAttribute("uses_params", "urlparse", "urllib.parse"), - MovedAttribute("uses_query", "urlparse", "urllib.parse"), - MovedAttribute("uses_relative", "urlparse", "urllib.parse"), -] -for attr in _urllib_parse_moved_attributes: - setattr(Module_six_moves_urllib_parse, attr.name, attr) -del attr - -Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes - -_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"), - "moves.urllib_parse", "moves.urllib.parse") - - -class Module_six_moves_urllib_error(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_error""" - - -_urllib_error_moved_attributes = [ - MovedAttribute("URLError", "urllib2", "urllib.error"), - MovedAttribute("HTTPError", "urllib2", "urllib.error"), - MovedAttribute("ContentTooShortError", "urllib", "urllib.error"), -] -for attr in _urllib_error_moved_attributes: - setattr(Module_six_moves_urllib_error, attr.name, attr) -del attr - -Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes - -_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"), - "moves.urllib_error", "moves.urllib.error") - - -class Module_six_moves_urllib_request(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_request""" - - -_urllib_request_moved_attributes = [ - MovedAttribute("urlopen", "urllib2", "urllib.request"), - MovedAttribute("install_opener", "urllib2", "urllib.request"), - MovedAttribute("build_opener", "urllib2", "urllib.request"), - MovedAttribute("pathname2url", "urllib", "urllib.request"), - MovedAttribute("url2pathname", "urllib", "urllib.request"), - MovedAttribute("getproxies", "urllib", "urllib.request"), - MovedAttribute("Request", "urllib2", "urllib.request"), - MovedAttribute("OpenerDirector", "urllib2", "urllib.request"), - MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"), - MovedAttribute("ProxyHandler", "urllib2", "urllib.request"), - MovedAttribute("BaseHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"), - MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"), - MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"), - MovedAttribute("FileHandler", "urllib2", "urllib.request"), - MovedAttribute("FTPHandler", "urllib2", "urllib.request"), - MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"), - MovedAttribute("UnknownHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"), - MovedAttribute("urlretrieve", "urllib", "urllib.request"), - MovedAttribute("urlcleanup", "urllib", "urllib.request"), - MovedAttribute("URLopener", "urllib", "urllib.request"), - MovedAttribute("FancyURLopener", "urllib", "urllib.request"), - MovedAttribute("proxy_bypass", "urllib", "urllib.request"), -] -for attr in _urllib_request_moved_attributes: - setattr(Module_six_moves_urllib_request, attr.name, attr) -del attr - -Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes - -_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"), - "moves.urllib_request", "moves.urllib.request") - - -class Module_six_moves_urllib_response(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_response""" - - -_urllib_response_moved_attributes = [ - MovedAttribute("addbase", "urllib", "urllib.response"), - MovedAttribute("addclosehook", "urllib", "urllib.response"), - MovedAttribute("addinfo", "urllib", "urllib.response"), - MovedAttribute("addinfourl", "urllib", "urllib.response"), -] -for attr in _urllib_response_moved_attributes: - setattr(Module_six_moves_urllib_response, attr.name, attr) -del attr - -Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes - -_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"), - "moves.urllib_response", "moves.urllib.response") - - -class Module_six_moves_urllib_robotparser(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_robotparser""" - - -_urllib_robotparser_moved_attributes = [ - MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"), -] -for attr in _urllib_robotparser_moved_attributes: - setattr(Module_six_moves_urllib_robotparser, attr.name, attr) -del attr - -Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes - -_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"), - "moves.urllib_robotparser", "moves.urllib.robotparser") - - -class Module_six_moves_urllib(types.ModuleType): - - """Create a six.moves.urllib namespace that resembles the Python 3 namespace""" - __path__ = [] # mark as package - parse = _importer._get_module("moves.urllib_parse") - error = _importer._get_module("moves.urllib_error") - request = _importer._get_module("moves.urllib_request") - response = _importer._get_module("moves.urllib_response") - robotparser = _importer._get_module("moves.urllib_robotparser") - - def __dir__(self): - return ['parse', 'error', 'request', 'response', 'robotparser'] - -_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"), - "moves.urllib") - - -def add_move(move): - """Add an item to six.moves.""" - setattr(_MovedItems, move.name, move) - - -def remove_move(name): - """Remove item from six.moves.""" - try: - delattr(_MovedItems, name) - except AttributeError: - try: - del moves.__dict__[name] - except KeyError: - raise AttributeError("no such move, %r" % (name,)) - - -if PY3: - _meth_func = "__func__" - _meth_self = "__self__" - - _func_closure = "__closure__" - _func_code = "__code__" - _func_defaults = "__defaults__" - _func_globals = "__globals__" -else: - _meth_func = "im_func" - _meth_self = "im_self" - - _func_closure = "func_closure" - _func_code = "func_code" - _func_defaults = "func_defaults" - _func_globals = "func_globals" - - -try: - advance_iterator = next -except NameError: - def advance_iterator(it): - return it.next() -next = advance_iterator - - -try: - callable = callable -except NameError: - def callable(obj): - return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) - - -if PY3: - def get_unbound_function(unbound): - return unbound - - create_bound_method = types.MethodType - - def create_unbound_method(func, cls): - return func - - Iterator = object -else: - def get_unbound_function(unbound): - return unbound.im_func - - def create_bound_method(func, obj): - return types.MethodType(func, obj, obj.__class__) - - def create_unbound_method(func, cls): - return types.MethodType(func, None, cls) - - class Iterator(object): - - def next(self): - return type(self).__next__(self) - - callable = callable -_add_doc(get_unbound_function, - """Get the function out of a possibly unbound function""") - - -get_method_function = operator.attrgetter(_meth_func) -get_method_self = operator.attrgetter(_meth_self) -get_function_closure = operator.attrgetter(_func_closure) -get_function_code = operator.attrgetter(_func_code) -get_function_defaults = operator.attrgetter(_func_defaults) -get_function_globals = operator.attrgetter(_func_globals) - - -if PY3: - def iterkeys(d, **kw): - return iter(d.keys(**kw)) - - def itervalues(d, **kw): - return iter(d.values(**kw)) - - def iteritems(d, **kw): - return iter(d.items(**kw)) - - def iterlists(d, **kw): - return iter(d.lists(**kw)) - - viewkeys = operator.methodcaller("keys") - - viewvalues = operator.methodcaller("values") - - viewitems = operator.methodcaller("items") -else: - def iterkeys(d, **kw): - return d.iterkeys(**kw) - - def itervalues(d, **kw): - return d.itervalues(**kw) - - def iteritems(d, **kw): - return d.iteritems(**kw) - - def iterlists(d, **kw): - return d.iterlists(**kw) - - viewkeys = operator.methodcaller("viewkeys") - - viewvalues = operator.methodcaller("viewvalues") - - viewitems = operator.methodcaller("viewitems") - -_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.") -_add_doc(itervalues, "Return an iterator over the values of a dictionary.") -_add_doc(iteritems, - "Return an iterator over the (key, value) pairs of a dictionary.") -_add_doc(iterlists, - "Return an iterator over the (key, [values]) pairs of a dictionary.") - - -if PY3: - def b(s): - return s.encode("latin-1") - - def u(s): - return s - unichr = chr - import struct - int2byte = struct.Struct(">B").pack - del struct - byte2int = operator.itemgetter(0) - indexbytes = operator.getitem - iterbytes = iter - import io - StringIO = io.StringIO - BytesIO = io.BytesIO - _assertCountEqual = "assertCountEqual" - if sys.version_info[1] <= 1: - _assertRaisesRegex = "assertRaisesRegexp" - _assertRegex = "assertRegexpMatches" - else: - _assertRaisesRegex = "assertRaisesRegex" - _assertRegex = "assertRegex" -else: - def b(s): - return s - # Workaround for standalone backslash - - def u(s): - return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape") - unichr = unichr - int2byte = chr - - def byte2int(bs): - return ord(bs[0]) - - def indexbytes(buf, i): - return ord(buf[i]) - iterbytes = functools.partial(itertools.imap, ord) - import StringIO - StringIO = BytesIO = StringIO.StringIO - _assertCountEqual = "assertItemsEqual" - _assertRaisesRegex = "assertRaisesRegexp" - _assertRegex = "assertRegexpMatches" -_add_doc(b, """Byte literal""") -_add_doc(u, """Text literal""") - - -def assertCountEqual(self, *args, **kwargs): - return getattr(self, _assertCountEqual)(*args, **kwargs) - - -def assertRaisesRegex(self, *args, **kwargs): - return getattr(self, _assertRaisesRegex)(*args, **kwargs) - - -def assertRegex(self, *args, **kwargs): - return getattr(self, _assertRegex)(*args, **kwargs) - - -if PY3: - exec_ = getattr(moves.builtins, "exec") - - def reraise(tp, value, tb=None): - if value is None: - value = tp() - if value.__traceback__ is not tb: - raise value.with_traceback(tb) - raise value - -else: - def exec_(_code_, _globs_=None, _locs_=None): - """Execute code in a namespace.""" - if _globs_ is None: - frame = sys._getframe(1) - _globs_ = frame.f_globals - if _locs_ is None: - _locs_ = frame.f_locals - del frame - elif _locs_ is None: - _locs_ = _globs_ - exec("""exec _code_ in _globs_, _locs_""") - - exec_("""def reraise(tp, value, tb=None): - raise tp, value, tb -""") - - -if sys.version_info[:2] == (3, 2): - exec_("""def raise_from(value, from_value): - if from_value is None: - raise value - raise value from from_value -""") -elif sys.version_info[:2] > (3, 2): - exec_("""def raise_from(value, from_value): - raise value from from_value -""") -else: - def raise_from(value, from_value): - raise value - - -print_ = getattr(moves.builtins, "print", None) -if print_ is None: - def print_(*args, **kwargs): - """The new-style print function for Python 2.4 and 2.5.""" - fp = kwargs.pop("file", sys.stdout) - if fp is None: - return - - def write(data): - if not isinstance(data, basestring): - data = str(data) - # If the file has an encoding, encode unicode with it. - if (isinstance(fp, file) and - isinstance(data, unicode) and - fp.encoding is not None): - errors = getattr(fp, "errors", None) - if errors is None: - errors = "strict" - data = data.encode(fp.encoding, errors) - fp.write(data) - want_unicode = False - sep = kwargs.pop("sep", None) - if sep is not None: - if isinstance(sep, unicode): - want_unicode = True - elif not isinstance(sep, str): - raise TypeError("sep must be None or a string") - end = kwargs.pop("end", None) - if end is not None: - if isinstance(end, unicode): - want_unicode = True - elif not isinstance(end, str): - raise TypeError("end must be None or a string") - if kwargs: - raise TypeError("invalid keyword arguments to print()") - if not want_unicode: - for arg in args: - if isinstance(arg, unicode): - want_unicode = True - break - if want_unicode: - newline = unicode("\n") - space = unicode(" ") - else: - newline = "\n" - space = " " - if sep is None: - sep = space - if end is None: - end = newline - for i, arg in enumerate(args): - if i: - write(sep) - write(arg) - write(end) -if sys.version_info[:2] < (3, 3): - _print = print_ - - def print_(*args, **kwargs): - fp = kwargs.get("file", sys.stdout) - flush = kwargs.pop("flush", False) - _print(*args, **kwargs) - if flush and fp is not None: - fp.flush() - -_add_doc(reraise, """Reraise an exception.""") - -if sys.version_info[0:2] < (3, 4): - def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, - updated=functools.WRAPPER_UPDATES): - def wrapper(f): - f = functools.wraps(wrapped, assigned, updated)(f) - f.__wrapped__ = wrapped - return f - return wrapper -else: - wraps = functools.wraps - - -def with_metaclass(meta, *bases): - """Create a base class with a metaclass.""" - # This requires a bit of explanation: the basic idea is to make a dummy - # metaclass for one level of class instantiation that replaces itself with - # the actual metaclass. - class metaclass(meta): - - def __new__(cls, name, this_bases, d): - return meta(name, bases, d) - return type.__new__(metaclass, 'temporary_class', (), {}) - - -def add_metaclass(metaclass): - """Class decorator for creating a class with a metaclass.""" - def wrapper(cls): - orig_vars = cls.__dict__.copy() - slots = orig_vars.get('__slots__') - if slots is not None: - if isinstance(slots, str): - slots = [slots] - for slots_var in slots: - orig_vars.pop(slots_var) - orig_vars.pop('__dict__', None) - orig_vars.pop('__weakref__', None) - return metaclass(cls.__name__, cls.__bases__, orig_vars) - return wrapper - - -def python_2_unicode_compatible(klass): - """ - A decorator that defines __unicode__ and __str__ methods under Python 2. - Under Python 3 it does nothing. - - To support Python 2 and 3 with a single code base, define a __str__ method - returning text and apply this decorator to the class. - """ - if PY2: - if '__str__' not in klass.__dict__: - raise ValueError("@python_2_unicode_compatible cannot be applied " - "to %s because it doesn't define __str__()." % - klass.__name__) - klass.__unicode__ = klass.__str__ - klass.__str__ = lambda self: self.__unicode__().encode('utf-8') - return klass - - -# Complete the moves implementation. -# This code is at the end of this module to speed up module loading. -# Turn this module into a package. -__path__ = [] # required for PEP 302 and PEP 451 -__package__ = __name__ # see PEP 366 @ReservedAssignment -if globals().get("__spec__") is not None: - __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable -# Remove other six meta path importers, since they cause problems. This can -# happen if six is removed from sys.modules and then reloaded. (Setuptools does -# this for some reason.) -if sys.meta_path: - for i, importer in enumerate(sys.meta_path): - # Here's some real nastiness: Another "instance" of the six module might - # be floating around. Therefore, we can't use isinstance() to check for - # the six meta path importer, since the other six instance will have - # inserted an importer with different class. - if (type(importer).__name__ == "_SixMetaPathImporter" and - importer.name == __name__): - del sys.meta_path[i] - break - del i, importer -# Finally, add the importer to the meta path import hook. -sys.meta_path.append(_importer) diff --git a/libs/common/pkg_resources/extern/__init__.py b/libs/common/pkg_resources/extern/__init__.py index c1eb9e99..948bcc60 100644 --- a/libs/common/pkg_resources/extern/__init__.py +++ b/libs/common/pkg_resources/extern/__init__.py @@ -1,3 +1,4 @@ +import importlib.util import sys @@ -20,17 +21,10 @@ class VendorImporter: yield self.vendor_pkg + '.' yield '' - def find_module(self, fullname, path=None): - """ - Return self when fullname starts with root_name and the - target module is one vendored through this importer. - """ + def _module_matches_namespace(self, fullname): + """Figure out if the target module is vendored.""" root, base, target = fullname.partition(self.root_name + '.') - if root: - return - if not any(map(target.startswith, self.vendored_names)): - return - return self + return not root and any(map(target.startswith, self.vendored_names)) def load_module(self, fullname): """ @@ -43,13 +37,6 @@ class VendorImporter: __import__(extant) mod = sys.modules[extant] sys.modules[fullname] = mod - # mysterious hack: - # Remove the reference to the extant package/module - # on later Python versions to cause relative imports - # in the vendor package to resolve the same modules - # as those going through this importer. - if prefix and sys.version_info > (3, 3): - del sys.modules[extant] return mod except ImportError: pass @@ -61,6 +48,20 @@ class VendorImporter: "distribution.".format(**locals()) ) + def create_module(self, spec): + return self.load_module(spec.name) + + def exec_module(self, module): + pass + + def find_spec(self, fullname, path=None, target=None): + """Return a module spec for vendored names.""" + return ( + importlib.util.spec_from_loader(fullname, self) + if self._module_matches_namespace(fullname) + else None + ) + def install(self): """ Install this importer into sys.meta_path if not already present. @@ -69,5 +70,11 @@ class VendorImporter: sys.meta_path.append(self) -names = 'packaging', 'pyparsing', 'six', 'appdirs' +names = ( + 'packaging', + 'platformdirs', + 'jaraco', + 'importlib_resources', + 'more_itertools', +) VendorImporter(__name__, names).install() diff --git a/libs/common/pkg_resources/py31compat.py b/libs/common/pkg_resources/py31compat.py deleted file mode 100644 index a381c424..00000000 --- a/libs/common/pkg_resources/py31compat.py +++ /dev/null @@ -1,23 +0,0 @@ -import os -import errno -import sys - -from .extern import six - - -def _makedirs_31(path, exist_ok=False): - try: - os.makedirs(path) - except OSError as exc: - if not exist_ok or exc.errno != errno.EEXIST: - raise - - -# rely on compatibility behavior until mode considerations -# and exists_ok considerations are disentangled. -# See https://github.com/pypa/setuptools/pull/1083#issuecomment-315168663 -needs_makedirs = ( - six.PY2 or - (3, 4) <= sys.version_info < (3, 4, 1) -) -makedirs = _makedirs_31 if needs_makedirs else os.makedirs diff --git a/libs/common/platformdirs/__init__.py b/libs/common/platformdirs/__init__.py new file mode 100644 index 00000000..75c8bff9 --- /dev/null +++ b/libs/common/platformdirs/__init__.py @@ -0,0 +1,625 @@ +""" +Utilities for determining application-specific dirs. See for details and +usage. +""" + +from __future__ import annotations + +import os +import sys +from typing import TYPE_CHECKING + +from .api import PlatformDirsABC +from .version import __version__ +from .version import __version_tuple__ as __version_info__ + +if TYPE_CHECKING: + from pathlib import Path + from typing import Literal + + +def _set_platform_dir_class() -> type[PlatformDirsABC]: + if sys.platform == "win32": + from platformdirs.windows import Windows as Result + elif sys.platform == "darwin": + from platformdirs.macos import MacOS as Result + else: + from platformdirs.unix import Unix as Result + + if os.getenv("ANDROID_DATA") == "/data" and os.getenv("ANDROID_ROOT") == "/system": + if os.getenv("SHELL") or os.getenv("PREFIX"): + return Result + + from platformdirs.android import _android_folder + + if _android_folder() is not None: + from platformdirs.android import Android + + return Android # return to avoid redefinition of result + + return Result + + +PlatformDirs = _set_platform_dir_class() #: Currently active platform +AppDirs = PlatformDirs #: Backwards compatibility with appdirs + + +def user_data_dir( + appname: str | None = None, + appauthor: str | None | Literal[False] = None, + version: str | None = None, + roaming: bool = False, # noqa: FBT001, FBT002 + ensure_exists: bool = False, # noqa: FBT001, FBT002 +) -> str: + """ + :param appname: See `appname `. + :param appauthor: See `appauthor `. + :param version: See `version `. + :param roaming: See `roaming `. + :param ensure_exists: See `ensure_exists `. + :returns: data directory tied to the user + """ + return PlatformDirs( + appname=appname, + appauthor=appauthor, + version=version, + roaming=roaming, + ensure_exists=ensure_exists, + ).user_data_dir + + +def site_data_dir( + appname: str | None = None, + appauthor: str | None | Literal[False] = None, + version: str | None = None, + multipath: bool = False, # noqa: FBT001, FBT002 + ensure_exists: bool = False, # noqa: FBT001, FBT002 +) -> str: + """ + :param appname: See `appname `. + :param appauthor: See `appauthor `. + :param version: See `version `. + :param multipath: See `roaming `. + :param ensure_exists: See `ensure_exists `. + :returns: data directory shared by users + """ + return PlatformDirs( + appname=appname, + appauthor=appauthor, + version=version, + multipath=multipath, + ensure_exists=ensure_exists, + ).site_data_dir + + +def user_config_dir( + appname: str | None = None, + appauthor: str | None | Literal[False] = None, + version: str | None = None, + roaming: bool = False, # noqa: FBT001, FBT002 + ensure_exists: bool = False, # noqa: FBT001, FBT002 +) -> str: + """ + :param appname: See `appname `. + :param appauthor: See `appauthor `. + :param version: See `version `. + :param roaming: See `roaming `. + :param ensure_exists: See `ensure_exists `. + :returns: config directory tied to the user + """ + return PlatformDirs( + appname=appname, + appauthor=appauthor, + version=version, + roaming=roaming, + ensure_exists=ensure_exists, + ).user_config_dir + + +def site_config_dir( + appname: str | None = None, + appauthor: str | None | Literal[False] = None, + version: str | None = None, + multipath: bool = False, # noqa: FBT001, FBT002 + ensure_exists: bool = False, # noqa: FBT001, FBT002 +) -> str: + """ + :param appname: See `appname `. + :param appauthor: See `appauthor `. + :param version: See `version `. + :param multipath: See `roaming `. + :param ensure_exists: See `ensure_exists `. + :returns: config directory shared by the users + """ + return PlatformDirs( + appname=appname, + appauthor=appauthor, + version=version, + multipath=multipath, + ensure_exists=ensure_exists, + ).site_config_dir + + +def user_cache_dir( + appname: str | None = None, + appauthor: str | None | Literal[False] = None, + version: str | None = None, + opinion: bool = True, # noqa: FBT001, FBT002 + ensure_exists: bool = False, # noqa: FBT001, FBT002 +) -> str: + """ + :param appname: See `appname `. + :param appauthor: See `appauthor `. + :param version: See `version `. + :param opinion: See `roaming `. + :param ensure_exists: See `ensure_exists `. + :returns: cache directory tied to the user + """ + return PlatformDirs( + appname=appname, + appauthor=appauthor, + version=version, + opinion=opinion, + ensure_exists=ensure_exists, + ).user_cache_dir + + +def site_cache_dir( + appname: str | None = None, + appauthor: str | None | Literal[False] = None, + version: str | None = None, + opinion: bool = True, # noqa: FBT001, FBT002 + ensure_exists: bool = False, # noqa: FBT001, FBT002 +) -> str: + """ + :param appname: See `appname `. + :param appauthor: See `appauthor `. + :param version: See `version `. + :param opinion: See `opinion `. + :param ensure_exists: See `ensure_exists `. + :returns: cache directory tied to the user + """ + return PlatformDirs( + appname=appname, + appauthor=appauthor, + version=version, + opinion=opinion, + ensure_exists=ensure_exists, + ).site_cache_dir + + +def user_state_dir( + appname: str | None = None, + appauthor: str | None | Literal[False] = None, + version: str | None = None, + roaming: bool = False, # noqa: FBT001, FBT002 + ensure_exists: bool = False, # noqa: FBT001, FBT002 +) -> str: + """ + :param appname: See `appname `. + :param appauthor: See `appauthor `. + :param version: See `version `. + :param roaming: See `roaming `. + :param ensure_exists: See `ensure_exists `. + :returns: state directory tied to the user + """ + return PlatformDirs( + appname=appname, + appauthor=appauthor, + version=version, + roaming=roaming, + ensure_exists=ensure_exists, + ).user_state_dir + + +def user_log_dir( + appname: str | None = None, + appauthor: str | None | Literal[False] = None, + version: str | None = None, + opinion: bool = True, # noqa: FBT001, FBT002 + ensure_exists: bool = False, # noqa: FBT001, FBT002 +) -> str: + """ + :param appname: See `appname `. + :param appauthor: See `appauthor `. + :param version: See `version `. + :param opinion: See `roaming `. + :param ensure_exists: See `ensure_exists `. + :returns: log directory tied to the user + """ + return PlatformDirs( + appname=appname, + appauthor=appauthor, + version=version, + opinion=opinion, + ensure_exists=ensure_exists, + ).user_log_dir + + +def user_documents_dir() -> str: + """:returns: documents directory tied to the user""" + return PlatformDirs().user_documents_dir + + +def user_downloads_dir() -> str: + """:returns: downloads directory tied to the user""" + return PlatformDirs().user_downloads_dir + + +def user_pictures_dir() -> str: + """:returns: pictures directory tied to the user""" + return PlatformDirs().user_pictures_dir + + +def user_videos_dir() -> str: + """:returns: videos directory tied to the user""" + return PlatformDirs().user_videos_dir + + +def user_music_dir() -> str: + """:returns: music directory tied to the user""" + return PlatformDirs().user_music_dir + + +def user_desktop_dir() -> str: + """:returns: desktop directory tied to the user""" + return PlatformDirs().user_desktop_dir + + +def user_runtime_dir( + appname: str | None = None, + appauthor: str | None | Literal[False] = None, + version: str | None = None, + opinion: bool = True, # noqa: FBT001, FBT002 + ensure_exists: bool = False, # noqa: FBT001, FBT002 +) -> str: + """ + :param appname: See `appname `. + :param appauthor: See `appauthor `. + :param version: See `version `. + :param opinion: See `opinion `. + :param ensure_exists: See `ensure_exists `. + :returns: runtime directory tied to the user + """ + return PlatformDirs( + appname=appname, + appauthor=appauthor, + version=version, + opinion=opinion, + ensure_exists=ensure_exists, + ).user_runtime_dir + + +def site_runtime_dir( + appname: str | None = None, + appauthor: str | None | Literal[False] = None, + version: str | None = None, + opinion: bool = True, # noqa: FBT001, FBT002 + ensure_exists: bool = False, # noqa: FBT001, FBT002 +) -> str: + """ + :param appname: See `appname `. + :param appauthor: See `appauthor `. + :param version: See `version `. + :param opinion: See `opinion `. + :param ensure_exists: See `ensure_exists `. + :returns: runtime directory shared by users + """ + return PlatformDirs( + appname=appname, + appauthor=appauthor, + version=version, + opinion=opinion, + ensure_exists=ensure_exists, + ).site_runtime_dir + + +def user_data_path( + appname: str | None = None, + appauthor: str | None | Literal[False] = None, + version: str | None = None, + roaming: bool = False, # noqa: FBT001, FBT002 + ensure_exists: bool = False, # noqa: FBT001, FBT002 +) -> Path: + """ + :param appname: See `appname `. + :param appauthor: See `appauthor `. + :param version: See `version `. + :param roaming: See `roaming `. + :param ensure_exists: See `ensure_exists `. + :returns: data path tied to the user + """ + return PlatformDirs( + appname=appname, + appauthor=appauthor, + version=version, + roaming=roaming, + ensure_exists=ensure_exists, + ).user_data_path + + +def site_data_path( + appname: str | None = None, + appauthor: str | None | Literal[False] = None, + version: str | None = None, + multipath: bool = False, # noqa: FBT001, FBT002 + ensure_exists: bool = False, # noqa: FBT001, FBT002 +) -> Path: + """ + :param appname: See `appname `. + :param appauthor: See `appauthor `. + :param version: See `version `. + :param multipath: See `multipath `. + :param ensure_exists: See `ensure_exists `. + :returns: data path shared by users + """ + return PlatformDirs( + appname=appname, + appauthor=appauthor, + version=version, + multipath=multipath, + ensure_exists=ensure_exists, + ).site_data_path + + +def user_config_path( + appname: str | None = None, + appauthor: str | None | Literal[False] = None, + version: str | None = None, + roaming: bool = False, # noqa: FBT001, FBT002 + ensure_exists: bool = False, # noqa: FBT001, FBT002 +) -> Path: + """ + :param appname: See `appname `. + :param appauthor: See `appauthor `. + :param version: See `version `. + :param roaming: See `roaming `. + :param ensure_exists: See `ensure_exists `. + :returns: config path tied to the user + """ + return PlatformDirs( + appname=appname, + appauthor=appauthor, + version=version, + roaming=roaming, + ensure_exists=ensure_exists, + ).user_config_path + + +def site_config_path( + appname: str | None = None, + appauthor: str | None | Literal[False] = None, + version: str | None = None, + multipath: bool = False, # noqa: FBT001, FBT002 + ensure_exists: bool = False, # noqa: FBT001, FBT002 +) -> Path: + """ + :param appname: See `appname `. + :param appauthor: See `appauthor `. + :param version: See `version `. + :param multipath: See `roaming `. + :param ensure_exists: See `ensure_exists `. + :returns: config path shared by the users + """ + return PlatformDirs( + appname=appname, + appauthor=appauthor, + version=version, + multipath=multipath, + ensure_exists=ensure_exists, + ).site_config_path + + +def site_cache_path( + appname: str | None = None, + appauthor: str | None | Literal[False] = None, + version: str | None = None, + opinion: bool = True, # noqa: FBT001, FBT002 + ensure_exists: bool = False, # noqa: FBT001, FBT002 +) -> Path: + """ + :param appname: See `appname `. + :param appauthor: See `appauthor `. + :param version: See `version `. + :param opinion: See `opinion `. + :param ensure_exists: See `ensure_exists `. + :returns: cache directory tied to the user + """ + return PlatformDirs( + appname=appname, + appauthor=appauthor, + version=version, + opinion=opinion, + ensure_exists=ensure_exists, + ).site_cache_path + + +def user_cache_path( + appname: str | None = None, + appauthor: str | None | Literal[False] = None, + version: str | None = None, + opinion: bool = True, # noqa: FBT001, FBT002 + ensure_exists: bool = False, # noqa: FBT001, FBT002 +) -> Path: + """ + :param appname: See `appname `. + :param appauthor: See `appauthor `. + :param version: See `version `. + :param opinion: See `roaming `. + :param ensure_exists: See `ensure_exists `. + :returns: cache path tied to the user + """ + return PlatformDirs( + appname=appname, + appauthor=appauthor, + version=version, + opinion=opinion, + ensure_exists=ensure_exists, + ).user_cache_path + + +def user_state_path( + appname: str | None = None, + appauthor: str | None | Literal[False] = None, + version: str | None = None, + roaming: bool = False, # noqa: FBT001, FBT002 + ensure_exists: bool = False, # noqa: FBT001, FBT002 +) -> Path: + """ + :param appname: See `appname `. + :param appauthor: See `appauthor `. + :param version: See `version `. + :param roaming: See `roaming `. + :param ensure_exists: See `ensure_exists `. + :returns: state path tied to the user + """ + return PlatformDirs( + appname=appname, + appauthor=appauthor, + version=version, + roaming=roaming, + ensure_exists=ensure_exists, + ).user_state_path + + +def user_log_path( + appname: str | None = None, + appauthor: str | None | Literal[False] = None, + version: str | None = None, + opinion: bool = True, # noqa: FBT001, FBT002 + ensure_exists: bool = False, # noqa: FBT001, FBT002 +) -> Path: + """ + :param appname: See `appname `. + :param appauthor: See `appauthor `. + :param version: See `version `. + :param opinion: See `roaming `. + :param ensure_exists: See `ensure_exists `. + :returns: log path tied to the user + """ + return PlatformDirs( + appname=appname, + appauthor=appauthor, + version=version, + opinion=opinion, + ensure_exists=ensure_exists, + ).user_log_path + + +def user_documents_path() -> Path: + """:returns: documents path tied to the user""" + return PlatformDirs().user_documents_path + + +def user_downloads_path() -> Path: + """:returns: downloads path tied to the user""" + return PlatformDirs().user_downloads_path + + +def user_pictures_path() -> Path: + """:returns: pictures path tied to the user""" + return PlatformDirs().user_pictures_path + + +def user_videos_path() -> Path: + """:returns: videos path tied to the user""" + return PlatformDirs().user_videos_path + + +def user_music_path() -> Path: + """:returns: music path tied to the user""" + return PlatformDirs().user_music_path + + +def user_desktop_path() -> Path: + """:returns: desktop path tied to the user""" + return PlatformDirs().user_desktop_path + + +def user_runtime_path( + appname: str | None = None, + appauthor: str | None | Literal[False] = None, + version: str | None = None, + opinion: bool = True, # noqa: FBT001, FBT002 + ensure_exists: bool = False, # noqa: FBT001, FBT002 +) -> Path: + """ + :param appname: See `appname `. + :param appauthor: See `appauthor `. + :param version: See `version `. + :param opinion: See `opinion `. + :param ensure_exists: See `ensure_exists `. + :returns: runtime path tied to the user + """ + return PlatformDirs( + appname=appname, + appauthor=appauthor, + version=version, + opinion=opinion, + ensure_exists=ensure_exists, + ).user_runtime_path + + +def site_runtime_path( + appname: str | None = None, + appauthor: str | None | Literal[False] = None, + version: str | None = None, + opinion: bool = True, # noqa: FBT001, FBT002 + ensure_exists: bool = False, # noqa: FBT001, FBT002 +) -> Path: + """ + :param appname: See `appname `. + :param appauthor: See `appauthor `. + :param version: See `version `. + :param opinion: See `opinion `. + :param ensure_exists: See `ensure_exists `. + :returns: runtime path shared by users + """ + return PlatformDirs( + appname=appname, + appauthor=appauthor, + version=version, + opinion=opinion, + ensure_exists=ensure_exists, + ).site_runtime_path + + +__all__ = [ + "__version__", + "__version_info__", + "PlatformDirs", + "AppDirs", + "PlatformDirsABC", + "user_data_dir", + "user_config_dir", + "user_cache_dir", + "user_state_dir", + "user_log_dir", + "user_documents_dir", + "user_downloads_dir", + "user_pictures_dir", + "user_videos_dir", + "user_music_dir", + "user_desktop_dir", + "user_runtime_dir", + "site_data_dir", + "site_config_dir", + "site_cache_dir", + "site_runtime_dir", + "user_data_path", + "user_config_path", + "user_cache_path", + "user_state_path", + "user_log_path", + "user_documents_path", + "user_downloads_path", + "user_pictures_path", + "user_videos_path", + "user_music_path", + "user_desktop_path", + "user_runtime_path", + "site_data_path", + "site_config_path", + "site_cache_path", + "site_runtime_path", +] diff --git a/libs/common/platformdirs/__main__.py b/libs/common/platformdirs/__main__.py new file mode 100644 index 00000000..4bf27c9c --- /dev/null +++ b/libs/common/platformdirs/__main__.py @@ -0,0 +1,55 @@ +"""Main entry point.""" + +from __future__ import annotations + +from platformdirs import PlatformDirs, __version__ + +PROPS = ( + "user_data_dir", + "user_config_dir", + "user_cache_dir", + "user_state_dir", + "user_log_dir", + "user_documents_dir", + "user_downloads_dir", + "user_pictures_dir", + "user_videos_dir", + "user_music_dir", + "user_runtime_dir", + "site_data_dir", + "site_config_dir", + "site_cache_dir", + "site_runtime_dir", +) + + +def main() -> None: + """Run main entry point.""" + app_name = "MyApp" + app_author = "MyCompany" + + print(f"-- platformdirs {__version__} --") # noqa: T201 + + print("-- app dirs (with optional 'version')") # noqa: T201 + dirs = PlatformDirs(app_name, app_author, version="1.0") + for prop in PROPS: + print(f"{prop}: {getattr(dirs, prop)}") # noqa: T201 + + print("\n-- app dirs (without optional 'version')") # noqa: T201 + dirs = PlatformDirs(app_name, app_author) + for prop in PROPS: + print(f"{prop}: {getattr(dirs, prop)}") # noqa: T201 + + print("\n-- app dirs (without optional 'appauthor')") # noqa: T201 + dirs = PlatformDirs(app_name) + for prop in PROPS: + print(f"{prop}: {getattr(dirs, prop)}") # noqa: T201 + + print("\n-- app dirs (with disabled 'appauthor')") # noqa: T201 + dirs = PlatformDirs(app_name, appauthor=False) + for prop in PROPS: + print(f"{prop}: {getattr(dirs, prop)}") # noqa: T201 + + +if __name__ == "__main__": + main() diff --git a/libs/common/platformdirs/android.py b/libs/common/platformdirs/android.py new file mode 100644 index 00000000..4acdb638 --- /dev/null +++ b/libs/common/platformdirs/android.py @@ -0,0 +1,221 @@ +"""Android.""" + +from __future__ import annotations + +import os +import re +import sys +from functools import lru_cache +from typing import cast + +from .api import PlatformDirsABC + + +class Android(PlatformDirsABC): + """ + Follows the guidance `from here `_. Makes use of the + `appname `, + `version `, + `ensure_exists `. + """ + + @property + def user_data_dir(self) -> str: + """:return: data directory tied to the user, e.g. ``/data/user///files/``""" + return self._append_app_name_and_version(cast(str, _android_folder()), "files") + + @property + def site_data_dir(self) -> str: + """:return: data directory shared by users, same as `user_data_dir`""" + return self.user_data_dir + + @property + def user_config_dir(self) -> str: + """ + :return: config directory tied to the user, e.g. \ + ``/data/user///shared_prefs/`` + """ + return self._append_app_name_and_version(cast(str, _android_folder()), "shared_prefs") + + @property + def site_config_dir(self) -> str: + """:return: config directory shared by the users, same as `user_config_dir`""" + return self.user_config_dir + + @property + def user_cache_dir(self) -> str: + """:return: cache directory tied to the user, e.g. e.g. ``/data/user///cache/``""" + return self._append_app_name_and_version(cast(str, _android_folder()), "cache") + + @property + def site_cache_dir(self) -> str: + """:return: cache directory shared by users, same as `user_cache_dir`""" + return self.user_cache_dir + + @property + def user_state_dir(self) -> str: + """:return: state directory tied to the user, same as `user_data_dir`""" + return self.user_data_dir + + @property + def user_log_dir(self) -> str: + """ + :return: log directory tied to the user, same as `user_cache_dir` if not opinionated else ``log`` in it, + e.g. ``/data/user///cache//log`` + """ + path = self.user_cache_dir + if self.opinion: + path = os.path.join(path, "log") # noqa: PTH118 + return path + + @property + def user_documents_dir(self) -> str: + """:return: documents directory tied to the user e.g. ``/storage/emulated/0/Documents``""" + return _android_documents_folder() + + @property + def user_downloads_dir(self) -> str: + """:return: downloads directory tied to the user e.g. ``/storage/emulated/0/Downloads``""" + return _android_downloads_folder() + + @property + def user_pictures_dir(self) -> str: + """:return: pictures directory tied to the user e.g. ``/storage/emulated/0/Pictures``""" + return _android_pictures_folder() + + @property + def user_videos_dir(self) -> str: + """:return: videos directory tied to the user e.g. ``/storage/emulated/0/DCIM/Camera``""" + return _android_videos_folder() + + @property + def user_music_dir(self) -> str: + """:return: music directory tied to the user e.g. ``/storage/emulated/0/Music``""" + return _android_music_folder() + + @property + def user_desktop_dir(self) -> str: + """:return: desktop directory tied to the user e.g. ``/storage/emulated/0/Desktop``""" + return "/storage/emulated/0/Desktop" + + @property + def user_runtime_dir(self) -> str: + """ + :return: runtime directory tied to the user, same as `user_cache_dir` if not opinionated else ``tmp`` in it, + e.g. ``/data/user///cache//tmp`` + """ + path = self.user_cache_dir + if self.opinion: + path = os.path.join(path, "tmp") # noqa: PTH118 + return path + + @property + def site_runtime_dir(self) -> str: + """:return: runtime directory shared by users, same as `user_runtime_dir`""" + return self.user_runtime_dir + + +@lru_cache(maxsize=1) +def _android_folder() -> str | None: + """:return: base folder for the Android OS or None if it cannot be found""" + try: + # First try to get path to android app via pyjnius + from jnius import autoclass + + context = autoclass("android.content.Context") + result: str | None = context.getFilesDir().getParentFile().getAbsolutePath() + except Exception: # noqa: BLE001 + # if fails find an android folder looking path on the sys.path + pattern = re.compile(r"/data/(data|user/\d+)/(.+)/files") + for path in sys.path: + if pattern.match(path): + result = path.split("/files")[0] + break + else: + result = None + return result + + +@lru_cache(maxsize=1) +def _android_documents_folder() -> str: + """:return: documents folder for the Android OS""" + # Get directories with pyjnius + try: + from jnius import autoclass + + context = autoclass("android.content.Context") + environment = autoclass("android.os.Environment") + documents_dir: str = context.getExternalFilesDir(environment.DIRECTORY_DOCUMENTS).getAbsolutePath() + except Exception: # noqa: BLE001 + documents_dir = "/storage/emulated/0/Documents" + + return documents_dir + + +@lru_cache(maxsize=1) +def _android_downloads_folder() -> str: + """:return: downloads folder for the Android OS""" + # Get directories with pyjnius + try: + from jnius import autoclass + + context = autoclass("android.content.Context") + environment = autoclass("android.os.Environment") + downloads_dir: str = context.getExternalFilesDir(environment.DIRECTORY_DOWNLOADS).getAbsolutePath() + except Exception: # noqa: BLE001 + downloads_dir = "/storage/emulated/0/Downloads" + + return downloads_dir + + +@lru_cache(maxsize=1) +def _android_pictures_folder() -> str: + """:return: pictures folder for the Android OS""" + # Get directories with pyjnius + try: + from jnius import autoclass + + context = autoclass("android.content.Context") + environment = autoclass("android.os.Environment") + pictures_dir: str = context.getExternalFilesDir(environment.DIRECTORY_PICTURES).getAbsolutePath() + except Exception: # noqa: BLE001 + pictures_dir = "/storage/emulated/0/Pictures" + + return pictures_dir + + +@lru_cache(maxsize=1) +def _android_videos_folder() -> str: + """:return: videos folder for the Android OS""" + # Get directories with pyjnius + try: + from jnius import autoclass + + context = autoclass("android.content.Context") + environment = autoclass("android.os.Environment") + videos_dir: str = context.getExternalFilesDir(environment.DIRECTORY_DCIM).getAbsolutePath() + except Exception: # noqa: BLE001 + videos_dir = "/storage/emulated/0/DCIM/Camera" + + return videos_dir + + +@lru_cache(maxsize=1) +def _android_music_folder() -> str: + """:return: music folder for the Android OS""" + # Get directories with pyjnius + try: + from jnius import autoclass + + context = autoclass("android.content.Context") + environment = autoclass("android.os.Environment") + music_dir: str = context.getExternalFilesDir(environment.DIRECTORY_MUSIC).getAbsolutePath() + except Exception: # noqa: BLE001 + music_dir = "/storage/emulated/0/Music" + + return music_dir + + +__all__ = [ + "Android", +] diff --git a/libs/common/platformdirs/api.py b/libs/common/platformdirs/api.py new file mode 100644 index 00000000..031a38a3 --- /dev/null +++ b/libs/common/platformdirs/api.py @@ -0,0 +1,279 @@ +"""Base API.""" + +from __future__ import annotations + +import os +from abc import ABC, abstractmethod +from pathlib import Path +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Iterator, Literal + + +class PlatformDirsABC(ABC): + """Abstract base class for platform directories.""" + + def __init__( # noqa: PLR0913 + self, + appname: str | None = None, + appauthor: str | None | Literal[False] = None, + version: str | None = None, + roaming: bool = False, # noqa: FBT001, FBT002 + multipath: bool = False, # noqa: FBT001, FBT002 + opinion: bool = True, # noqa: FBT001, FBT002 + ensure_exists: bool = False, # noqa: FBT001, FBT002 + ) -> None: + """ + Create a new platform directory. + + :param appname: See `appname`. + :param appauthor: See `appauthor`. + :param version: See `version`. + :param roaming: See `roaming`. + :param multipath: See `multipath`. + :param opinion: See `opinion`. + :param ensure_exists: See `ensure_exists`. + """ + self.appname = appname #: The name of application. + self.appauthor = appauthor + """ + The name of the app author or distributing body for this application. Typically, it is the owning company name. + Defaults to `appname`. You may pass ``False`` to disable it. + """ + self.version = version + """ + An optional version path element to append to the path. You might want to use this if you want multiple versions + of your app to be able to run independently. If used, this would typically be ``.``. + """ + self.roaming = roaming + """ + Whether to use the roaming appdata directory on Windows. That means that for users on a Windows network setup + for roaming profiles, this user data will be synced on login (see + `here `_). + """ + self.multipath = multipath + """ + An optional parameter which indicates that the entire list of data dirs should be returned. + By default, the first item would only be returned. + """ + self.opinion = opinion #: A flag to indicating to use opinionated values. + self.ensure_exists = ensure_exists + """ + Optionally create the directory (and any missing parents) upon access if it does not exist. + By default, no directories are created. + """ + + def _append_app_name_and_version(self, *base: str) -> str: + params = list(base[1:]) + if self.appname: + params.append(self.appname) + if self.version: + params.append(self.version) + path = os.path.join(base[0], *params) # noqa: PTH118 + self._optionally_create_directory(path) + return path + + def _optionally_create_directory(self, path: str) -> None: + if self.ensure_exists: + Path(path).mkdir(parents=True, exist_ok=True) + + @property + @abstractmethod + def user_data_dir(self) -> str: + """:return: data directory tied to the user""" + + @property + @abstractmethod + def site_data_dir(self) -> str: + """:return: data directory shared by users""" + + @property + @abstractmethod + def user_config_dir(self) -> str: + """:return: config directory tied to the user""" + + @property + @abstractmethod + def site_config_dir(self) -> str: + """:return: config directory shared by the users""" + + @property + @abstractmethod + def user_cache_dir(self) -> str: + """:return: cache directory tied to the user""" + + @property + @abstractmethod + def site_cache_dir(self) -> str: + """:return: cache directory shared by users""" + + @property + @abstractmethod + def user_state_dir(self) -> str: + """:return: state directory tied to the user""" + + @property + @abstractmethod + def user_log_dir(self) -> str: + """:return: log directory tied to the user""" + + @property + @abstractmethod + def user_documents_dir(self) -> str: + """:return: documents directory tied to the user""" + + @property + @abstractmethod + def user_downloads_dir(self) -> str: + """:return: downloads directory tied to the user""" + + @property + @abstractmethod + def user_pictures_dir(self) -> str: + """:return: pictures directory tied to the user""" + + @property + @abstractmethod + def user_videos_dir(self) -> str: + """:return: videos directory tied to the user""" + + @property + @abstractmethod + def user_music_dir(self) -> str: + """:return: music directory tied to the user""" + + @property + @abstractmethod + def user_desktop_dir(self) -> str: + """:return: desktop directory tied to the user""" + + @property + @abstractmethod + def user_runtime_dir(self) -> str: + """:return: runtime directory tied to the user""" + + @property + @abstractmethod + def site_runtime_dir(self) -> str: + """:return: runtime directory shared by users""" + + @property + def user_data_path(self) -> Path: + """:return: data path tied to the user""" + return Path(self.user_data_dir) + + @property + def site_data_path(self) -> Path: + """:return: data path shared by users""" + return Path(self.site_data_dir) + + @property + def user_config_path(self) -> Path: + """:return: config path tied to the user""" + return Path(self.user_config_dir) + + @property + def site_config_path(self) -> Path: + """:return: config path shared by the users""" + return Path(self.site_config_dir) + + @property + def user_cache_path(self) -> Path: + """:return: cache path tied to the user""" + return Path(self.user_cache_dir) + + @property + def site_cache_path(self) -> Path: + """:return: cache path shared by users""" + return Path(self.site_cache_dir) + + @property + def user_state_path(self) -> Path: + """:return: state path tied to the user""" + return Path(self.user_state_dir) + + @property + def user_log_path(self) -> Path: + """:return: log path tied to the user""" + return Path(self.user_log_dir) + + @property + def user_documents_path(self) -> Path: + """:return: documents path tied to the user""" + return Path(self.user_documents_dir) + + @property + def user_downloads_path(self) -> Path: + """:return: downloads path tied to the user""" + return Path(self.user_downloads_dir) + + @property + def user_pictures_path(self) -> Path: + """:return: pictures path tied to the user""" + return Path(self.user_pictures_dir) + + @property + def user_videos_path(self) -> Path: + """:return: videos path tied to the user""" + return Path(self.user_videos_dir) + + @property + def user_music_path(self) -> Path: + """:return: music path tied to the user""" + return Path(self.user_music_dir) + + @property + def user_desktop_path(self) -> Path: + """:return: desktop path tied to the user""" + return Path(self.user_desktop_dir) + + @property + def user_runtime_path(self) -> Path: + """:return: runtime path tied to the user""" + return Path(self.user_runtime_dir) + + @property + def site_runtime_path(self) -> Path: + """:return: runtime path shared by users""" + return Path(self.site_runtime_dir) + + def iter_config_dirs(self) -> Iterator[str]: + """:yield: all user and site configuration directories.""" + yield self.user_config_dir + yield self.site_config_dir + + def iter_data_dirs(self) -> Iterator[str]: + """:yield: all user and site data directories.""" + yield self.user_data_dir + yield self.site_data_dir + + def iter_cache_dirs(self) -> Iterator[str]: + """:yield: all user and site cache directories.""" + yield self.user_cache_dir + yield self.site_cache_dir + + def iter_runtime_dirs(self) -> Iterator[str]: + """:yield: all user and site runtime directories.""" + yield self.user_runtime_dir + yield self.site_runtime_dir + + def iter_config_paths(self) -> Iterator[Path]: + """:yield: all user and site configuration paths.""" + for path in self.iter_config_dirs(): + yield Path(path) + + def iter_data_paths(self) -> Iterator[Path]: + """:yield: all user and site data paths.""" + for path in self.iter_data_dirs(): + yield Path(path) + + def iter_cache_paths(self) -> Iterator[Path]: + """:yield: all user and site cache paths.""" + for path in self.iter_cache_dirs(): + yield Path(path) + + def iter_runtime_paths(self) -> Iterator[Path]: + """:yield: all user and site runtime paths.""" + for path in self.iter_runtime_dirs(): + yield Path(path) diff --git a/libs/common/platformdirs/macos.py b/libs/common/platformdirs/macos.py new file mode 100644 index 00000000..b7b48808 --- /dev/null +++ b/libs/common/platformdirs/macos.py @@ -0,0 +1,127 @@ +"""macOS.""" + +from __future__ import annotations + +import os.path +import sys + +from .api import PlatformDirsABC + + +class MacOS(PlatformDirsABC): + """ + Platform directories for the macOS operating system. Follows the guidance from `Apple documentation + `_. + Makes use of the `appname `, + `version `, + `ensure_exists `. + """ + + @property + def user_data_dir(self) -> str: + """:return: data directory tied to the user, e.g. ``~/Library/Application Support/$appname/$version``""" + return self._append_app_name_and_version(os.path.expanduser("~/Library/Application Support")) # noqa: PTH111 + + @property + def site_data_dir(self) -> str: + """ + :return: data directory shared by users, e.g. ``/Library/Application Support/$appname/$version``. + If we're using a Python binary managed by `Homebrew `_, the directory + will be under the Homebrew prefix, e.g. ``/opt/homebrew/share/$appname/$version``. + If `multipath ` is enabled and we're in Homebrew, + the response is a multi-path string separated by ":", e.g. + ``/opt/homebrew/share/$appname/$version:/Library/Application Support/$appname/$version`` + """ + is_homebrew = sys.prefix.startswith("/opt/homebrew") + path_list = [self._append_app_name_and_version("/opt/homebrew/share")] if is_homebrew else [] + path_list.append(self._append_app_name_and_version("/Library/Application Support")) + if self.multipath: + return os.pathsep.join(path_list) + return path_list[0] + + @property + def user_config_dir(self) -> str: + """:return: config directory tied to the user, same as `user_data_dir`""" + return self.user_data_dir + + @property + def site_config_dir(self) -> str: + """:return: config directory shared by the users, same as `site_data_dir`""" + return self.site_data_dir + + @property + def user_cache_dir(self) -> str: + """:return: cache directory tied to the user, e.g. ``~/Library/Caches/$appname/$version``""" + return self._append_app_name_and_version(os.path.expanduser("~/Library/Caches")) # noqa: PTH111 + + @property + def site_cache_dir(self) -> str: + """ + :return: cache directory shared by users, e.g. ``/Library/Caches/$appname/$version``. + If we're using a Python binary managed by `Homebrew `_, the directory + will be under the Homebrew prefix, e.g. ``/opt/homebrew/var/cache/$appname/$version``. + If `multipath ` is enabled and we're in Homebrew, + the response is a multi-path string separated by ":", e.g. + ``/opt/homebrew/var/cache/$appname/$version:/Library/Caches/$appname/$version`` + """ + is_homebrew = sys.prefix.startswith("/opt/homebrew") + path_list = [self._append_app_name_and_version("/opt/homebrew/var/cache")] if is_homebrew else [] + path_list.append(self._append_app_name_and_version("/Library/Caches")) + if self.multipath: + return os.pathsep.join(path_list) + return path_list[0] + + @property + def user_state_dir(self) -> str: + """:return: state directory tied to the user, same as `user_data_dir`""" + return self.user_data_dir + + @property + def user_log_dir(self) -> str: + """:return: log directory tied to the user, e.g. ``~/Library/Logs/$appname/$version``""" + return self._append_app_name_and_version(os.path.expanduser("~/Library/Logs")) # noqa: PTH111 + + @property + def user_documents_dir(self) -> str: + """:return: documents directory tied to the user, e.g. ``~/Documents``""" + return os.path.expanduser("~/Documents") # noqa: PTH111 + + @property + def user_downloads_dir(self) -> str: + """:return: downloads directory tied to the user, e.g. ``~/Downloads``""" + return os.path.expanduser("~/Downloads") # noqa: PTH111 + + @property + def user_pictures_dir(self) -> str: + """:return: pictures directory tied to the user, e.g. ``~/Pictures``""" + return os.path.expanduser("~/Pictures") # noqa: PTH111 + + @property + def user_videos_dir(self) -> str: + """:return: videos directory tied to the user, e.g. ``~/Movies``""" + return os.path.expanduser("~/Movies") # noqa: PTH111 + + @property + def user_music_dir(self) -> str: + """:return: music directory tied to the user, e.g. ``~/Music``""" + return os.path.expanduser("~/Music") # noqa: PTH111 + + @property + def user_desktop_dir(self) -> str: + """:return: desktop directory tied to the user, e.g. ``~/Desktop``""" + return os.path.expanduser("~/Desktop") # noqa: PTH111 + + @property + def user_runtime_dir(self) -> str: + """:return: runtime directory tied to the user, e.g. ``~/Library/Caches/TemporaryItems/$appname/$version``""" + return self._append_app_name_and_version(os.path.expanduser("~/Library/Caches/TemporaryItems")) # noqa: PTH111 + + @property + def site_runtime_dir(self) -> str: + """:return: runtime directory shared by users, same as `user_runtime_dir`""" + return self.user_runtime_dir + + +__all__ = [ + "MacOS", +] diff --git a/libs/common/platformdirs/py.typed b/libs/common/platformdirs/py.typed new file mode 100644 index 00000000..e69de29b diff --git a/libs/common/platformdirs/unix.py b/libs/common/platformdirs/unix.py new file mode 100644 index 00000000..ca4728e6 --- /dev/null +++ b/libs/common/platformdirs/unix.py @@ -0,0 +1,270 @@ +"""Unix.""" + +from __future__ import annotations + +import os +import sys +from configparser import ConfigParser +from pathlib import Path +from typing import Iterator + +from .api import PlatformDirsABC + +if sys.platform == "win32": + + def getuid() -> int: + msg = "should only be used on Unix" + raise RuntimeError(msg) + +else: + from os import getuid + + +class Unix(PlatformDirsABC): + """ + On Unix/Linux, we follow the + `XDG Basedir Spec `_. The spec allows + overriding directories with environment variables. The examples show are the default values, alongside the name of + the environment variable that overrides them. Makes use of the + `appname `, + `version `, + `multipath `, + `opinion `, + `ensure_exists `. + """ + + @property + def user_data_dir(self) -> str: + """ + :return: data directory tied to the user, e.g. ``~/.local/share/$appname/$version`` or + ``$XDG_DATA_HOME/$appname/$version`` + """ + path = os.environ.get("XDG_DATA_HOME", "") + if not path.strip(): + path = os.path.expanduser("~/.local/share") # noqa: PTH111 + return self._append_app_name_and_version(path) + + @property + def _site_data_dirs(self) -> list[str]: + path = os.environ.get("XDG_DATA_DIRS", "") + if not path.strip(): + path = f"/usr/local/share{os.pathsep}/usr/share" + return [self._append_app_name_and_version(p) for p in path.split(os.pathsep)] + + @property + def site_data_dir(self) -> str: + """ + :return: data directories shared by users (if `multipath ` is + enabled and ``XDG_DATA_DIRS`` is set and a multi path the response is also a multi path separated by the + OS path separator), e.g. ``/usr/local/share/$appname/$version`` or ``/usr/share/$appname/$version`` + """ + # XDG default for $XDG_DATA_DIRS; only first, if multipath is False + dirs = self._site_data_dirs + if not self.multipath: + return dirs[0] + return os.pathsep.join(dirs) + + @property + def user_config_dir(self) -> str: + """ + :return: config directory tied to the user, e.g. ``~/.config/$appname/$version`` or + ``$XDG_CONFIG_HOME/$appname/$version`` + """ + path = os.environ.get("XDG_CONFIG_HOME", "") + if not path.strip(): + path = os.path.expanduser("~/.config") # noqa: PTH111 + return self._append_app_name_and_version(path) + + @property + def _site_config_dirs(self) -> list[str]: + path = os.environ.get("XDG_CONFIG_DIRS", "") + if not path.strip(): + path = "/etc/xdg" + return [self._append_app_name_and_version(p) for p in path.split(os.pathsep)] + + @property + def site_config_dir(self) -> str: + """ + :return: config directories shared by users (if `multipath ` + is enabled and ``XDG_CONFIG_DIRS`` is set and a multi path the response is also a multi path separated by + the OS path separator), e.g. ``/etc/xdg/$appname/$version`` + """ + # XDG default for $XDG_CONFIG_DIRS only first, if multipath is False + dirs = self._site_config_dirs + if not self.multipath: + return dirs[0] + return os.pathsep.join(dirs) + + @property + def user_cache_dir(self) -> str: + """ + :return: cache directory tied to the user, e.g. ``~/.cache/$appname/$version`` or + ``~/$XDG_CACHE_HOME/$appname/$version`` + """ + path = os.environ.get("XDG_CACHE_HOME", "") + if not path.strip(): + path = os.path.expanduser("~/.cache") # noqa: PTH111 + return self._append_app_name_and_version(path) + + @property + def site_cache_dir(self) -> str: + """:return: cache directory shared by users, e.g. ``/var/cache/$appname/$version``""" + return self._append_app_name_and_version("/var/cache") + + @property + def user_state_dir(self) -> str: + """ + :return: state directory tied to the user, e.g. ``~/.local/state/$appname/$version`` or + ``$XDG_STATE_HOME/$appname/$version`` + """ + path = os.environ.get("XDG_STATE_HOME", "") + if not path.strip(): + path = os.path.expanduser("~/.local/state") # noqa: PTH111 + return self._append_app_name_and_version(path) + + @property + def user_log_dir(self) -> str: + """:return: log directory tied to the user, same as `user_state_dir` if not opinionated else ``log`` in it""" + path = self.user_state_dir + if self.opinion: + path = os.path.join(path, "log") # noqa: PTH118 + self._optionally_create_directory(path) + return path + + @property + def user_documents_dir(self) -> str: + """:return: documents directory tied to the user, e.g. ``~/Documents``""" + return _get_user_media_dir("XDG_DOCUMENTS_DIR", "~/Documents") + + @property + def user_downloads_dir(self) -> str: + """:return: downloads directory tied to the user, e.g. ``~/Downloads``""" + return _get_user_media_dir("XDG_DOWNLOAD_DIR", "~/Downloads") + + @property + def user_pictures_dir(self) -> str: + """:return: pictures directory tied to the user, e.g. ``~/Pictures``""" + return _get_user_media_dir("XDG_PICTURES_DIR", "~/Pictures") + + @property + def user_videos_dir(self) -> str: + """:return: videos directory tied to the user, e.g. ``~/Videos``""" + return _get_user_media_dir("XDG_VIDEOS_DIR", "~/Videos") + + @property + def user_music_dir(self) -> str: + """:return: music directory tied to the user, e.g. ``~/Music``""" + return _get_user_media_dir("XDG_MUSIC_DIR", "~/Music") + + @property + def user_desktop_dir(self) -> str: + """:return: desktop directory tied to the user, e.g. ``~/Desktop``""" + return _get_user_media_dir("XDG_DESKTOP_DIR", "~/Desktop") + + @property + def user_runtime_dir(self) -> str: + """ + :return: runtime directory tied to the user, e.g. ``/run/user/$(id -u)/$appname/$version`` or + ``$XDG_RUNTIME_DIR/$appname/$version``. + + For FreeBSD/OpenBSD/NetBSD, it would return ``/var/run/user/$(id -u)/$appname/$version`` if + exists, otherwise ``/tmp/runtime-$(id -u)/$appname/$version``, if``$XDG_RUNTIME_DIR`` + is not set. + """ + path = os.environ.get("XDG_RUNTIME_DIR", "") + if not path.strip(): + if sys.platform.startswith(("freebsd", "openbsd", "netbsd")): + path = f"/var/run/user/{getuid()}" + if not Path(path).exists(): + path = f"/tmp/runtime-{getuid()}" # noqa: S108 + else: + path = f"/run/user/{getuid()}" + return self._append_app_name_and_version(path) + + @property + def site_runtime_dir(self) -> str: + """ + :return: runtime directory shared by users, e.g. ``/run/$appname/$version`` or \ + ``$XDG_RUNTIME_DIR/$appname/$version``. + + Note that this behaves almost exactly like `user_runtime_dir` if ``$XDG_RUNTIME_DIR`` is set, but will + fall back to paths associated to the root user instead of a regular logged-in user if it's not set. + + If you wish to ensure that a logged-in root user path is returned e.g. ``/run/user/0``, use `user_runtime_dir` + instead. + + For FreeBSD/OpenBSD/NetBSD, it would return ``/var/run/$appname/$version`` if ``$XDG_RUNTIME_DIR`` is not set. + """ + path = os.environ.get("XDG_RUNTIME_DIR", "") + if not path.strip(): + if sys.platform.startswith(("freebsd", "openbsd", "netbsd")): + path = "/var/run" + else: + path = "/run" + return self._append_app_name_and_version(path) + + @property + def site_data_path(self) -> Path: + """:return: data path shared by users. Only return first item, even if ``multipath`` is set to ``True``""" + return self._first_item_as_path_if_multipath(self.site_data_dir) + + @property + def site_config_path(self) -> Path: + """:return: config path shared by the users. Only return first item, even if ``multipath`` is set to ``True``""" + return self._first_item_as_path_if_multipath(self.site_config_dir) + + @property + def site_cache_path(self) -> Path: + """:return: cache path shared by users. Only return first item, even if ``multipath`` is set to ``True``""" + return self._first_item_as_path_if_multipath(self.site_cache_dir) + + def _first_item_as_path_if_multipath(self, directory: str) -> Path: + if self.multipath: + # If multipath is True, the first path is returned. + directory = directory.split(os.pathsep)[0] + return Path(directory) + + def iter_config_dirs(self) -> Iterator[str]: + """:yield: all user and site configuration directories.""" + yield self.user_config_dir + yield from self._site_config_dirs + + def iter_data_dirs(self) -> Iterator[str]: + """:yield: all user and site data directories.""" + yield self.user_data_dir + yield from self._site_data_dirs + + +def _get_user_media_dir(env_var: str, fallback_tilde_path: str) -> str: + media_dir = _get_user_dirs_folder(env_var) + if media_dir is None: + media_dir = os.environ.get(env_var, "").strip() + if not media_dir: + media_dir = os.path.expanduser(fallback_tilde_path) # noqa: PTH111 + + return media_dir + + +def _get_user_dirs_folder(key: str) -> str | None: + """Return directory from user-dirs.dirs config file. See https://freedesktop.org/wiki/Software/xdg-user-dirs/.""" + user_dirs_config_path = Path(Unix().user_config_dir) / "user-dirs.dirs" + if user_dirs_config_path.exists(): + parser = ConfigParser() + + with user_dirs_config_path.open() as stream: + # Add fake section header, so ConfigParser doesn't complain + parser.read_string(f"[top]\n{stream.read()}") + + if key not in parser["top"]: + return None + + path = parser["top"][key].strip('"') + # Handle relative home paths + return path.replace("$HOME", os.path.expanduser("~")) # noqa: PTH111 + + return None + + +__all__ = [ + "Unix", +] diff --git a/libs/common/platformdirs/version.py b/libs/common/platformdirs/version.py new file mode 100644 index 00000000..cc1e3456 --- /dev/null +++ b/libs/common/platformdirs/version.py @@ -0,0 +1,16 @@ +# file generated by setuptools_scm +# don't change, don't track in version control +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Tuple, Union + VERSION_TUPLE = Tuple[Union[int, str], ...] +else: + VERSION_TUPLE = object + +version: str +__version__: str +__version_tuple__: VERSION_TUPLE +version_tuple: VERSION_TUPLE + +__version__ = version = '4.2.0' +__version_tuple__ = version_tuple = (4, 2, 0) diff --git a/libs/common/platformdirs/windows.py b/libs/common/platformdirs/windows.py new file mode 100644 index 00000000..c62d0c8d --- /dev/null +++ b/libs/common/platformdirs/windows.py @@ -0,0 +1,267 @@ +"""Windows.""" + +from __future__ import annotations + +import ctypes +import os +import sys +from functools import lru_cache +from typing import TYPE_CHECKING + +from .api import PlatformDirsABC + +if TYPE_CHECKING: + from collections.abc import Callable + + +class Windows(PlatformDirsABC): + """ + `MSDN on where to store app data files + `_. + Makes use of the + `appname `, + `appauthor `, + `version `, + `roaming `, + `opinion `, + `ensure_exists `. + """ + + @property + def user_data_dir(self) -> str: + """ + :return: data directory tied to the user, e.g. + ``%USERPROFILE%\\AppData\\Local\\$appauthor\\$appname`` (not roaming) or + ``%USERPROFILE%\\AppData\\Roaming\\$appauthor\\$appname`` (roaming) + """ + const = "CSIDL_APPDATA" if self.roaming else "CSIDL_LOCAL_APPDATA" + path = os.path.normpath(get_win_folder(const)) + return self._append_parts(path) + + def _append_parts(self, path: str, *, opinion_value: str | None = None) -> str: + params = [] + if self.appname: + if self.appauthor is not False: + author = self.appauthor or self.appname + params.append(author) + params.append(self.appname) + if opinion_value is not None and self.opinion: + params.append(opinion_value) + if self.version: + params.append(self.version) + path = os.path.join(path, *params) # noqa: PTH118 + self._optionally_create_directory(path) + return path + + @property + def site_data_dir(self) -> str: + """:return: data directory shared by users, e.g. ``C:\\ProgramData\\$appauthor\\$appname``""" + path = os.path.normpath(get_win_folder("CSIDL_COMMON_APPDATA")) + return self._append_parts(path) + + @property + def user_config_dir(self) -> str: + """:return: config directory tied to the user, same as `user_data_dir`""" + return self.user_data_dir + + @property + def site_config_dir(self) -> str: + """:return: config directory shared by the users, same as `site_data_dir`""" + return self.site_data_dir + + @property + def user_cache_dir(self) -> str: + """ + :return: cache directory tied to the user (if opinionated with ``Cache`` folder within ``$appname``) e.g. + ``%USERPROFILE%\\AppData\\Local\\$appauthor\\$appname\\Cache\\$version`` + """ + path = os.path.normpath(get_win_folder("CSIDL_LOCAL_APPDATA")) + return self._append_parts(path, opinion_value="Cache") + + @property + def site_cache_dir(self) -> str: + """:return: cache directory shared by users, e.g. ``C:\\ProgramData\\$appauthor\\$appname\\Cache\\$version``""" + path = os.path.normpath(get_win_folder("CSIDL_COMMON_APPDATA")) + return self._append_parts(path, opinion_value="Cache") + + @property + def user_state_dir(self) -> str: + """:return: state directory tied to the user, same as `user_data_dir`""" + return self.user_data_dir + + @property + def user_log_dir(self) -> str: + """:return: log directory tied to the user, same as `user_data_dir` if not opinionated else ``Logs`` in it""" + path = self.user_data_dir + if self.opinion: + path = os.path.join(path, "Logs") # noqa: PTH118 + self._optionally_create_directory(path) + return path + + @property + def user_documents_dir(self) -> str: + """:return: documents directory tied to the user e.g. ``%USERPROFILE%\\Documents``""" + return os.path.normpath(get_win_folder("CSIDL_PERSONAL")) + + @property + def user_downloads_dir(self) -> str: + """:return: downloads directory tied to the user e.g. ``%USERPROFILE%\\Downloads``""" + return os.path.normpath(get_win_folder("CSIDL_DOWNLOADS")) + + @property + def user_pictures_dir(self) -> str: + """:return: pictures directory tied to the user e.g. ``%USERPROFILE%\\Pictures``""" + return os.path.normpath(get_win_folder("CSIDL_MYPICTURES")) + + @property + def user_videos_dir(self) -> str: + """:return: videos directory tied to the user e.g. ``%USERPROFILE%\\Videos``""" + return os.path.normpath(get_win_folder("CSIDL_MYVIDEO")) + + @property + def user_music_dir(self) -> str: + """:return: music directory tied to the user e.g. ``%USERPROFILE%\\Music``""" + return os.path.normpath(get_win_folder("CSIDL_MYMUSIC")) + + @property + def user_desktop_dir(self) -> str: + """:return: desktop directory tied to the user, e.g. ``%USERPROFILE%\\Desktop``""" + return os.path.normpath(get_win_folder("CSIDL_DESKTOPDIRECTORY")) + + @property + def user_runtime_dir(self) -> str: + """ + :return: runtime directory tied to the user, e.g. + ``%USERPROFILE%\\AppData\\Local\\Temp\\$appauthor\\$appname`` + """ + path = os.path.normpath(os.path.join(get_win_folder("CSIDL_LOCAL_APPDATA"), "Temp")) # noqa: PTH118 + return self._append_parts(path) + + @property + def site_runtime_dir(self) -> str: + """:return: runtime directory shared by users, same as `user_runtime_dir`""" + return self.user_runtime_dir + + +def get_win_folder_from_env_vars(csidl_name: str) -> str: + """Get folder from environment variables.""" + result = get_win_folder_if_csidl_name_not_env_var(csidl_name) + if result is not None: + return result + + env_var_name = { + "CSIDL_APPDATA": "APPDATA", + "CSIDL_COMMON_APPDATA": "ALLUSERSPROFILE", + "CSIDL_LOCAL_APPDATA": "LOCALAPPDATA", + }.get(csidl_name) + if env_var_name is None: + msg = f"Unknown CSIDL name: {csidl_name}" + raise ValueError(msg) + result = os.environ.get(env_var_name) + if result is None: + msg = f"Unset environment variable: {env_var_name}" + raise ValueError(msg) + return result + + +def get_win_folder_if_csidl_name_not_env_var(csidl_name: str) -> str | None: + """Get folder for a CSIDL name that does not exist as an environment variable.""" + if csidl_name == "CSIDL_PERSONAL": + return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Documents") # noqa: PTH118 + + if csidl_name == "CSIDL_DOWNLOADS": + return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Downloads") # noqa: PTH118 + + if csidl_name == "CSIDL_MYPICTURES": + return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Pictures") # noqa: PTH118 + + if csidl_name == "CSIDL_MYVIDEO": + return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Videos") # noqa: PTH118 + + if csidl_name == "CSIDL_MYMUSIC": + return os.path.join(os.path.normpath(os.environ["USERPROFILE"]), "Music") # noqa: PTH118 + return None + + +def get_win_folder_from_registry(csidl_name: str) -> str: + """ + Get folder from the registry. + + This is a fallback technique at best. I'm not sure if using the registry for these guarantees us the correct answer + for all CSIDL_* names. + """ + shell_folder_name = { + "CSIDL_APPDATA": "AppData", + "CSIDL_COMMON_APPDATA": "Common AppData", + "CSIDL_LOCAL_APPDATA": "Local AppData", + "CSIDL_PERSONAL": "Personal", + "CSIDL_DOWNLOADS": "{374DE290-123F-4565-9164-39C4925E467B}", + "CSIDL_MYPICTURES": "My Pictures", + "CSIDL_MYVIDEO": "My Video", + "CSIDL_MYMUSIC": "My Music", + }.get(csidl_name) + if shell_folder_name is None: + msg = f"Unknown CSIDL name: {csidl_name}" + raise ValueError(msg) + if sys.platform != "win32": # only needed for mypy type checker to know that this code runs only on Windows + raise NotImplementedError + import winreg + + key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders") + directory, _ = winreg.QueryValueEx(key, shell_folder_name) + return str(directory) + + +def get_win_folder_via_ctypes(csidl_name: str) -> str: + """Get folder with ctypes.""" + # There is no 'CSIDL_DOWNLOADS'. + # Use 'CSIDL_PROFILE' (40) and append the default folder 'Downloads' instead. + # https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid + + csidl_const = { + "CSIDL_APPDATA": 26, + "CSIDL_COMMON_APPDATA": 35, + "CSIDL_LOCAL_APPDATA": 28, + "CSIDL_PERSONAL": 5, + "CSIDL_MYPICTURES": 39, + "CSIDL_MYVIDEO": 14, + "CSIDL_MYMUSIC": 13, + "CSIDL_DOWNLOADS": 40, + "CSIDL_DESKTOPDIRECTORY": 16, + }.get(csidl_name) + if csidl_const is None: + msg = f"Unknown CSIDL name: {csidl_name}" + raise ValueError(msg) + + buf = ctypes.create_unicode_buffer(1024) + windll = getattr(ctypes, "windll") # noqa: B009 # using getattr to avoid false positive with mypy type checker + windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf) + + # Downgrade to short path name if it has high-bit chars. + if any(ord(c) > 255 for c in buf): # noqa: PLR2004 + buf2 = ctypes.create_unicode_buffer(1024) + if windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024): + buf = buf2 + + if csidl_name == "CSIDL_DOWNLOADS": + return os.path.join(buf.value, "Downloads") # noqa: PTH118 + + return buf.value + + +def _pick_get_win_folder() -> Callable[[str], str]: + if hasattr(ctypes, "windll"): + return get_win_folder_via_ctypes + try: + import winreg # noqa: F401 + except ImportError: + return get_win_folder_from_env_vars + else: + return get_win_folder_from_registry + + +get_win_folder = lru_cache(maxsize=None)(_pick_get_win_folder()) + +__all__ = [ + "Windows", +] diff --git a/libs/common/setuptools/__init__.py b/libs/common/setuptools/__init__.py index 9d8ae1ed..563ca1c4 100644 --- a/libs/common/setuptools/__init__.py +++ b/libs/common/setuptools/__init__.py @@ -1,129 +1,40 @@ """Extensions to the 'distutils' for large or complex distributions""" -import os -import sys import functools -import distutils.core -import distutils.filelist +import os import re + +import _distutils_hack.override # noqa: F401 +import distutils.core from distutils.errors import DistutilsOptionError -from distutils.util import convert_path -from fnmatch import fnmatchcase - -from ._deprecation_warning import SetuptoolsDeprecationWarning - -from setuptools.extern.six import PY3, string_types -from setuptools.extern.six.moves import filter, map - -import setuptools.version -from setuptools.extension import Extension -from setuptools.dist import Distribution, Feature -from setuptools.depends import Require -from . import monkey - -__metaclass__ = type +from distutils.util import convert_path as _convert_path +from . import logging, monkey +from . import version as _version_module +from .depends import Require +from .discovery import PackageFinder, PEP420PackageFinder +from .dist import Distribution +from .extension import Extension +from .warnings import SetuptoolsDeprecationWarning __all__ = [ - 'setup', 'Distribution', 'Feature', 'Command', 'Extension', 'Require', + 'setup', + 'Distribution', + 'Command', + 'Extension', + 'Require', 'SetuptoolsDeprecationWarning', - 'find_packages' + 'find_packages', + 'find_namespace_packages', ] -if PY3: - __all__.append('find_namespace_packages') - -__version__ = setuptools.version.__version__ +__version__ = _version_module.__version__ bootstrap_install_from = None -# If we run 2to3 on .py files, should we also convert docstrings? -# Default: yes; assume that we can detect doctests reliably -run_2to3_on_doctests = True -# Standard package names for fixer packages -lib2to3_fixer_packages = ['lib2to3.fixes'] - - -class PackageFinder: - """ - Generate a list of all Python packages found within a directory - """ - - @classmethod - def find(cls, where='.', exclude=(), include=('*',)): - """Return a list all Python packages found within directory 'where' - - 'where' is the root directory which will be searched for packages. It - should be supplied as a "cross-platform" (i.e. URL-style) path; it will - be converted to the appropriate local path syntax. - - 'exclude' is a sequence of package names to exclude; '*' can be used - as a wildcard in the names, such that 'foo.*' will exclude all - subpackages of 'foo' (but not 'foo' itself). - - 'include' is a sequence of package names to include. If it's - specified, only the named packages will be included. If it's not - specified, all found packages will be included. 'include' can contain - shell style wildcard patterns just like 'exclude'. - """ - - return list(cls._find_packages_iter( - convert_path(where), - cls._build_filter('ez_setup', '*__pycache__', *exclude), - cls._build_filter(*include))) - - @classmethod - def _find_packages_iter(cls, where, exclude, include): - """ - All the packages found in 'where' that pass the 'include' filter, but - not the 'exclude' filter. - """ - for root, dirs, files in os.walk(where, followlinks=True): - # Copy dirs to iterate over it, then empty dirs. - all_dirs = dirs[:] - dirs[:] = [] - - for dir in all_dirs: - full_path = os.path.join(root, dir) - rel_path = os.path.relpath(full_path, where) - package = rel_path.replace(os.path.sep, '.') - - # Skip directory trees that are not valid packages - if ('.' in dir or not cls._looks_like_package(full_path)): - continue - - # Should this package be included? - if include(package) and not exclude(package): - yield package - - # Keep searching subdirectories, as there may be more packages - # down there, even if the parent was excluded. - dirs.append(dir) - - @staticmethod - def _looks_like_package(path): - """Does a directory look like a package?""" - return os.path.isfile(os.path.join(path, '__init__.py')) - - @staticmethod - def _build_filter(*patterns): - """ - Given a list of patterns, return a callable that will be true only if - the input matches at least one of the patterns. - """ - return lambda name: any(fnmatchcase(name, pat=pat) for pat in patterns) - - -class PEP420PackageFinder(PackageFinder): - @staticmethod - def _looks_like_package(path): - return True - find_packages = PackageFinder.find - -if PY3: - find_namespace_packages = PEP420PackageFinder.find +find_namespace_packages = PEP420PackageFinder.find def _install_setup_requires(attrs): @@ -134,13 +45,21 @@ def _install_setup_requires(attrs): A minimal version of a distribution for supporting the fetch_build_eggs interface. """ + def __init__(self, attrs): _incl = 'dependency_links', 'setup_requires' - filtered = { - k: attrs[k] - for k in set(_incl) & set(attrs) - } - distutils.core.Distribution.__init__(self, filtered) + filtered = {k: attrs[k] for k in set(_incl) & set(attrs)} + super().__init__(filtered) + # Prevent accidentally triggering discovery with incomplete set of attrs + self.set_defaults._disable() + + def _get_project_config_files(self, filenames=None): + """Ignore ``pyproject.toml``, they are not related to setup_requires""" + try: + cfg, toml = super()._split_standard_project_metadata(filenames) + return cfg, () + except Exception: + return filenames, () def finalize_options(self): """ @@ -153,14 +72,37 @@ def _install_setup_requires(attrs): # Honor setup.cfg's options. dist.parse_config_files(ignore_option_errors=True) if dist.setup_requires: + _fetch_build_eggs(dist) + + +def _fetch_build_eggs(dist): + try: dist.fetch_build_eggs(dist.setup_requires) + except Exception as ex: + msg = """ + It is possible a package already installed in your system + contains an version that is invalid according to PEP 440. + You can try `pip install --use-pep517` as a workaround for this problem, + or rely on a new virtual environment. + + If the problem refers to a package that is not installed yet, + please contact that package's maintainers or distributors. + """ + if "InvalidVersion" in ex.__class__.__name__: + if hasattr(ex, "add_note"): + ex.add_note(msg) # PEP 678 + else: + dist.announce(f"\n{msg}\n") + raise def setup(**attrs): # Make sure we have any requirements needed to interpret 'attrs'. + logging.configure() _install_setup_requires(attrs) return distutils.core.setup(**attrs) + setup.__doc__ = distutils.core.setup.__doc__ @@ -168,7 +110,59 @@ _Command = monkey.get_unpatched(distutils.core.Command) class Command(_Command): - __doc__ = _Command.__doc__ + """ + Setuptools internal actions are organized using a *command design pattern*. + This means that each action (or group of closely related actions) executed during + the build should be implemented as a ``Command`` subclass. + + These commands are abstractions and do not necessarily correspond to a command that + can (or should) be executed via a terminal, in a CLI fashion (although historically + they would). + + When creating a new command from scratch, custom defined classes **SHOULD** inherit + from ``setuptools.Command`` and implement a few mandatory methods. + Between these mandatory methods, are listed: + + .. method:: initialize_options(self) + + Set or (reset) all options/attributes/caches used by the command + to their default values. Note that these values may be overwritten during + the build. + + .. method:: finalize_options(self) + + Set final values for all options/attributes used by the command. + Most of the time, each option/attribute/cache should only be set if it does not + have any value yet (e.g. ``if self.attr is None: self.attr = val``). + + .. method:: run(self) + + Execute the actions intended by the command. + (Side effects **SHOULD** only take place when ``run`` is executed, + for example, creating new files or writing to the terminal output). + + A useful analogy for command classes is to think of them as subroutines with local + variables called "options". The options are "declared" in ``initialize_options()`` + and "defined" (given their final values, aka "finalized") in ``finalize_options()``, + both of which must be defined by every command class. The "body" of the subroutine, + (where it does all the work) is the ``run()`` method. + Between ``initialize_options()`` and ``finalize_options()``, ``setuptools`` may set + the values for options/attributes based on user's input (or circumstance), + which means that the implementation should be careful to not overwrite values in + ``finalize_options`` unless necessary. + + Please note that other commands (or other parts of setuptools) may also overwrite + the values of the command's options/attributes multiple times during the build + process. + Therefore it is important to consistently implement ``initialize_options()`` and + ``finalize_options()``. For example, all derived attributes (or attributes that + depend on the value of other attributes) **SHOULD** be recomputed in + ``finalize_options``. + + When overwriting existing commands, custom defined classes **MUST** abide by the + same APIs implemented by the original class. They also **SHOULD** inherit from the + original class. + """ command_consumes_arguments = False @@ -177,7 +171,7 @@ class Command(_Command): Construct the command for dist, updating vars(self) with any keyword parameters. """ - _Command.__init__(self, dist) + super().__init__(dist) vars(self).update(kw) def _ensure_stringlike(self, option, what, default=None): @@ -185,9 +179,10 @@ class Command(_Command): if val is None: setattr(self, option, default) return default - elif not isinstance(val, string_types): - raise DistutilsOptionError("'%s' must be a %s (got `%s`)" - % (option, what, val)) + elif not isinstance(val, str): + raise DistutilsOptionError( + "'%s' must be a %s (got `%s`)" % (option, what, val) + ) return val def ensure_string_list(self, option): @@ -195,21 +190,27 @@ class Command(_Command): currently a string, we split it either on /,\s*/ or /\s+/, so "foo bar baz", "foo,bar,baz", and "foo, bar baz" all become ["foo", "bar", "baz"]. + + .. + TODO: This method seems to be similar to the one in ``distutils.cmd`` + Probably it is just here for backward compatibility with old Python versions? + + :meta private: """ val = getattr(self, option) if val is None: return - elif isinstance(val, string_types): + elif isinstance(val, str): setattr(self, option, re.split(r',\s*|\s+', val)) else: if isinstance(val, list): - ok = all(isinstance(v, string_types) for v in val) + ok = all(isinstance(v, str) for v in val) else: ok = False if not ok: raise DistutilsOptionError( - "'%s' must be a list of strings (got %r)" - % (option, val)) + "'%s' must be a list of strings (got %r)" % (option, val) + ) def reinitialize_command(self, command, reinit_subcommands=0, **kw): cmd = _Command.reinitialize_command(self, command, reinit_subcommands) @@ -241,5 +242,25 @@ def findall(dir=os.curdir): return list(files) +@functools.wraps(_convert_path) +def convert_path(pathname): + SetuptoolsDeprecationWarning.emit( + "Access to implementation detail", + """ + The function `convert_path` is not provided by setuptools itself, + and therefore not part of the public API. + + Its direct usage by 3rd-party packages is considered improper and the function + may be removed in the future. + """, + due_date=(2023, 12, 13), # initial deprecation 2022-03-25, see #3201 + ) + return _convert_path(pathname) + + +class sic(str): + """Treat this string as-is (https://en.wikipedia.org/wiki/Sic)""" + + # Apply monkey patches monkey.patch_all() diff --git a/libs/common/setuptools/_deprecation_warning.py b/libs/common/setuptools/_deprecation_warning.py deleted file mode 100644 index 086b64dd..00000000 --- a/libs/common/setuptools/_deprecation_warning.py +++ /dev/null @@ -1,7 +0,0 @@ -class SetuptoolsDeprecationWarning(Warning): - """ - Base class for warning deprecations in ``setuptools`` - - This class is not derived from ``DeprecationWarning``, and as such is - visible by default. - """ diff --git a/libs/common/setuptools/_imp.py b/libs/common/setuptools/_imp.py index a3cce9b2..9d4ead0e 100644 --- a/libs/common/setuptools/_imp.py +++ b/libs/common/setuptools/_imp.py @@ -7,7 +7,7 @@ import os import importlib.util import importlib.machinery -from .py34compat import module_from_spec +from importlib.util import module_from_spec PY_SOURCE = 1 @@ -17,9 +17,18 @@ C_BUILTIN = 6 PY_FROZEN = 7 +def find_spec(module, paths): + finder = ( + importlib.machinery.PathFinder().find_spec + if isinstance(paths, list) + else importlib.util.find_spec + ) + return finder(module, paths) + + def find_module(module, paths=None): """Just like 'imp.find_module()', but with package support""" - spec = importlib.util.find_spec(module, paths) + spec = find_spec(module, paths) if spec is None: raise ImportError("Can't find %s" % module) if not spec.has_location and hasattr(spec, 'submodule_search_locations'): @@ -28,16 +37,22 @@ def find_module(module, paths=None): kind = -1 file = None static = isinstance(spec.loader, type) - if spec.origin == 'frozen' or static and issubclass( - spec.loader, importlib.machinery.FrozenImporter): + if ( + spec.origin == 'frozen' + or static + and issubclass(spec.loader, importlib.machinery.FrozenImporter) + ): kind = PY_FROZEN path = None # imp compabilty - suffix = mode = '' # imp compability - elif spec.origin == 'built-in' or static and issubclass( - spec.loader, importlib.machinery.BuiltinImporter): + suffix = mode = '' # imp compatibility + elif ( + spec.origin == 'built-in' + or static + and issubclass(spec.loader, importlib.machinery.BuiltinImporter) + ): kind = C_BUILTIN path = None # imp compabilty - suffix = mode = '' # imp compability + suffix = mode = '' # imp compatibility elif spec.has_location: path = spec.origin suffix = os.path.splitext(path)[1] @@ -60,14 +75,14 @@ def find_module(module, paths=None): def get_frozen_object(module, paths=None): - spec = importlib.util.find_spec(module, paths) + spec = find_spec(module, paths) if not spec: raise ImportError("Can't find %s" % module) return spec.loader.get_code(module) def get_module(module, paths, info): - spec = importlib.util.find_spec(module, paths) + spec = find_spec(module, paths) if not spec: raise ImportError("Can't find %s" % module) return module_from_spec(spec) diff --git a/libs/common/setuptools/_vendor/packaging/__about__.py b/libs/common/setuptools/_vendor/packaging/__about__.py deleted file mode 100644 index dc95138d..00000000 --- a/libs/common/setuptools/_vendor/packaging/__about__.py +++ /dev/null @@ -1,27 +0,0 @@ -# This file is dual licensed under the terms of the Apache License, Version -# 2.0, and the BSD License. See the LICENSE file in the root of this repository -# for complete details. -from __future__ import absolute_import, division, print_function - -__all__ = [ - "__title__", - "__summary__", - "__uri__", - "__version__", - "__author__", - "__email__", - "__license__", - "__copyright__", -] - -__title__ = "packaging" -__summary__ = "Core utilities for Python packages" -__uri__ = "https://github.com/pypa/packaging" - -__version__ = "19.2" - -__author__ = "Donald Stufft and individual contributors" -__email__ = "donald@stufft.io" - -__license__ = "BSD or Apache License, Version 2.0" -__copyright__ = "Copyright 2014-2019 %s" % __author__ diff --git a/libs/common/setuptools/_vendor/packaging/__init__.py b/libs/common/setuptools/_vendor/packaging/__init__.py index a0cf67df..13cadc7f 100644 --- a/libs/common/setuptools/_vendor/packaging/__init__.py +++ b/libs/common/setuptools/_vendor/packaging/__init__.py @@ -1,26 +1,15 @@ # This file is dual licensed under the terms of the Apache License, Version # 2.0, and the BSD License. See the LICENSE file in the root of this repository # for complete details. -from __future__ import absolute_import, division, print_function -from .__about__ import ( - __author__, - __copyright__, - __email__, - __license__, - __summary__, - __title__, - __uri__, - __version__, -) +__title__ = "packaging" +__summary__ = "Core utilities for Python packages" +__uri__ = "https://github.com/pypa/packaging" -__all__ = [ - "__title__", - "__summary__", - "__uri__", - "__version__", - "__author__", - "__email__", - "__license__", - "__copyright__", -] +__version__ = "23.1" + +__author__ = "Donald Stufft and individual contributors" +__email__ = "donald@stufft.io" + +__license__ = "BSD-2-Clause or Apache-2.0" +__copyright__ = "2014-2019 %s" % __author__ diff --git a/libs/common/setuptools/_vendor/packaging/_compat.py b/libs/common/setuptools/_vendor/packaging/_compat.py deleted file mode 100644 index 25da473c..00000000 --- a/libs/common/setuptools/_vendor/packaging/_compat.py +++ /dev/null @@ -1,31 +0,0 @@ -# This file is dual licensed under the terms of the Apache License, Version -# 2.0, and the BSD License. See the LICENSE file in the root of this repository -# for complete details. -from __future__ import absolute_import, division, print_function - -import sys - - -PY2 = sys.version_info[0] == 2 -PY3 = sys.version_info[0] == 3 - -# flake8: noqa - -if PY3: - string_types = (str,) -else: - string_types = (basestring,) - - -def with_metaclass(meta, *bases): - """ - Create a base class with a metaclass. - """ - # This requires a bit of explanation: the basic idea is to make a dummy - # metaclass for one level of class instantiation that replaces itself with - # the actual metaclass. - class metaclass(meta): - def __new__(cls, name, this_bases, d): - return meta(name, bases, d) - - return type.__new__(metaclass, "temporary_class", (), {}) diff --git a/libs/common/setuptools/_vendor/packaging/_structures.py b/libs/common/setuptools/_vendor/packaging/_structures.py index 68dcca63..90a6465f 100644 --- a/libs/common/setuptools/_vendor/packaging/_structures.py +++ b/libs/common/setuptools/_vendor/packaging/_structures.py @@ -1,68 +1,61 @@ # This file is dual licensed under the terms of the Apache License, Version # 2.0, and the BSD License. See the LICENSE file in the root of this repository # for complete details. -from __future__ import absolute_import, division, print_function -class Infinity(object): - def __repr__(self): +class InfinityType: + def __repr__(self) -> str: return "Infinity" - def __hash__(self): + def __hash__(self) -> int: return hash(repr(self)) - def __lt__(self, other): + def __lt__(self, other: object) -> bool: return False - def __le__(self, other): + def __le__(self, other: object) -> bool: return False - def __eq__(self, other): + def __eq__(self, other: object) -> bool: return isinstance(other, self.__class__) - def __ne__(self, other): - return not isinstance(other, self.__class__) - - def __gt__(self, other): + def __gt__(self, other: object) -> bool: return True - def __ge__(self, other): + def __ge__(self, other: object) -> bool: return True - def __neg__(self): + def __neg__(self: object) -> "NegativeInfinityType": return NegativeInfinity -Infinity = Infinity() +Infinity = InfinityType() -class NegativeInfinity(object): - def __repr__(self): +class NegativeInfinityType: + def __repr__(self) -> str: return "-Infinity" - def __hash__(self): + def __hash__(self) -> int: return hash(repr(self)) - def __lt__(self, other): + def __lt__(self, other: object) -> bool: return True - def __le__(self, other): + def __le__(self, other: object) -> bool: return True - def __eq__(self, other): + def __eq__(self, other: object) -> bool: return isinstance(other, self.__class__) - def __ne__(self, other): - return not isinstance(other, self.__class__) - - def __gt__(self, other): + def __gt__(self, other: object) -> bool: return False - def __ge__(self, other): + def __ge__(self, other: object) -> bool: return False - def __neg__(self): + def __neg__(self: object) -> InfinityType: return Infinity -NegativeInfinity = NegativeInfinity() +NegativeInfinity = NegativeInfinityType() diff --git a/libs/common/setuptools/_vendor/packaging/markers.py b/libs/common/setuptools/_vendor/packaging/markers.py index 4bdfdb24..8b98fca7 100644 --- a/libs/common/setuptools/_vendor/packaging/markers.py +++ b/libs/common/setuptools/_vendor/packaging/markers.py @@ -1,20 +1,24 @@ # This file is dual licensed under the terms of the Apache License, Version # 2.0, and the BSD License. See the LICENSE file in the root of this repository # for complete details. -from __future__ import absolute_import, division, print_function import operator import os import platform import sys +from typing import Any, Callable, Dict, List, Optional, Tuple, Union -from setuptools.extern.pyparsing import ParseException, ParseResults, stringStart, stringEnd -from setuptools.extern.pyparsing import ZeroOrMore, Group, Forward, QuotedString -from setuptools.extern.pyparsing import Literal as L # noqa - -from ._compat import string_types -from .specifiers import Specifier, InvalidSpecifier - +from ._parser import ( + MarkerAtom, + MarkerList, + Op, + Value, + Variable, + parse_marker as _parse_marker, +) +from ._tokenizer import ParserSyntaxError +from .specifiers import InvalidSpecifier, Specifier +from .utils import canonicalize_name __all__ = [ "InvalidMarker", @@ -24,6 +28,8 @@ __all__ = [ "default_environment", ] +Operator = Callable[[str, str], bool] + class InvalidMarker(ValueError): """ @@ -44,101 +50,27 @@ class UndefinedEnvironmentName(ValueError): """ -class Node(object): - def __init__(self, value): - self.value = value - - def __str__(self): - return str(self.value) - - def __repr__(self): - return "<{0}({1!r})>".format(self.__class__.__name__, str(self)) - - def serialize(self): - raise NotImplementedError +def _normalize_extra_values(results: Any) -> Any: + """ + Normalize extra values. + """ + if isinstance(results[0], tuple): + lhs, op, rhs = results[0] + if isinstance(lhs, Variable) and lhs.value == "extra": + normalized_extra = canonicalize_name(rhs.value) + rhs = Value(normalized_extra) + elif isinstance(rhs, Variable) and rhs.value == "extra": + normalized_extra = canonicalize_name(lhs.value) + lhs = Value(normalized_extra) + results[0] = lhs, op, rhs + return results -class Variable(Node): - def serialize(self): - return str(self) +def _format_marker( + marker: Union[List[str], MarkerAtom, str], first: Optional[bool] = True +) -> str: - -class Value(Node): - def serialize(self): - return '"{0}"'.format(self) - - -class Op(Node): - def serialize(self): - return str(self) - - -VARIABLE = ( - L("implementation_version") - | L("platform_python_implementation") - | L("implementation_name") - | L("python_full_version") - | L("platform_release") - | L("platform_version") - | L("platform_machine") - | L("platform_system") - | L("python_version") - | L("sys_platform") - | L("os_name") - | L("os.name") - | L("sys.platform") # PEP-345 - | L("platform.version") # PEP-345 - | L("platform.machine") # PEP-345 - | L("platform.python_implementation") # PEP-345 - | L("python_implementation") # PEP-345 - | L("extra") # undocumented setuptools legacy -) -ALIASES = { - "os.name": "os_name", - "sys.platform": "sys_platform", - "platform.version": "platform_version", - "platform.machine": "platform_machine", - "platform.python_implementation": "platform_python_implementation", - "python_implementation": "platform_python_implementation", -} -VARIABLE.setParseAction(lambda s, l, t: Variable(ALIASES.get(t[0], t[0]))) - -VERSION_CMP = ( - L("===") | L("==") | L(">=") | L("<=") | L("!=") | L("~=") | L(">") | L("<") -) - -MARKER_OP = VERSION_CMP | L("not in") | L("in") -MARKER_OP.setParseAction(lambda s, l, t: Op(t[0])) - -MARKER_VALUE = QuotedString("'") | QuotedString('"') -MARKER_VALUE.setParseAction(lambda s, l, t: Value(t[0])) - -BOOLOP = L("and") | L("or") - -MARKER_VAR = VARIABLE | MARKER_VALUE - -MARKER_ITEM = Group(MARKER_VAR + MARKER_OP + MARKER_VAR) -MARKER_ITEM.setParseAction(lambda s, l, t: tuple(t[0])) - -LPAREN = L("(").suppress() -RPAREN = L(")").suppress() - -MARKER_EXPR = Forward() -MARKER_ATOM = MARKER_ITEM | Group(LPAREN + MARKER_EXPR + RPAREN) -MARKER_EXPR << MARKER_ATOM + ZeroOrMore(BOOLOP + MARKER_EXPR) - -MARKER = stringStart + MARKER_EXPR + stringEnd - - -def _coerce_parse_result(results): - if isinstance(results, ParseResults): - return [_coerce_parse_result(i) for i in results] - else: - return results - - -def _format_marker(marker, first=True): - assert isinstance(marker, (list, tuple, string_types)) + assert isinstance(marker, (list, tuple, str)) # Sometimes we have a structure like [[...]] which is a single item list # where the single item is itself it's own list. In that case we want skip @@ -163,7 +95,7 @@ def _format_marker(marker, first=True): return marker -_operators = { +_operators: Dict[str, Operator] = { "in": lambda lhs, rhs: lhs in rhs, "not in": lambda lhs, rhs: lhs not in rhs, "<": operator.lt, @@ -175,42 +107,38 @@ _operators = { } -def _eval_op(lhs, op, rhs): +def _eval_op(lhs: str, op: Op, rhs: str) -> bool: try: spec = Specifier("".join([op.serialize(), rhs])) except InvalidSpecifier: pass else: - return spec.contains(lhs) + return spec.contains(lhs, prereleases=True) - oper = _operators.get(op.serialize()) + oper: Optional[Operator] = _operators.get(op.serialize()) if oper is None: - raise UndefinedComparison( - "Undefined {0!r} on {1!r} and {2!r}.".format(op, lhs, rhs) - ) + raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.") return oper(lhs, rhs) -_undefined = object() +def _normalize(*values: str, key: str) -> Tuple[str, ...]: + # PEP 685 – Comparison of extra names for optional distribution dependencies + # https://peps.python.org/pep-0685/ + # > When comparing extra names, tools MUST normalize the names being + # > compared using the semantics outlined in PEP 503 for names + if key == "extra": + return tuple(canonicalize_name(v) for v in values) + + # other environment markers don't have such standards + return values -def _get_env(environment, name): - value = environment.get(name, _undefined) - - if value is _undefined: - raise UndefinedEnvironmentName( - "{0!r} does not exist in evaluation environment.".format(name) - ) - - return value - - -def _evaluate_markers(markers, environment): - groups = [[]] +def _evaluate_markers(markers: MarkerList, environment: Dict[str, str]) -> bool: + groups: List[List[bool]] = [[]] for marker in markers: - assert isinstance(marker, (list, tuple, string_types)) + assert isinstance(marker, (list, tuple, str)) if isinstance(marker, list): groups[-1].append(_evaluate_markers(marker, environment)) @@ -218,12 +146,15 @@ def _evaluate_markers(markers, environment): lhs, op, rhs = marker if isinstance(lhs, Variable): - lhs_value = _get_env(environment, lhs.value) + environment_key = lhs.value + lhs_value = environment[environment_key] rhs_value = rhs.value else: lhs_value = lhs.value - rhs_value = _get_env(environment, rhs.value) + environment_key = rhs.value + rhs_value = environment[environment_key] + lhs_value, rhs_value = _normalize(lhs_value, rhs_value, key=environment_key) groups[-1].append(_eval_op(lhs_value, op, rhs_value)) else: assert marker in ["and", "or"] @@ -233,7 +164,7 @@ def _evaluate_markers(markers, environment): return any(all(item) for item in groups) -def format_full_version(info): +def format_full_version(info: "sys._version_info") -> str: version = "{0.major}.{0.minor}.{0.micro}".format(info) kind = info.releaselevel if kind != "final": @@ -241,14 +172,9 @@ def format_full_version(info): return version -def default_environment(): - if hasattr(sys, "implementation"): - iver = format_full_version(sys.implementation.version) - implementation_name = sys.implementation.name - else: - iver = "0" - implementation_name = "" - +def default_environment() -> Dict[str, str]: + iver = format_full_version(sys.implementation.version) + implementation_name = sys.implementation.name return { "implementation_name": implementation_name, "implementation_version": iver, @@ -264,23 +190,48 @@ def default_environment(): } -class Marker(object): - def __init__(self, marker): +class Marker: + def __init__(self, marker: str) -> None: + # Note: We create a Marker object without calling this constructor in + # packaging.requirements.Requirement. If any additional logic is + # added here, make sure to mirror/adapt Requirement. try: - self._markers = _coerce_parse_result(MARKER.parseString(marker)) - except ParseException as e: - err_str = "Invalid marker: {0!r}, parse error at {1!r}".format( - marker, marker[e.loc : e.loc + 8] - ) - raise InvalidMarker(err_str) + self._markers = _normalize_extra_values(_parse_marker(marker)) + # The attribute `_markers` can be described in terms of a recursive type: + # MarkerList = List[Union[Tuple[Node, ...], str, MarkerList]] + # + # For example, the following expression: + # python_version > "3.6" or (python_version == "3.6" and os_name == "unix") + # + # is parsed into: + # [ + # (, ')>, ), + # 'and', + # [ + # (, , ), + # 'or', + # (, , ) + # ] + # ] + except ParserSyntaxError as e: + raise InvalidMarker(str(e)) from e - def __str__(self): + def __str__(self) -> str: return _format_marker(self._markers) - def __repr__(self): - return "".format(str(self)) + def __repr__(self) -> str: + return f"" - def evaluate(self, environment=None): + def __hash__(self) -> int: + return hash((self.__class__.__name__, str(self))) + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, Marker): + return NotImplemented + + return str(self) == str(other) + + def evaluate(self, environment: Optional[Dict[str, str]] = None) -> bool: """Evaluate a marker. Return the boolean from evaluating the given marker against the @@ -290,7 +241,12 @@ class Marker(object): The environment is determined from the current Python process. """ current_environment = default_environment() + current_environment["extra"] = "" if environment is not None: current_environment.update(environment) + # The API used to allow setting extra to None. We need to handle this + # case for backwards compatibility. + if current_environment["extra"] is None: + current_environment["extra"] = "" return _evaluate_markers(self._markers, current_environment) diff --git a/libs/common/setuptools/_vendor/packaging/requirements.py b/libs/common/setuptools/_vendor/packaging/requirements.py index 8a0c2cb9..f34bfa85 100644 --- a/libs/common/setuptools/_vendor/packaging/requirements.py +++ b/libs/common/setuptools/_vendor/packaging/requirements.py @@ -1,18 +1,14 @@ # This file is dual licensed under the terms of the Apache License, Version # 2.0, and the BSD License. See the LICENSE file in the root of this repository # for complete details. -from __future__ import absolute_import, division, print_function -import string -import re +import urllib.parse +from typing import Any, List, Optional, Set -from setuptools.extern.pyparsing import stringStart, stringEnd, originalTextFor, ParseException -from setuptools.extern.pyparsing import ZeroOrMore, Word, Optional, Regex, Combine -from setuptools.extern.pyparsing import Literal as L # noqa -from setuptools.extern.six.moves.urllib import parse as urlparse - -from .markers import MARKER_EXPR, Marker -from .specifiers import LegacySpecifier, Specifier, SpecifierSet +from ._parser import parse_requirement as _parse_requirement +from ._tokenizer import ParserSyntaxError +from .markers import Marker, _normalize_extra_values +from .specifiers import SpecifierSet class InvalidRequirement(ValueError): @@ -21,61 +17,7 @@ class InvalidRequirement(ValueError): """ -ALPHANUM = Word(string.ascii_letters + string.digits) - -LBRACKET = L("[").suppress() -RBRACKET = L("]").suppress() -LPAREN = L("(").suppress() -RPAREN = L(")").suppress() -COMMA = L(",").suppress() -SEMICOLON = L(";").suppress() -AT = L("@").suppress() - -PUNCTUATION = Word("-_.") -IDENTIFIER_END = ALPHANUM | (ZeroOrMore(PUNCTUATION) + ALPHANUM) -IDENTIFIER = Combine(ALPHANUM + ZeroOrMore(IDENTIFIER_END)) - -NAME = IDENTIFIER("name") -EXTRA = IDENTIFIER - -URI = Regex(r"[^ ]+")("url") -URL = AT + URI - -EXTRAS_LIST = EXTRA + ZeroOrMore(COMMA + EXTRA) -EXTRAS = (LBRACKET + Optional(EXTRAS_LIST) + RBRACKET)("extras") - -VERSION_PEP440 = Regex(Specifier._regex_str, re.VERBOSE | re.IGNORECASE) -VERSION_LEGACY = Regex(LegacySpecifier._regex_str, re.VERBOSE | re.IGNORECASE) - -VERSION_ONE = VERSION_PEP440 ^ VERSION_LEGACY -VERSION_MANY = Combine( - VERSION_ONE + ZeroOrMore(COMMA + VERSION_ONE), joinString=",", adjacent=False -)("_raw_spec") -_VERSION_SPEC = Optional(((LPAREN + VERSION_MANY + RPAREN) | VERSION_MANY)) -_VERSION_SPEC.setParseAction(lambda s, l, t: t._raw_spec or "") - -VERSION_SPEC = originalTextFor(_VERSION_SPEC)("specifier") -VERSION_SPEC.setParseAction(lambda s, l, t: t[1]) - -MARKER_EXPR = originalTextFor(MARKER_EXPR())("marker") -MARKER_EXPR.setParseAction( - lambda s, l, t: Marker(s[t._original_start : t._original_end]) -) -MARKER_SEPARATOR = SEMICOLON -MARKER = MARKER_SEPARATOR + MARKER_EXPR - -VERSION_AND_MARKER = VERSION_SPEC + Optional(MARKER) -URL_AND_MARKER = URL + Optional(MARKER) - -NAMED_REQUIREMENT = NAME + Optional(EXTRAS) + (URL_AND_MARKER | VERSION_AND_MARKER) - -REQUIREMENT = stringStart + NAMED_REQUIREMENT + stringEnd -# setuptools.extern.pyparsing isn't thread safe during initialization, so we do it eagerly, see -# issue #104 -REQUIREMENT.parseString("x[]") - - -class Requirement(object): +class Requirement: """Parse a requirement. Parse a given requirement string into its parts, such as name, specifier, @@ -88,51 +30,66 @@ class Requirement(object): # the thing as well as the version? What about the markers? # TODO: Can we normalize the name and extra name? - def __init__(self, requirement_string): + def __init__(self, requirement_string: str) -> None: try: - req = REQUIREMENT.parseString(requirement_string) - except ParseException as e: - raise InvalidRequirement( - 'Parse error at "{0!r}": {1}'.format( - requirement_string[e.loc : e.loc + 8], e.msg - ) - ) + parsed = _parse_requirement(requirement_string) + except ParserSyntaxError as e: + raise InvalidRequirement(str(e)) from e - self.name = req.name - if req.url: - parsed_url = urlparse.urlparse(req.url) + self.name: str = parsed.name + if parsed.url: + parsed_url = urllib.parse.urlparse(parsed.url) if parsed_url.scheme == "file": - if urlparse.urlunparse(parsed_url) != req.url: + if urllib.parse.urlunparse(parsed_url) != parsed.url: raise InvalidRequirement("Invalid URL given") elif not (parsed_url.scheme and parsed_url.netloc) or ( not parsed_url.scheme and not parsed_url.netloc ): - raise InvalidRequirement("Invalid URL: {0}".format(req.url)) - self.url = req.url + raise InvalidRequirement(f"Invalid URL: {parsed.url}") + self.url: Optional[str] = parsed.url else: self.url = None - self.extras = set(req.extras.asList() if req.extras else []) - self.specifier = SpecifierSet(req.specifier) - self.marker = req.marker if req.marker else None + self.extras: Set[str] = set(parsed.extras if parsed.extras else []) + self.specifier: SpecifierSet = SpecifierSet(parsed.specifier) + self.marker: Optional[Marker] = None + if parsed.marker is not None: + self.marker = Marker.__new__(Marker) + self.marker._markers = _normalize_extra_values(parsed.marker) - def __str__(self): - parts = [self.name] + def __str__(self) -> str: + parts: List[str] = [self.name] if self.extras: - parts.append("[{0}]".format(",".join(sorted(self.extras)))) + formatted_extras = ",".join(sorted(self.extras)) + parts.append(f"[{formatted_extras}]") if self.specifier: parts.append(str(self.specifier)) if self.url: - parts.append("@ {0}".format(self.url)) + parts.append(f"@ {self.url}") if self.marker: parts.append(" ") if self.marker: - parts.append("; {0}".format(self.marker)) + parts.append(f"; {self.marker}") return "".join(parts) - def __repr__(self): - return "".format(str(self)) + def __repr__(self) -> str: + return f"" + + def __hash__(self) -> int: + return hash((self.__class__.__name__, str(self))) + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, Requirement): + return NotImplemented + + return ( + self.name == other.name + and self.extras == other.extras + and self.specifier == other.specifier + and self.url == other.url + and self.marker == other.marker + ) diff --git a/libs/common/setuptools/_vendor/packaging/specifiers.py b/libs/common/setuptools/_vendor/packaging/specifiers.py index 743576a0..ba8fe37b 100644 --- a/libs/common/setuptools/_vendor/packaging/specifiers.py +++ b/libs/common/setuptools/_vendor/packaging/specifiers.py @@ -1,272 +1,123 @@ # This file is dual licensed under the terms of the Apache License, Version # 2.0, and the BSD License. See the LICENSE file in the root of this repository # for complete details. -from __future__ import absolute_import, division, print_function +""" +.. testsetup:: + + from packaging.specifiers import Specifier, SpecifierSet, InvalidSpecifier + from packaging.version import Version +""" import abc -import functools import itertools import re +from typing import ( + Callable, + Iterable, + Iterator, + List, + Optional, + Set, + Tuple, + TypeVar, + Union, +) -from ._compat import string_types, with_metaclass -from .version import Version, LegacyVersion, parse +from .utils import canonicalize_version +from .version import Version + +UnparsedVersion = Union[Version, str] +UnparsedVersionVar = TypeVar("UnparsedVersionVar", bound=UnparsedVersion) +CallableOperator = Callable[[Version, str], bool] + + +def _coerce_version(version: UnparsedVersion) -> Version: + if not isinstance(version, Version): + version = Version(version) + return version class InvalidSpecifier(ValueError): """ - An invalid specifier was found, users should refer to PEP 440. + Raised when attempting to create a :class:`Specifier` with a specifier + string that is invalid. + + >>> Specifier("lolwat") + Traceback (most recent call last): + ... + packaging.specifiers.InvalidSpecifier: Invalid specifier: 'lolwat' """ -class BaseSpecifier(with_metaclass(abc.ABCMeta, object)): +class BaseSpecifier(metaclass=abc.ABCMeta): @abc.abstractmethod - def __str__(self): + def __str__(self) -> str: """ - Returns the str representation of this Specifier like object. This + Returns the str representation of this Specifier-like object. This should be representative of the Specifier itself. """ @abc.abstractmethod - def __hash__(self): + def __hash__(self) -> int: """ - Returns a hash value for this Specifier like object. + Returns a hash value for this Specifier-like object. """ @abc.abstractmethod - def __eq__(self, other): + def __eq__(self, other: object) -> bool: """ - Returns a boolean representing whether or not the two Specifier like + Returns a boolean representing whether or not the two Specifier-like objects are equal. + + :param other: The other object to check against. """ + @property @abc.abstractmethod - def __ne__(self, other): - """ - Returns a boolean representing whether or not the two Specifier like - objects are not equal. - """ + def prereleases(self) -> Optional[bool]: + """Whether or not pre-releases as a whole are allowed. - @abc.abstractproperty - def prereleases(self): - """ - Returns whether or not pre-releases as a whole are allowed by this - specifier. + This can be set to either ``True`` or ``False`` to explicitly enable or disable + prereleases or it can be set to ``None`` (the default) to use default semantics. """ @prereleases.setter - def prereleases(self, value): - """ - Sets whether or not pre-releases as a whole are allowed by this - specifier. + def prereleases(self, value: bool) -> None: + """Setter for :attr:`prereleases`. + + :param value: The value to set. """ @abc.abstractmethod - def contains(self, item, prereleases=None): + def contains(self, item: str, prereleases: Optional[bool] = None) -> bool: """ Determines if the given item is contained within this specifier. """ @abc.abstractmethod - def filter(self, iterable, prereleases=None): + def filter( + self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None + ) -> Iterator[UnparsedVersionVar]: """ Takes an iterable of items and filters them so that only items which are contained within this specifier are allowed in it. """ -class _IndividualSpecifier(BaseSpecifier): +class Specifier(BaseSpecifier): + """This class abstracts handling of version specifiers. - _operators = {} + .. tip:: - def __init__(self, spec="", prereleases=None): - match = self._regex.search(spec) - if not match: - raise InvalidSpecifier("Invalid specifier: '{0}'".format(spec)) + It is generally not required to instantiate this manually. You should instead + prefer to work with :class:`SpecifierSet` instead, which can parse + comma-separated version specifiers (which is what package metadata contains). + """ - self._spec = (match.group("operator").strip(), match.group("version").strip()) - - # Store whether or not this Specifier should accept prereleases - self._prereleases = prereleases - - def __repr__(self): - pre = ( - ", prereleases={0!r}".format(self.prereleases) - if self._prereleases is not None - else "" - ) - - return "<{0}({1!r}{2})>".format(self.__class__.__name__, str(self), pre) - - def __str__(self): - return "{0}{1}".format(*self._spec) - - def __hash__(self): - return hash(self._spec) - - def __eq__(self, other): - if isinstance(other, string_types): - try: - other = self.__class__(other) - except InvalidSpecifier: - return NotImplemented - elif not isinstance(other, self.__class__): - return NotImplemented - - return self._spec == other._spec - - def __ne__(self, other): - if isinstance(other, string_types): - try: - other = self.__class__(other) - except InvalidSpecifier: - return NotImplemented - elif not isinstance(other, self.__class__): - return NotImplemented - - return self._spec != other._spec - - def _get_operator(self, op): - return getattr(self, "_compare_{0}".format(self._operators[op])) - - def _coerce_version(self, version): - if not isinstance(version, (LegacyVersion, Version)): - version = parse(version) - return version - - @property - def operator(self): - return self._spec[0] - - @property - def version(self): - return self._spec[1] - - @property - def prereleases(self): - return self._prereleases - - @prereleases.setter - def prereleases(self, value): - self._prereleases = value - - def __contains__(self, item): - return self.contains(item) - - def contains(self, item, prereleases=None): - # Determine if prereleases are to be allowed or not. - if prereleases is None: - prereleases = self.prereleases - - # Normalize item to a Version or LegacyVersion, this allows us to have - # a shortcut for ``"2.0" in Specifier(">=2") - item = self._coerce_version(item) - - # Determine if we should be supporting prereleases in this specifier - # or not, if we do not support prereleases than we can short circuit - # logic if this version is a prereleases. - if item.is_prerelease and not prereleases: - return False - - # Actually do the comparison to determine if this item is contained - # within this Specifier or not. - return self._get_operator(self.operator)(item, self.version) - - def filter(self, iterable, prereleases=None): - yielded = False - found_prereleases = [] - - kw = {"prereleases": prereleases if prereleases is not None else True} - - # Attempt to iterate over all the values in the iterable and if any of - # them match, yield them. - for version in iterable: - parsed_version = self._coerce_version(version) - - if self.contains(parsed_version, **kw): - # If our version is a prerelease, and we were not set to allow - # prereleases, then we'll store it for later incase nothing - # else matches this specifier. - if parsed_version.is_prerelease and not ( - prereleases or self.prereleases - ): - found_prereleases.append(version) - # Either this is not a prerelease, or we should have been - # accepting prereleases from the beginning. - else: - yielded = True - yield version - - # Now that we've iterated over everything, determine if we've yielded - # any values, and if we have not and we have any prereleases stored up - # then we will go ahead and yield the prereleases. - if not yielded and found_prereleases: - for version in found_prereleases: - yield version - - -class LegacySpecifier(_IndividualSpecifier): - - _regex_str = r""" - (?P(==|!=|<=|>=|<|>)) - \s* - (?P - [^,;\s)]* # Since this is a "legacy" specifier, and the version - # string can be just about anything, we match everything - # except for whitespace, a semi-colon for marker support, - # a closing paren since versions can be enclosed in - # them, and a comma since it's a version separator. - ) - """ - - _regex = re.compile(r"^\s*" + _regex_str + r"\s*$", re.VERBOSE | re.IGNORECASE) - - _operators = { - "==": "equal", - "!=": "not_equal", - "<=": "less_than_equal", - ">=": "greater_than_equal", - "<": "less_than", - ">": "greater_than", - } - - def _coerce_version(self, version): - if not isinstance(version, LegacyVersion): - version = LegacyVersion(str(version)) - return version - - def _compare_equal(self, prospective, spec): - return prospective == self._coerce_version(spec) - - def _compare_not_equal(self, prospective, spec): - return prospective != self._coerce_version(spec) - - def _compare_less_than_equal(self, prospective, spec): - return prospective <= self._coerce_version(spec) - - def _compare_greater_than_equal(self, prospective, spec): - return prospective >= self._coerce_version(spec) - - def _compare_less_than(self, prospective, spec): - return prospective < self._coerce_version(spec) - - def _compare_greater_than(self, prospective, spec): - return prospective > self._coerce_version(spec) - - -def _require_version_compare(fn): - @functools.wraps(fn) - def wrapped(self, prospective, spec): - if not isinstance(prospective, Version): - return False - return fn(self, prospective, spec) - - return wrapped - - -class Specifier(_IndividualSpecifier): - - _regex_str = r""" + _operator_regex_str = r""" (?P(~=|==|!=|<=|>=|<|>|===)) + """ + _version_regex_str = r""" (?P (?: # The identity operators allow for an escape hatch that will @@ -276,8 +127,10 @@ class Specifier(_IndividualSpecifier): # but included entirely as an escape hatch. (?<====) # Only match for the identity operator \s* - [^\s]* # We just match everything, except for whitespace - # since we are only testing for strict identity. + [^\s;)]* # The arbitrary version can be just about anything, + # we match everything except for whitespace, a + # semi-colon for marker support, and a closing paren + # since versions can be enclosed in them. ) | (?: @@ -290,23 +143,23 @@ class Specifier(_IndividualSpecifier): v? (?:[0-9]+!)? # epoch [0-9]+(?:\.[0-9]+)* # release - (?: # pre release - [-_\.]? - (a|b|c|rc|alpha|beta|pre|preview) - [-_\.]? - [0-9]* - )? - (?: # post release - (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*) - )? - # You cannot use a wild card and a dev or local version - # together so group them with a | and make them optional. + # You cannot use a wild card and a pre-release, post-release, a dev or + # local version together so group them with a | and make them optional. (?: + \.\* # Wild card syntax of .* + | + (?: # pre release + [-_\.]? + (alpha|beta|preview|pre|a|b|c|rc) + [-_\.]? + [0-9]* + )? + (?: # post release + (?:-[0-9]+)|(?:[-_\.]?(post|rev|r)[-_\.]?[0-9]*) + )? (?:[-_\.]?dev[-_\.]?[0-9]*)? # dev release (?:\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*)? # local - | - \.\* # Wild card syntax of .* )? ) | @@ -321,7 +174,7 @@ class Specifier(_IndividualSpecifier): [0-9]+(?:\.[0-9]+)+ # release (We have a + instead of a *) (?: # pre release [-_\.]? - (a|b|c|rc|alpha|beta|pre|preview) + (alpha|beta|preview|pre|a|b|c|rc) [-_\.]? [0-9]* )? @@ -346,7 +199,7 @@ class Specifier(_IndividualSpecifier): [0-9]+(?:\.[0-9]+)* # release (?: # pre release [-_\.]? - (a|b|c|rc|alpha|beta|pre|preview) + (alpha|beta|preview|pre|a|b|c|rc) [-_\.]? [0-9]* )? @@ -358,7 +211,10 @@ class Specifier(_IndividualSpecifier): ) """ - _regex = re.compile(r"^\s*" + _regex_str + r"\s*$", re.VERBOSE | re.IGNORECASE) + _regex = re.compile( + r"^\s*" + _operator_regex_str + _version_regex_str + r"\s*$", + re.VERBOSE | re.IGNORECASE, + ) _operators = { "~=": "compatible", @@ -371,8 +227,154 @@ class Specifier(_IndividualSpecifier): "===": "arbitrary", } - @_require_version_compare - def _compare_compatible(self, prospective, spec): + def __init__(self, spec: str = "", prereleases: Optional[bool] = None) -> None: + """Initialize a Specifier instance. + + :param spec: + The string representation of a specifier which will be parsed and + normalized before use. + :param prereleases: + This tells the specifier if it should accept prerelease versions if + applicable or not. The default of ``None`` will autodetect it from the + given specifiers. + :raises InvalidSpecifier: + If the given specifier is invalid (i.e. bad syntax). + """ + match = self._regex.search(spec) + if not match: + raise InvalidSpecifier(f"Invalid specifier: '{spec}'") + + self._spec: Tuple[str, str] = ( + match.group("operator").strip(), + match.group("version").strip(), + ) + + # Store whether or not this Specifier should accept prereleases + self._prereleases = prereleases + + # https://github.com/python/mypy/pull/13475#pullrequestreview-1079784515 + @property # type: ignore[override] + def prereleases(self) -> bool: + # If there is an explicit prereleases set for this, then we'll just + # blindly use that. + if self._prereleases is not None: + return self._prereleases + + # Look at all of our specifiers and determine if they are inclusive + # operators, and if they are if they are including an explicit + # prerelease. + operator, version = self._spec + if operator in ["==", ">=", "<=", "~=", "==="]: + # The == specifier can include a trailing .*, if it does we + # want to remove before parsing. + if operator == "==" and version.endswith(".*"): + version = version[:-2] + + # Parse the version, and if it is a pre-release than this + # specifier allows pre-releases. + if Version(version).is_prerelease: + return True + + return False + + @prereleases.setter + def prereleases(self, value: bool) -> None: + self._prereleases = value + + @property + def operator(self) -> str: + """The operator of this specifier. + + >>> Specifier("==1.2.3").operator + '==' + """ + return self._spec[0] + + @property + def version(self) -> str: + """The version of this specifier. + + >>> Specifier("==1.2.3").version + '1.2.3' + """ + return self._spec[1] + + def __repr__(self) -> str: + """A representation of the Specifier that shows all internal state. + + >>> Specifier('>=1.0.0') + =1.0.0')> + >>> Specifier('>=1.0.0', prereleases=False) + =1.0.0', prereleases=False)> + >>> Specifier('>=1.0.0', prereleases=True) + =1.0.0', prereleases=True)> + """ + pre = ( + f", prereleases={self.prereleases!r}" + if self._prereleases is not None + else "" + ) + + return f"<{self.__class__.__name__}({str(self)!r}{pre})>" + + def __str__(self) -> str: + """A string representation of the Specifier that can be round-tripped. + + >>> str(Specifier('>=1.0.0')) + '>=1.0.0' + >>> str(Specifier('>=1.0.0', prereleases=False)) + '>=1.0.0' + """ + return "{}{}".format(*self._spec) + + @property + def _canonical_spec(self) -> Tuple[str, str]: + canonical_version = canonicalize_version( + self._spec[1], + strip_trailing_zero=(self._spec[0] != "~="), + ) + return self._spec[0], canonical_version + + def __hash__(self) -> int: + return hash(self._canonical_spec) + + def __eq__(self, other: object) -> bool: + """Whether or not the two Specifier-like objects are equal. + + :param other: The other object to check against. + + The value of :attr:`prereleases` is ignored. + + >>> Specifier("==1.2.3") == Specifier("== 1.2.3.0") + True + >>> (Specifier("==1.2.3", prereleases=False) == + ... Specifier("==1.2.3", prereleases=True)) + True + >>> Specifier("==1.2.3") == "==1.2.3" + True + >>> Specifier("==1.2.3") == Specifier("==1.2.4") + False + >>> Specifier("==1.2.3") == Specifier("~=1.2.3") + False + """ + if isinstance(other, str): + try: + other = self.__class__(str(other)) + except InvalidSpecifier: + return NotImplemented + elif not isinstance(other, self.__class__): + return NotImplemented + + return self._canonical_spec == other._canonical_spec + + def _get_operator(self, op: str) -> CallableOperator: + operator_callable: CallableOperator = getattr( + self, f"_compare_{self._operators[op]}" + ) + return operator_callable + + def _compare_compatible(self, prospective: Version, spec: str) -> bool: + # Compatible releases have an equivalent combination of >= and ==. That # is that ~=2.2 is equivalent to >=2.2,==2.*. This allows us to # implement this in terms of the other specifiers instead of @@ -380,15 +382,9 @@ class Specifier(_IndividualSpecifier): # the other specifiers. # We want everything but the last item in the version, but we want to - # ignore post and dev releases and we want to treat the pre-release as - # it's own separate segment. + # ignore suffix segments. prefix = ".".join( - list( - itertools.takewhile( - lambda x: (not x.startswith("post") and not x.startswith("dev")), - _version_split(spec), - ) - )[:-1] + list(itertools.takewhile(_is_not_suffix, _version_split(spec)))[:-1] ) # Add the prefix notation to the end of our string @@ -398,58 +394,69 @@ class Specifier(_IndividualSpecifier): prospective, prefix ) - @_require_version_compare - def _compare_equal(self, prospective, spec): + def _compare_equal(self, prospective: Version, spec: str) -> bool: + # We need special logic to handle prefix matching if spec.endswith(".*"): # In the case of prefix matching we want to ignore local segment. - prospective = Version(prospective.public) + normalized_prospective = canonicalize_version( + prospective.public, strip_trailing_zero=False + ) + # Get the normalized version string ignoring the trailing .* + normalized_spec = canonicalize_version(spec[:-2], strip_trailing_zero=False) # Split the spec out by dots, and pretend that there is an implicit # dot in between a release segment and a pre-release segment. - spec = _version_split(spec[:-2]) # Remove the trailing .* + split_spec = _version_split(normalized_spec) # Split the prospective version out by dots, and pretend that there # is an implicit dot in between a release segment and a pre-release # segment. - prospective = _version_split(str(prospective)) + split_prospective = _version_split(normalized_prospective) + + # 0-pad the prospective version before shortening it to get the correct + # shortened version. + padded_prospective, _ = _pad_version(split_prospective, split_spec) # Shorten the prospective version to be the same length as the spec # so that we can determine if the specifier is a prefix of the # prospective version or not. - prospective = prospective[: len(spec)] + shortened_prospective = padded_prospective[: len(split_spec)] - # Pad out our two sides with zeros so that they both equal the same - # length. - spec, prospective = _pad_version(spec, prospective) + return shortened_prospective == split_spec else: # Convert our spec string into a Version - spec = Version(spec) + spec_version = Version(spec) # If the specifier does not have a local segment, then we want to # act as if the prospective version also does not have a local # segment. - if not spec.local: + if not spec_version.local: prospective = Version(prospective.public) - return prospective == spec + return prospective == spec_version - @_require_version_compare - def _compare_not_equal(self, prospective, spec): + def _compare_not_equal(self, prospective: Version, spec: str) -> bool: return not self._compare_equal(prospective, spec) - @_require_version_compare - def _compare_less_than_equal(self, prospective, spec): - return prospective <= Version(spec) + def _compare_less_than_equal(self, prospective: Version, spec: str) -> bool: - @_require_version_compare - def _compare_greater_than_equal(self, prospective, spec): - return prospective >= Version(spec) + # NB: Local version identifiers are NOT permitted in the version + # specifier, so local version labels can be universally removed from + # the prospective version. + return Version(prospective.public) <= Version(spec) + + def _compare_greater_than_equal(self, prospective: Version, spec: str) -> bool: + + # NB: Local version identifiers are NOT permitted in the version + # specifier, so local version labels can be universally removed from + # the prospective version. + return Version(prospective.public) >= Version(spec) + + def _compare_less_than(self, prospective: Version, spec_str: str) -> bool: - @_require_version_compare - def _compare_less_than(self, prospective, spec): # Convert our spec to a Version instance, since we'll want to work with # it as a version. - spec = Version(spec) + spec = Version(spec_str) # Check to see if the prospective version is less than the spec # version. If it's not we can short circuit and just return False now @@ -470,11 +477,11 @@ class Specifier(_IndividualSpecifier): # version in the spec. return True - @_require_version_compare - def _compare_greater_than(self, prospective, spec): + def _compare_greater_than(self, prospective: Version, spec_str: str) -> bool: + # Convert our spec to a Version instance, since we'll want to work with # it as a version. - spec = Version(spec) + spec = Version(spec_str) # Check to see if the prospective version is greater than the spec # version. If it's not we can short circuit and just return False now @@ -501,43 +508,143 @@ class Specifier(_IndividualSpecifier): # same version in the spec. return True - def _compare_arbitrary(self, prospective, spec): + def _compare_arbitrary(self, prospective: Version, spec: str) -> bool: return str(prospective).lower() == str(spec).lower() - @property - def prereleases(self): - # If there is an explicit prereleases set for this, then we'll just - # blindly use that. - if self._prereleases is not None: - return self._prereleases + def __contains__(self, item: Union[str, Version]) -> bool: + """Return whether or not the item is contained in this specifier. - # Look at all of our specifiers and determine if they are inclusive - # operators, and if they are if they are including an explicit - # prerelease. - operator, version = self._spec - if operator in ["==", ">=", "<=", "~=", "==="]: - # The == specifier can include a trailing .*, if it does we - # want to remove before parsing. - if operator == "==" and version.endswith(".*"): - version = version[:-2] + :param item: The item to check for. - # Parse the version, and if it is a pre-release than this - # specifier allows pre-releases. - if parse(version).is_prerelease: - return True + This is used for the ``in`` operator and behaves the same as + :meth:`contains` with no ``prereleases`` argument passed. - return False + >>> "1.2.3" in Specifier(">=1.2.3") + True + >>> Version("1.2.3") in Specifier(">=1.2.3") + True + >>> "1.0.0" in Specifier(">=1.2.3") + False + >>> "1.3.0a1" in Specifier(">=1.2.3") + False + >>> "1.3.0a1" in Specifier(">=1.2.3", prereleases=True) + True + """ + return self.contains(item) - @prereleases.setter - def prereleases(self, value): - self._prereleases = value + def contains( + self, item: UnparsedVersion, prereleases: Optional[bool] = None + ) -> bool: + """Return whether or not the item is contained in this specifier. + + :param item: + The item to check for, which can be a version string or a + :class:`Version` instance. + :param prereleases: + Whether or not to match prereleases with this Specifier. If set to + ``None`` (the default), it uses :attr:`prereleases` to determine + whether or not prereleases are allowed. + + >>> Specifier(">=1.2.3").contains("1.2.3") + True + >>> Specifier(">=1.2.3").contains(Version("1.2.3")) + True + >>> Specifier(">=1.2.3").contains("1.0.0") + False + >>> Specifier(">=1.2.3").contains("1.3.0a1") + False + >>> Specifier(">=1.2.3", prereleases=True).contains("1.3.0a1") + True + >>> Specifier(">=1.2.3").contains("1.3.0a1", prereleases=True) + True + """ + + # Determine if prereleases are to be allowed or not. + if prereleases is None: + prereleases = self.prereleases + + # Normalize item to a Version, this allows us to have a shortcut for + # "2.0" in Specifier(">=2") + normalized_item = _coerce_version(item) + + # Determine if we should be supporting prereleases in this specifier + # or not, if we do not support prereleases than we can short circuit + # logic if this version is a prereleases. + if normalized_item.is_prerelease and not prereleases: + return False + + # Actually do the comparison to determine if this item is contained + # within this Specifier or not. + operator_callable: CallableOperator = self._get_operator(self.operator) + return operator_callable(normalized_item, self.version) + + def filter( + self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None + ) -> Iterator[UnparsedVersionVar]: + """Filter items in the given iterable, that match the specifier. + + :param iterable: + An iterable that can contain version strings and :class:`Version` instances. + The items in the iterable will be filtered according to the specifier. + :param prereleases: + Whether or not to allow prereleases in the returned iterator. If set to + ``None`` (the default), it will be intelligently decide whether to allow + prereleases or not (based on the :attr:`prereleases` attribute, and + whether the only versions matching are prereleases). + + This method is smarter than just ``filter(Specifier().contains, [...])`` + because it implements the rule from :pep:`440` that a prerelease item + SHOULD be accepted if no other versions match the given specifier. + + >>> list(Specifier(">=1.2.3").filter(["1.2", "1.3", "1.5a1"])) + ['1.3'] + >>> list(Specifier(">=1.2.3").filter(["1.2", "1.2.3", "1.3", Version("1.4")])) + ['1.2.3', '1.3', ] + >>> list(Specifier(">=1.2.3").filter(["1.2", "1.5a1"])) + ['1.5a1'] + >>> list(Specifier(">=1.2.3").filter(["1.3", "1.5a1"], prereleases=True)) + ['1.3', '1.5a1'] + >>> list(Specifier(">=1.2.3", prereleases=True).filter(["1.3", "1.5a1"])) + ['1.3', '1.5a1'] + """ + + yielded = False + found_prereleases = [] + + kw = {"prereleases": prereleases if prereleases is not None else True} + + # Attempt to iterate over all the values in the iterable and if any of + # them match, yield them. + for version in iterable: + parsed_version = _coerce_version(version) + + if self.contains(parsed_version, **kw): + # If our version is a prerelease, and we were not set to allow + # prereleases, then we'll store it for later in case nothing + # else matches this specifier. + if parsed_version.is_prerelease and not ( + prereleases or self.prereleases + ): + found_prereleases.append(version) + # Either this is not a prerelease, or we should have been + # accepting prereleases from the beginning. + else: + yielded = True + yield version + + # Now that we've iterated over everything, determine if we've yielded + # any values, and if we have not and we have any prereleases stored up + # then we will go ahead and yield the prereleases. + if not yielded and found_prereleases: + for version in found_prereleases: + yield version _prefix_regex = re.compile(r"^([0-9]+)((?:a|b|c|rc)[0-9]+)$") -def _version_split(version): - result = [] +def _version_split(version: str) -> List[str]: + result: List[str] = [] for item in version.split("."): match = _prefix_regex.search(item) if match: @@ -547,7 +654,13 @@ def _version_split(version): return result -def _pad_version(left, right): +def _is_not_suffix(segment: str) -> bool: + return not any( + segment.startswith(prefix) for prefix in ("dev", "a", "b", "rc", "post") + ) + + +def _pad_version(left: List[str], right: List[str]) -> Tuple[List[str], List[str]]: left_split, right_split = [], [] # Get the release segment of our versions @@ -566,19 +679,39 @@ def _pad_version(left, right): class SpecifierSet(BaseSpecifier): - def __init__(self, specifiers="", prereleases=None): - # Split on , to break each indidivual specifier into it's own item, and + """This class abstracts handling of a set of version specifiers. + + It can be passed a single specifier (``>=3.0``), a comma-separated list of + specifiers (``>=3.0,!=3.1``), or no specifier at all. + """ + + def __init__( + self, specifiers: str = "", prereleases: Optional[bool] = None + ) -> None: + """Initialize a SpecifierSet instance. + + :param specifiers: + The string representation of a specifier or a comma-separated list of + specifiers which will be parsed and normalized before use. + :param prereleases: + This tells the SpecifierSet if it should accept prerelease versions if + applicable or not. The default of ``None`` will autodetect it from the + given specifiers. + + :raises InvalidSpecifier: + If the given ``specifiers`` are not parseable than this exception will be + raised. + """ + + # Split on `,` to break each individual specifier into it's own item, and # strip each item to remove leading/trailing whitespace. - specifiers = [s.strip() for s in specifiers.split(",") if s.strip()] + split_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()] # Parsed each individual specifier, attempting first to make it a - # Specifier and falling back to a LegacySpecifier. - parsed = set() - for specifier in specifiers: - try: - parsed.add(Specifier(specifier)) - except InvalidSpecifier: - parsed.add(LegacySpecifier(specifier)) + # Specifier. + parsed: Set[Specifier] = set() + for specifier in split_specifiers: + parsed.add(Specifier(specifier)) # Turn our parsed specifiers into a frozen set and save them for later. self._specs = frozenset(parsed) @@ -587,23 +720,75 @@ class SpecifierSet(BaseSpecifier): # we accept prereleases or not. self._prereleases = prereleases - def __repr__(self): + @property + def prereleases(self) -> Optional[bool]: + # If we have been given an explicit prerelease modifier, then we'll + # pass that through here. + if self._prereleases is not None: + return self._prereleases + + # If we don't have any specifiers, and we don't have a forced value, + # then we'll just return None since we don't know if this should have + # pre-releases or not. + if not self._specs: + return None + + # Otherwise we'll see if any of the given specifiers accept + # prereleases, if any of them do we'll return True, otherwise False. + return any(s.prereleases for s in self._specs) + + @prereleases.setter + def prereleases(self, value: bool) -> None: + self._prereleases = value + + def __repr__(self) -> str: + """A representation of the specifier set that shows all internal state. + + Note that the ordering of the individual specifiers within the set may not + match the input string. + + >>> SpecifierSet('>=1.0.0,!=2.0.0') + =1.0.0')> + >>> SpecifierSet('>=1.0.0,!=2.0.0', prereleases=False) + =1.0.0', prereleases=False)> + >>> SpecifierSet('>=1.0.0,!=2.0.0', prereleases=True) + =1.0.0', prereleases=True)> + """ pre = ( - ", prereleases={0!r}".format(self.prereleases) + f", prereleases={self.prereleases!r}" if self._prereleases is not None else "" ) - return "".format(str(self), pre) + return f"" - def __str__(self): + def __str__(self) -> str: + """A string representation of the specifier set that can be round-tripped. + + Note that the ordering of the individual specifiers within the set may not + match the input string. + + >>> str(SpecifierSet(">=1.0.0,!=1.0.1")) + '!=1.0.1,>=1.0.0' + >>> str(SpecifierSet(">=1.0.0,!=1.0.1", prereleases=False)) + '!=1.0.1,>=1.0.0' + """ return ",".join(sorted(str(s) for s in self._specs)) - def __hash__(self): + def __hash__(self) -> int: return hash(self._specs) - def __and__(self, other): - if isinstance(other, string_types): + def __and__(self, other: Union["SpecifierSet", str]) -> "SpecifierSet": + """Return a SpecifierSet which is a combination of the two sets. + + :param other: The other object to combine with. + + >>> SpecifierSet(">=1.0.0,!=1.0.1") & '<=2.0.0,!=2.0.1' + =1.0.0')> + >>> SpecifierSet(">=1.0.0,!=1.0.1") & SpecifierSet('<=2.0.0,!=2.0.1') + =1.0.0')> + """ + if isinstance(other, str): other = SpecifierSet(other) elif not isinstance(other, SpecifierSet): return NotImplemented @@ -625,60 +810,99 @@ class SpecifierSet(BaseSpecifier): return specifier - def __eq__(self, other): - if isinstance(other, string_types): - other = SpecifierSet(other) - elif isinstance(other, _IndividualSpecifier): + def __eq__(self, other: object) -> bool: + """Whether or not the two SpecifierSet-like objects are equal. + + :param other: The other object to check against. + + The value of :attr:`prereleases` is ignored. + + >>> SpecifierSet(">=1.0.0,!=1.0.1") == SpecifierSet(">=1.0.0,!=1.0.1") + True + >>> (SpecifierSet(">=1.0.0,!=1.0.1", prereleases=False) == + ... SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True)) + True + >>> SpecifierSet(">=1.0.0,!=1.0.1") == ">=1.0.0,!=1.0.1" + True + >>> SpecifierSet(">=1.0.0,!=1.0.1") == SpecifierSet(">=1.0.0") + False + >>> SpecifierSet(">=1.0.0,!=1.0.1") == SpecifierSet(">=1.0.0,!=1.0.2") + False + """ + if isinstance(other, (str, Specifier)): other = SpecifierSet(str(other)) elif not isinstance(other, SpecifierSet): return NotImplemented return self._specs == other._specs - def __ne__(self, other): - if isinstance(other, string_types): - other = SpecifierSet(other) - elif isinstance(other, _IndividualSpecifier): - other = SpecifierSet(str(other)) - elif not isinstance(other, SpecifierSet): - return NotImplemented - - return self._specs != other._specs - - def __len__(self): + def __len__(self) -> int: + """Returns the number of specifiers in this specifier set.""" return len(self._specs) - def __iter__(self): + def __iter__(self) -> Iterator[Specifier]: + """ + Returns an iterator over all the underlying :class:`Specifier` instances + in this specifier set. + + >>> sorted(SpecifierSet(">=1.0.0,!=1.0.1"), key=str) + [, =1.0.0')>] + """ return iter(self._specs) - @property - def prereleases(self): - # If we have been given an explicit prerelease modifier, then we'll - # pass that through here. - if self._prereleases is not None: - return self._prereleases + def __contains__(self, item: UnparsedVersion) -> bool: + """Return whether or not the item is contained in this specifier. - # If we don't have any specifiers, and we don't have a forced value, - # then we'll just return None since we don't know if this should have - # pre-releases or not. - if not self._specs: - return None + :param item: The item to check for. - # Otherwise we'll see if any of the given specifiers accept - # prereleases, if any of them do we'll return True, otherwise False. - return any(s.prereleases for s in self._specs) + This is used for the ``in`` operator and behaves the same as + :meth:`contains` with no ``prereleases`` argument passed. - @prereleases.setter - def prereleases(self, value): - self._prereleases = value - - def __contains__(self, item): + >>> "1.2.3" in SpecifierSet(">=1.0.0,!=1.0.1") + True + >>> Version("1.2.3") in SpecifierSet(">=1.0.0,!=1.0.1") + True + >>> "1.0.1" in SpecifierSet(">=1.0.0,!=1.0.1") + False + >>> "1.3.0a1" in SpecifierSet(">=1.0.0,!=1.0.1") + False + >>> "1.3.0a1" in SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True) + True + """ return self.contains(item) - def contains(self, item, prereleases=None): - # Ensure that our item is a Version or LegacyVersion instance. - if not isinstance(item, (LegacyVersion, Version)): - item = parse(item) + def contains( + self, + item: UnparsedVersion, + prereleases: Optional[bool] = None, + installed: Optional[bool] = None, + ) -> bool: + """Return whether or not the item is contained in this SpecifierSet. + + :param item: + The item to check for, which can be a version string or a + :class:`Version` instance. + :param prereleases: + Whether or not to match prereleases with this SpecifierSet. If set to + ``None`` (the default), it uses :attr:`prereleases` to determine + whether or not prereleases are allowed. + + >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.2.3") + True + >>> SpecifierSet(">=1.0.0,!=1.0.1").contains(Version("1.2.3")) + True + >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.0.1") + False + >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.3.0a1") + False + >>> SpecifierSet(">=1.0.0,!=1.0.1", prereleases=True).contains("1.3.0a1") + True + >>> SpecifierSet(">=1.0.0,!=1.0.1").contains("1.3.0a1", prereleases=True) + True + """ + # Ensure that our item is a Version instance. + if not isinstance(item, Version): + item = Version(item) # Determine if we're forcing a prerelease or not, if we're not forcing # one for this particular filter call, then we'll use whatever the @@ -695,13 +919,56 @@ class SpecifierSet(BaseSpecifier): if not prereleases and item.is_prerelease: return False + if installed and item.is_prerelease: + item = Version(item.base_version) + # We simply dispatch to the underlying specs here to make sure that the # given version is contained within all of them. # Note: This use of all() here means that an empty set of specifiers # will always return True, this is an explicit design decision. return all(s.contains(item, prereleases=prereleases) for s in self._specs) - def filter(self, iterable, prereleases=None): + def filter( + self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None + ) -> Iterator[UnparsedVersionVar]: + """Filter items in the given iterable, that match the specifiers in this set. + + :param iterable: + An iterable that can contain version strings and :class:`Version` instances. + The items in the iterable will be filtered according to the specifier. + :param prereleases: + Whether or not to allow prereleases in the returned iterator. If set to + ``None`` (the default), it will be intelligently decide whether to allow + prereleases or not (based on the :attr:`prereleases` attribute, and + whether the only versions matching are prereleases). + + This method is smarter than just ``filter(SpecifierSet(...).contains, [...])`` + because it implements the rule from :pep:`440` that a prerelease item + SHOULD be accepted if no other versions match the given specifier. + + >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.3", "1.5a1"])) + ['1.3'] + >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.3", Version("1.4")])) + ['1.3', ] + >>> list(SpecifierSet(">=1.2.3").filter(["1.2", "1.5a1"])) + [] + >>> list(SpecifierSet(">=1.2.3").filter(["1.3", "1.5a1"], prereleases=True)) + ['1.3', '1.5a1'] + >>> list(SpecifierSet(">=1.2.3", prereleases=True).filter(["1.3", "1.5a1"])) + ['1.3', '1.5a1'] + + An "empty" SpecifierSet will filter items based on the presence of prerelease + versions in the set. + + >>> list(SpecifierSet("").filter(["1.3", "1.5a1"])) + ['1.3'] + >>> list(SpecifierSet("").filter(["1.5a1"])) + ['1.5a1'] + >>> list(SpecifierSet("", prereleases=True).filter(["1.3", "1.5a1"])) + ['1.3', '1.5a1'] + >>> list(SpecifierSet("").filter(["1.3", "1.5a1"], prereleases=True)) + ['1.3', '1.5a1'] + """ # Determine if we're forcing a prerelease or not, if we're not forcing # one for this particular filter call, then we'll use whatever the # SpecifierSet thinks for whether or not we should support prereleases. @@ -714,24 +981,16 @@ class SpecifierSet(BaseSpecifier): if self._specs: for spec in self._specs: iterable = spec.filter(iterable, prereleases=bool(prereleases)) - return iterable + return iter(iterable) # If we do not have any specifiers, then we need to have a rough filter # which will filter out any pre-releases, unless there are no final - # releases, and which will filter out LegacyVersion in general. + # releases. else: - filtered = [] - found_prereleases = [] + filtered: List[UnparsedVersionVar] = [] + found_prereleases: List[UnparsedVersionVar] = [] for item in iterable: - # Ensure that we some kind of Version class for this item. - if not isinstance(item, (LegacyVersion, Version)): - parsed_version = parse(item) - else: - parsed_version = item - - # Filter out any item which is parsed as a LegacyVersion - if isinstance(parsed_version, LegacyVersion): - continue + parsed_version = _coerce_version(item) # Store any item which is a pre-release for later unless we've # already found a final version or we are accepting prereleases @@ -744,6 +1003,6 @@ class SpecifierSet(BaseSpecifier): # If we've found no items except for pre-releases, then we'll go # ahead and use the pre-releases if not filtered and found_prereleases and prereleases is None: - return found_prereleases + return iter(found_prereleases) - return filtered + return iter(filtered) diff --git a/libs/common/setuptools/_vendor/packaging/tags.py b/libs/common/setuptools/_vendor/packaging/tags.py index ec9942f0..76d24341 100644 --- a/libs/common/setuptools/_vendor/packaging/tags.py +++ b/libs/common/setuptools/_vendor/packaging/tags.py @@ -2,25 +2,33 @@ # 2.0, and the BSD License. See the LICENSE file in the root of this repository # for complete details. -from __future__ import absolute_import - -import distutils.util - -try: - from importlib.machinery import EXTENSION_SUFFIXES -except ImportError: # pragma: no cover - import imp - - EXTENSION_SUFFIXES = [x[0] for x in imp.get_suffixes()] - del imp +import logging import platform -import re +import subprocess import sys import sysconfig -import warnings +from importlib.machinery import EXTENSION_SUFFIXES +from typing import ( + Dict, + FrozenSet, + Iterable, + Iterator, + List, + Optional, + Sequence, + Tuple, + Union, + cast, +) +from . import _manylinux, _musllinux -INTERPRETER_SHORT_NAMES = { +logger = logging.getLogger(__name__) + +PythonVersion = Sequence[int] +MacVersion = Tuple[int, int] + +INTERPRETER_SHORT_NAMES: Dict[str, str] = { "python": "py", # Generic. "cpython": "cp", "pypy": "pp", @@ -29,48 +37,70 @@ INTERPRETER_SHORT_NAMES = { } -_32_BIT_INTERPRETER = sys.maxsize <= 2 ** 32 +_32_BIT_INTERPRETER = sys.maxsize <= 2**32 -class Tag(object): +class Tag: + """ + A representation of the tag triple for a wheel. - __slots__ = ["_interpreter", "_abi", "_platform"] + Instances are considered immutable and thus are hashable. Equality checking + is also supported. + """ - def __init__(self, interpreter, abi, platform): + __slots__ = ["_interpreter", "_abi", "_platform", "_hash"] + + def __init__(self, interpreter: str, abi: str, platform: str) -> None: self._interpreter = interpreter.lower() self._abi = abi.lower() self._platform = platform.lower() + # The __hash__ of every single element in a Set[Tag] will be evaluated each time + # that a set calls its `.disjoint()` method, which may be called hundreds of + # times when scanning a page of links for packages with tags matching that + # Set[Tag]. Pre-computing the value here produces significant speedups for + # downstream consumers. + self._hash = hash((self._interpreter, self._abi, self._platform)) @property - def interpreter(self): + def interpreter(self) -> str: return self._interpreter @property - def abi(self): + def abi(self) -> str: return self._abi @property - def platform(self): + def platform(self) -> str: return self._platform - def __eq__(self, other): + def __eq__(self, other: object) -> bool: + if not isinstance(other, Tag): + return NotImplemented + return ( - (self.platform == other.platform) - and (self.abi == other.abi) - and (self.interpreter == other.interpreter) + (self._hash == other._hash) # Short-circuit ASAP for perf reasons. + and (self._platform == other._platform) + and (self._abi == other._abi) + and (self._interpreter == other._interpreter) ) - def __hash__(self): - return hash((self._interpreter, self._abi, self._platform)) + def __hash__(self) -> int: + return self._hash - def __str__(self): - return "{}-{}-{}".format(self._interpreter, self._abi, self._platform) + def __str__(self) -> str: + return f"{self._interpreter}-{self._abi}-{self._platform}" - def __repr__(self): - return "<{self} @ {self_id}>".format(self=self, self_id=id(self)) + def __repr__(self) -> str: + return f"<{self} @ {id(self)}>" -def parse_tag(tag): +def parse_tag(tag: str) -> FrozenSet[Tag]: + """ + Parses the provided tag (e.g. `py3-none-any`) into a frozenset of Tag instances. + + Returning a set is required due to the possibility that the tag is a + compressed tag set. + """ tags = set() interpreters, abis, platforms = tag.split("-") for interpreter in interpreters.split("."): @@ -80,20 +110,34 @@ def parse_tag(tag): return frozenset(tags) -def _normalize_string(string): - return string.replace(".", "_").replace("-", "_") +def _get_config_var(name: str, warn: bool = False) -> Union[int, str, None]: + value: Union[int, str, None] = sysconfig.get_config_var(name) + if value is None and warn: + logger.debug( + "Config variable '%s' is unset, Python ABI tag may be incorrect", name + ) + return value -def _cpython_interpreter(py_version): - # TODO: Is using py_version_nodot for interpreter version critical? - return "cp{major}{minor}".format(major=py_version[0], minor=py_version[1]) +def _normalize_string(string: str) -> str: + return string.replace(".", "_").replace("-", "_").replace(" ", "_") -def _cpython_abis(py_version): +def _abi3_applies(python_version: PythonVersion) -> bool: + """ + Determine if the Python version supports abi3. + + PEP 384 was first implemented in Python 3.2. + """ + return len(python_version) > 1 and tuple(python_version) >= (3, 2) + + +def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> List[str]: + py_version = tuple(py_version) # To allow for version comparison. abis = [] - version = "{}{}".format(*py_version[:2]) + version = _version_nodot(py_version[:2]) debug = pymalloc = ucs4 = "" - with_debug = sysconfig.get_config_var("Py_DEBUG") + with_debug = _get_config_var("Py_DEBUG", warn) has_refcount = hasattr(sys, "gettotalrefcount") # Windows doesn't set Py_DEBUG, so checking for support of debug-compiled # extension modules is the best option. @@ -102,11 +146,11 @@ def _cpython_abis(py_version): if with_debug or (with_debug is None and (has_refcount or has_ext)): debug = "d" if py_version < (3, 8): - with_pymalloc = sysconfig.get_config_var("WITH_PYMALLOC") + with_pymalloc = _get_config_var("WITH_PYMALLOC", warn) if with_pymalloc or with_pymalloc is None: pymalloc = "m" if py_version < (3, 3): - unicode_size = sysconfig.get_config_var("Py_UNICODE_SIZE") + unicode_size = _get_config_var("Py_UNICODE_SIZE", warn) if unicode_size == 4 or ( unicode_size is None and sys.maxunicode == 0x10FFFF ): @@ -114,7 +158,7 @@ def _cpython_abis(py_version): elif debug: # Debug builds can also load "normal" extension modules. # We can also assume no UCS-4 or pymalloc requirement. - abis.append("cp{version}".format(version=version)) + abis.append(f"cp{version}") abis.insert( 0, "cp{version}{debug}{pymalloc}{ucs4}".format( @@ -124,86 +168,176 @@ def _cpython_abis(py_version): return abis -def _cpython_tags(py_version, interpreter, abis, platforms): +def cpython_tags( + python_version: Optional[PythonVersion] = None, + abis: Optional[Iterable[str]] = None, + platforms: Optional[Iterable[str]] = None, + *, + warn: bool = False, +) -> Iterator[Tag]: + """ + Yields the tags for a CPython interpreter. + + The tags consist of: + - cp-- + - cp-abi3- + - cp-none- + - cp-abi3- # Older Python versions down to 3.2. + + If python_version only specifies a major version then user-provided ABIs and + the 'none' ABItag will be used. + + If 'abi3' or 'none' are specified in 'abis' then they will be yielded at + their normal position and not at the beginning. + """ + if not python_version: + python_version = sys.version_info[:2] + + interpreter = f"cp{_version_nodot(python_version[:2])}" + + if abis is None: + if len(python_version) > 1: + abis = _cpython_abis(python_version, warn) + else: + abis = [] + abis = list(abis) + # 'abi3' and 'none' are explicitly handled later. + for explicit_abi in ("abi3", "none"): + try: + abis.remove(explicit_abi) + except ValueError: + pass + + platforms = list(platforms or platform_tags()) for abi in abis: for platform_ in platforms: yield Tag(interpreter, abi, platform_) - for tag in (Tag(interpreter, "abi3", platform_) for platform_ in platforms): - yield tag - for tag in (Tag(interpreter, "none", platform_) for platform_ in platforms): - yield tag - # PEP 384 was first implemented in Python 3.2. - for minor_version in range(py_version[1] - 1, 1, -1): - for platform_ in platforms: - interpreter = "cp{major}{minor}".format( - major=py_version[0], minor=minor_version - ) - yield Tag(interpreter, "abi3", platform_) + if _abi3_applies(python_version): + yield from (Tag(interpreter, "abi3", platform_) for platform_ in platforms) + yield from (Tag(interpreter, "none", platform_) for platform_ in platforms) + + if _abi3_applies(python_version): + for minor_version in range(python_version[1] - 1, 1, -1): + for platform_ in platforms: + interpreter = "cp{version}".format( + version=_version_nodot((python_version[0], minor_version)) + ) + yield Tag(interpreter, "abi3", platform_) -def _pypy_interpreter(): - return "pp{py_major}{pypy_major}{pypy_minor}".format( - py_major=sys.version_info[0], - pypy_major=sys.pypy_version_info.major, - pypy_minor=sys.pypy_version_info.minor, - ) - - -def _generic_abi(): - abi = sysconfig.get_config_var("SOABI") - if abi: - return _normalize_string(abi) - else: - return "none" - - -def _pypy_tags(py_version, interpreter, abi, platforms): - for tag in (Tag(interpreter, abi, platform) for platform in platforms): - yield tag - for tag in (Tag(interpreter, "none", platform) for platform in platforms): - yield tag - - -def _generic_tags(interpreter, py_version, abi, platforms): - for tag in (Tag(interpreter, abi, platform) for platform in platforms): - yield tag - if abi != "none": - tags = (Tag(interpreter, "none", platform_) for platform_ in platforms) - for tag in tags: - yield tag - - -def _py_interpreter_range(py_version): +def _generic_abi() -> List[str]: """ - Yield Python versions in descending order. + Return the ABI tag based on EXT_SUFFIX. + """ + # The following are examples of `EXT_SUFFIX`. + # We want to keep the parts which are related to the ABI and remove the + # parts which are related to the platform: + # - linux: '.cpython-310-x86_64-linux-gnu.so' => cp310 + # - mac: '.cpython-310-darwin.so' => cp310 + # - win: '.cp310-win_amd64.pyd' => cp310 + # - win: '.pyd' => cp37 (uses _cpython_abis()) + # - pypy: '.pypy38-pp73-x86_64-linux-gnu.so' => pypy38_pp73 + # - graalpy: '.graalpy-38-native-x86_64-darwin.dylib' + # => graalpy_38_native + + ext_suffix = _get_config_var("EXT_SUFFIX", warn=True) + if not isinstance(ext_suffix, str) or ext_suffix[0] != ".": + raise SystemError("invalid sysconfig.get_config_var('EXT_SUFFIX')") + parts = ext_suffix.split(".") + if len(parts) < 3: + # CPython3.7 and earlier uses ".pyd" on Windows. + return _cpython_abis(sys.version_info[:2]) + soabi = parts[1] + if soabi.startswith("cpython"): + # non-windows + abi = "cp" + soabi.split("-")[1] + elif soabi.startswith("cp"): + # windows + abi = soabi.split("-")[0] + elif soabi.startswith("pypy"): + abi = "-".join(soabi.split("-")[:2]) + elif soabi.startswith("graalpy"): + abi = "-".join(soabi.split("-")[:3]) + elif soabi: + # pyston, ironpython, others? + abi = soabi + else: + return [] + return [_normalize_string(abi)] + + +def generic_tags( + interpreter: Optional[str] = None, + abis: Optional[Iterable[str]] = None, + platforms: Optional[Iterable[str]] = None, + *, + warn: bool = False, +) -> Iterator[Tag]: + """ + Yields the tags for a generic interpreter. + + The tags consist of: + - -- + + The "none" ABI will be added if it was not explicitly provided. + """ + if not interpreter: + interp_name = interpreter_name() + interp_version = interpreter_version(warn=warn) + interpreter = "".join([interp_name, interp_version]) + if abis is None: + abis = _generic_abi() + else: + abis = list(abis) + platforms = list(platforms or platform_tags()) + if "none" not in abis: + abis.append("none") + for abi in abis: + for platform_ in platforms: + yield Tag(interpreter, abi, platform_) + + +def _py_interpreter_range(py_version: PythonVersion) -> Iterator[str]: + """ + Yields Python versions in descending order. After the latest version, the major-only version will be yielded, and then - all following versions up to 'end'. + all previous versions of that major version. """ - yield "py{major}{minor}".format(major=py_version[0], minor=py_version[1]) - yield "py{major}".format(major=py_version[0]) - for minor in range(py_version[1] - 1, -1, -1): - yield "py{major}{minor}".format(major=py_version[0], minor=minor) + if len(py_version) > 1: + yield f"py{_version_nodot(py_version[:2])}" + yield f"py{py_version[0]}" + if len(py_version) > 1: + for minor in range(py_version[1] - 1, -1, -1): + yield f"py{_version_nodot((py_version[0], minor))}" -def _independent_tags(interpreter, py_version, platforms): +def compatible_tags( + python_version: Optional[PythonVersion] = None, + interpreter: Optional[str] = None, + platforms: Optional[Iterable[str]] = None, +) -> Iterator[Tag]: """ - Return the sequence of tags that are consistent across implementations. + Yields the sequence of tags that are compatible with a specific version of Python. The tags consist of: - py*-none- - - -none-any + - -none-any # ... if `interpreter` is provided. - py*-none-any """ - for version in _py_interpreter_range(py_version): + if not python_version: + python_version = sys.version_info[:2] + platforms = list(platforms or platform_tags()) + for version in _py_interpreter_range(python_version): for platform_ in platforms: yield Tag(version, "none", platform_) - yield Tag(interpreter, "none", "any") - for version in _py_interpreter_range(py_version): + if interpreter: + yield Tag(interpreter, "none", "any") + for version in _py_interpreter_range(python_version): yield Tag(version, "none", "any") -def _mac_arch(arch, is_32bit=_32_BIT_INTERPRETER): +def _mac_arch(arch: str, is_32bit: bool = _32_BIT_INTERPRETER) -> str: if not is_32bit: return arch @@ -213,7 +347,7 @@ def _mac_arch(arch, is_32bit=_32_BIT_INTERPRETER): return "i386" -def _mac_binary_formats(version, cpu_arch): +def _mac_binary_formats(version: MacVersion, cpu_arch: str) -> List[str]: formats = [cpu_arch] if cpu_arch == "x86_64": if version < (10, 4): @@ -236,169 +370,177 @@ def _mac_binary_formats(version, cpu_arch): return [] formats.extend(["fat32", "fat"]) - formats.append("universal") + if cpu_arch in {"arm64", "x86_64"}: + formats.append("universal2") + + if cpu_arch in {"x86_64", "i386", "ppc64", "ppc", "intel"}: + formats.append("universal") + return formats -def _mac_platforms(version=None, arch=None): +def mac_platforms( + version: Optional[MacVersion] = None, arch: Optional[str] = None +) -> Iterator[str]: + """ + Yields the platform tags for a macOS system. + + The `version` parameter is a two-item tuple specifying the macOS version to + generate platform tags for. The `arch` parameter is the CPU architecture to + generate platform tags for. Both parameters default to the appropriate value + for the current system. + """ version_str, _, cpu_arch = platform.mac_ver() if version is None: - version = tuple(map(int, version_str.split(".")[:2])) + version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2]))) + if version == (10, 16): + # When built against an older macOS SDK, Python will report macOS 10.16 + # instead of the real version. + version_str = subprocess.run( + [ + sys.executable, + "-sS", + "-c", + "import platform; print(platform.mac_ver()[0])", + ], + check=True, + env={"SYSTEM_VERSION_COMPAT": "0"}, + stdout=subprocess.PIPE, + universal_newlines=True, + ).stdout + version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2]))) + else: + version = version if arch is None: arch = _mac_arch(cpu_arch) - platforms = [] - for minor_version in range(version[1], -1, -1): - compat_version = version[0], minor_version - binary_formats = _mac_binary_formats(compat_version, arch) - for binary_format in binary_formats: - platforms.append( - "macosx_{major}_{minor}_{binary_format}".format( + else: + arch = arch + + if (10, 0) <= version and version < (11, 0): + # Prior to Mac OS 11, each yearly release of Mac OS bumped the + # "minor" version number. The major version was always 10. + for minor_version in range(version[1], -1, -1): + compat_version = 10, minor_version + binary_formats = _mac_binary_formats(compat_version, arch) + for binary_format in binary_formats: + yield "macosx_{major}_{minor}_{binary_format}".format( + major=10, minor=minor_version, binary_format=binary_format + ) + + if version >= (11, 0): + # Starting with Mac OS 11, each yearly release bumps the major version + # number. The minor versions are now the midyear updates. + for major_version in range(version[0], 10, -1): + compat_version = major_version, 0 + binary_formats = _mac_binary_formats(compat_version, arch) + for binary_format in binary_formats: + yield "macosx_{major}_{minor}_{binary_format}".format( + major=major_version, minor=0, binary_format=binary_format + ) + + if version >= (11, 0): + # Mac OS 11 on x86_64 is compatible with binaries from previous releases. + # Arm64 support was introduced in 11.0, so no Arm binaries from previous + # releases exist. + # + # However, the "universal2" binary format can have a + # macOS version earlier than 11.0 when the x86_64 part of the binary supports + # that version of macOS. + if arch == "x86_64": + for minor_version in range(16, 3, -1): + compat_version = 10, minor_version + binary_formats = _mac_binary_formats(compat_version, arch) + for binary_format in binary_formats: + yield "macosx_{major}_{minor}_{binary_format}".format( + major=compat_version[0], + minor=compat_version[1], + binary_format=binary_format, + ) + else: + for minor_version in range(16, 3, -1): + compat_version = 10, minor_version + binary_format = "universal2" + yield "macosx_{major}_{minor}_{binary_format}".format( major=compat_version[0], minor=compat_version[1], binary_format=binary_format, ) - ) - return platforms -# From PEP 513. -def _is_manylinux_compatible(name, glibc_version): - # Check for presence of _manylinux module. - try: - import _manylinux - - return bool(getattr(_manylinux, name + "_compatible")) - except (ImportError, AttributeError): - # Fall through to heuristic check below. - pass - - return _have_compatible_glibc(*glibc_version) +def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]: + linux = _normalize_string(sysconfig.get_platform()) + if is_32bit: + if linux == "linux_x86_64": + linux = "linux_i686" + elif linux == "linux_aarch64": + linux = "linux_armv7l" + _, arch = linux.split("_", 1) + yield from _manylinux.platform_tags(linux, arch) + yield from _musllinux.platform_tags(arch) + yield linux -def _glibc_version_string(): - # Returns glibc version string, or None if not using glibc. - import ctypes - - # ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen - # manpage says, "If filename is NULL, then the returned handle is for the - # main program". This way we can let the linker do the work to figure out - # which libc our process is actually using. - process_namespace = ctypes.CDLL(None) - try: - gnu_get_libc_version = process_namespace.gnu_get_libc_version - except AttributeError: - # Symbol doesn't exist -> therefore, we are not linked to - # glibc. - return None - - # Call gnu_get_libc_version, which returns a string like "2.5" - gnu_get_libc_version.restype = ctypes.c_char_p - version_str = gnu_get_libc_version() - # py2 / py3 compatibility: - if not isinstance(version_str, str): - version_str = version_str.decode("ascii") - - return version_str +def _generic_platforms() -> Iterator[str]: + yield _normalize_string(sysconfig.get_platform()) -# Separated out from have_compatible_glibc for easier unit testing. -def _check_glibc_version(version_str, required_major, minimum_minor): - # Parse string and check against requested version. - # - # We use a regexp instead of str.split because we want to discard any - # random junk that might come after the minor version -- this might happen - # in patched/forked versions of glibc (e.g. Linaro's version of glibc - # uses version strings like "2.20-2014.11"). See gh-3588. - m = re.match(r"(?P[0-9]+)\.(?P[0-9]+)", version_str) - if not m: - warnings.warn( - "Expected glibc version with 2 components major.minor," - " got: %s" % version_str, - RuntimeWarning, - ) - return False - return ( - int(m.group("major")) == required_major - and int(m.group("minor")) >= minimum_minor - ) - - -def _have_compatible_glibc(required_major, minimum_minor): - version_str = _glibc_version_string() - if version_str is None: - return False - return _check_glibc_version(version_str, required_major, minimum_minor) - - -def _linux_platforms(is_32bit=_32_BIT_INTERPRETER): - linux = _normalize_string(distutils.util.get_platform()) - if linux == "linux_x86_64" and is_32bit: - linux = "linux_i686" - manylinux_support = ( - ("manylinux2014", (2, 17)), # CentOS 7 w/ glibc 2.17 (PEP 599) - ("manylinux2010", (2, 12)), # CentOS 6 w/ glibc 2.12 (PEP 571) - ("manylinux1", (2, 5)), # CentOS 5 w/ glibc 2.5 (PEP 513) - ) - manylinux_support_iter = iter(manylinux_support) - for name, glibc_version in manylinux_support_iter: - if _is_manylinux_compatible(name, glibc_version): - platforms = [linux.replace("linux", name)] - break +def platform_tags() -> Iterator[str]: + """ + Provides the platform tags for this installation. + """ + if platform.system() == "Darwin": + return mac_platforms() + elif platform.system() == "Linux": + return _linux_platforms() else: - platforms = [] - # Support for a later manylinux implies support for an earlier version. - platforms += [linux.replace("linux", name) for name, _ in manylinux_support_iter] - platforms.append(linux) - return platforms + return _generic_platforms() -def _generic_platforms(): - platform = _normalize_string(distutils.util.get_platform()) - return [platform] +def interpreter_name() -> str: + """ + Returns the name of the running interpreter. - -def _interpreter_name(): - name = platform.python_implementation().lower() + Some implementations have a reserved, two-letter abbreviation which will + be returned when appropriate. + """ + name = sys.implementation.name return INTERPRETER_SHORT_NAMES.get(name) or name -def _generic_interpreter(name, py_version): - version = sysconfig.get_config_var("py_version_nodot") - if not version: - version = "".join(map(str, py_version[:2])) - return "{name}{version}".format(name=name, version=version) +def interpreter_version(*, warn: bool = False) -> str: + """ + Returns the version of the running interpreter. + """ + version = _get_config_var("py_version_nodot", warn=warn) + if version: + version = str(version) + else: + version = _version_nodot(sys.version_info[:2]) + return version -def sys_tags(): +def _version_nodot(version: PythonVersion) -> str: + return "".join(map(str, version)) + + +def sys_tags(*, warn: bool = False) -> Iterator[Tag]: """ Returns the sequence of tag triples for the running interpreter. The order of the sequence corresponds to priority order for the interpreter, from most to least important. """ - py_version = sys.version_info[:2] - interpreter_name = _interpreter_name() - if platform.system() == "Darwin": - platforms = _mac_platforms() - elif platform.system() == "Linux": - platforms = _linux_platforms() - else: - platforms = _generic_platforms() - if interpreter_name == "cp": - interpreter = _cpython_interpreter(py_version) - abis = _cpython_abis(py_version) - for tag in _cpython_tags(py_version, interpreter, abis, platforms): - yield tag - elif interpreter_name == "pp": - interpreter = _pypy_interpreter() - abi = _generic_abi() - for tag in _pypy_tags(py_version, interpreter, abi, platforms): - yield tag + interp_name = interpreter_name() + if interp_name == "cp": + yield from cpython_tags(warn=warn) else: - interpreter = _generic_interpreter(interpreter_name, py_version) - abi = _generic_abi() - for tag in _generic_tags(interpreter, py_version, abi, platforms): - yield tag - for tag in _independent_tags(interpreter, py_version, platforms): - yield tag + yield from generic_tags() + + if interp_name == "pp": + interp = "pp3" + elif interp_name == "cp": + interp = "cp" + interpreter_version(warn=warn) + else: + interp = None + yield from compatible_tags(interpreter=interp) diff --git a/libs/common/setuptools/_vendor/packaging/utils.py b/libs/common/setuptools/_vendor/packaging/utils.py index 88418786..33c613b7 100644 --- a/libs/common/setuptools/_vendor/packaging/utils.py +++ b/libs/common/setuptools/_vendor/packaging/utils.py @@ -1,57 +1,141 @@ # This file is dual licensed under the terms of the Apache License, Version # 2.0, and the BSD License. See the LICENSE file in the root of this repository # for complete details. -from __future__ import absolute_import, division, print_function import re +from typing import FrozenSet, NewType, Tuple, Union, cast +from .tags import Tag, parse_tag from .version import InvalidVersion, Version +BuildTag = Union[Tuple[()], Tuple[int, str]] +NormalizedName = NewType("NormalizedName", str) + + +class InvalidWheelFilename(ValueError): + """ + An invalid wheel filename was found, users should refer to PEP 427. + """ + + +class InvalidSdistFilename(ValueError): + """ + An invalid sdist filename was found, users should refer to the packaging user guide. + """ + _canonicalize_regex = re.compile(r"[-_.]+") +# PEP 427: The build number must start with a digit. +_build_tag_regex = re.compile(r"(\d+)(.*)") -def canonicalize_name(name): +def canonicalize_name(name: str) -> NormalizedName: # This is taken from PEP 503. - return _canonicalize_regex.sub("-", name).lower() + value = _canonicalize_regex.sub("-", name).lower() + return cast(NormalizedName, value) -def canonicalize_version(version): +def canonicalize_version( + version: Union[Version, str], *, strip_trailing_zero: bool = True +) -> str: """ - This is very similar to Version.__str__, but has one subtle differences + This is very similar to Version.__str__, but has one subtle difference with the way it handles the release segment. """ - - try: - version = Version(version) - except InvalidVersion: - # Legacy versions cannot be normalized - return version + if isinstance(version, str): + try: + parsed = Version(version) + except InvalidVersion: + # Legacy versions cannot be normalized + return version + else: + parsed = version parts = [] # Epoch - if version.epoch != 0: - parts.append("{0}!".format(version.epoch)) + if parsed.epoch != 0: + parts.append(f"{parsed.epoch}!") # Release segment - # NB: This strips trailing '.0's to normalize - parts.append(re.sub(r"(\.0)+$", "", ".".join(str(x) for x in version.release))) + release_segment = ".".join(str(x) for x in parsed.release) + if strip_trailing_zero: + # NB: This strips trailing '.0's to normalize + release_segment = re.sub(r"(\.0)+$", "", release_segment) + parts.append(release_segment) # Pre-release - if version.pre is not None: - parts.append("".join(str(x) for x in version.pre)) + if parsed.pre is not None: + parts.append("".join(str(x) for x in parsed.pre)) # Post-release - if version.post is not None: - parts.append(".post{0}".format(version.post)) + if parsed.post is not None: + parts.append(f".post{parsed.post}") # Development release - if version.dev is not None: - parts.append(".dev{0}".format(version.dev)) + if parsed.dev is not None: + parts.append(f".dev{parsed.dev}") # Local version segment - if version.local is not None: - parts.append("+{0}".format(version.local)) + if parsed.local is not None: + parts.append(f"+{parsed.local}") return "".join(parts) + + +def parse_wheel_filename( + filename: str, +) -> Tuple[NormalizedName, Version, BuildTag, FrozenSet[Tag]]: + if not filename.endswith(".whl"): + raise InvalidWheelFilename( + f"Invalid wheel filename (extension must be '.whl'): {filename}" + ) + + filename = filename[:-4] + dashes = filename.count("-") + if dashes not in (4, 5): + raise InvalidWheelFilename( + f"Invalid wheel filename (wrong number of parts): {filename}" + ) + + parts = filename.split("-", dashes - 2) + name_part = parts[0] + # See PEP 427 for the rules on escaping the project name + if "__" in name_part or re.match(r"^[\w\d._]*$", name_part, re.UNICODE) is None: + raise InvalidWheelFilename(f"Invalid project name: {filename}") + name = canonicalize_name(name_part) + version = Version(parts[1]) + if dashes == 5: + build_part = parts[2] + build_match = _build_tag_regex.match(build_part) + if build_match is None: + raise InvalidWheelFilename( + f"Invalid build number: {build_part} in '{filename}'" + ) + build = cast(BuildTag, (int(build_match.group(1)), build_match.group(2))) + else: + build = () + tags = parse_tag(parts[-1]) + return (name, version, build, tags) + + +def parse_sdist_filename(filename: str) -> Tuple[NormalizedName, Version]: + if filename.endswith(".tar.gz"): + file_stem = filename[: -len(".tar.gz")] + elif filename.endswith(".zip"): + file_stem = filename[: -len(".zip")] + else: + raise InvalidSdistFilename( + f"Invalid sdist filename (extension must be '.tar.gz' or '.zip'):" + f" {filename}" + ) + + # We are requiring a PEP 440 version, which cannot contain dashes, + # so we split on the last dash. + name_part, sep, version_part = file_stem.rpartition("-") + if not sep: + raise InvalidSdistFilename(f"Invalid sdist filename: {filename}") + + name = canonicalize_name(name_part) + version = Version(version_part) + return (name, version) diff --git a/libs/common/setuptools/_vendor/packaging/version.py b/libs/common/setuptools/_vendor/packaging/version.py index 95157a1f..b30e8cbf 100644 --- a/libs/common/setuptools/_vendor/packaging/version.py +++ b/libs/common/setuptools/_vendor/packaging/version.py @@ -1,184 +1,116 @@ # This file is dual licensed under the terms of the Apache License, Version # 2.0, and the BSD License. See the LICENSE file in the root of this repository # for complete details. -from __future__ import absolute_import, division, print_function +""" +.. testsetup:: + + from packaging.version import parse, Version +""" import collections import itertools import re +from typing import Any, Callable, Optional, SupportsInt, Tuple, Union -from ._structures import Infinity +from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType +__all__ = ["VERSION_PATTERN", "parse", "Version", "InvalidVersion"] -__all__ = ["parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN"] - +InfiniteTypes = Union[InfinityType, NegativeInfinityType] +PrePostDevType = Union[InfiniteTypes, Tuple[str, int]] +SubLocalType = Union[InfiniteTypes, int, str] +LocalType = Union[ + NegativeInfinityType, + Tuple[ + Union[ + SubLocalType, + Tuple[SubLocalType, str], + Tuple[NegativeInfinityType, SubLocalType], + ], + ..., + ], +] +CmpKey = Tuple[ + int, Tuple[int, ...], PrePostDevType, PrePostDevType, PrePostDevType, LocalType +] +VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool] _Version = collections.namedtuple( "_Version", ["epoch", "release", "dev", "pre", "post", "local"] ) -def parse(version): +def parse(version: str) -> "Version": + """Parse the given version string. + + >>> parse('1.0.dev1') + + + :param version: The version string to parse. + :raises InvalidVersion: When the version string is not a valid version. """ - Parse the given version string and return either a :class:`Version` object - or a :class:`LegacyVersion` object depending on if the given version is - a valid PEP 440 version or a legacy version. - """ - try: - return Version(version) - except InvalidVersion: - return LegacyVersion(version) + return Version(version) class InvalidVersion(ValueError): - """ - An invalid version was found, users should refer to PEP 440. + """Raised when a version string is not a valid version. + + >>> Version("invalid") + Traceback (most recent call last): + ... + packaging.version.InvalidVersion: Invalid version: 'invalid' """ -class _BaseVersion(object): - def __hash__(self): +class _BaseVersion: + _key: Tuple[Any, ...] + + def __hash__(self) -> int: return hash(self._key) - def __lt__(self, other): - return self._compare(other, lambda s, o: s < o) - - def __le__(self, other): - return self._compare(other, lambda s, o: s <= o) - - def __eq__(self, other): - return self._compare(other, lambda s, o: s == o) - - def __ge__(self, other): - return self._compare(other, lambda s, o: s >= o) - - def __gt__(self, other): - return self._compare(other, lambda s, o: s > o) - - def __ne__(self, other): - return self._compare(other, lambda s, o: s != o) - - def _compare(self, other, method): + # Please keep the duplicated `isinstance` check + # in the six comparisons hereunder + # unless you find a way to avoid adding overhead function calls. + def __lt__(self, other: "_BaseVersion") -> bool: if not isinstance(other, _BaseVersion): return NotImplemented - return method(self._key, other._key) + return self._key < other._key + def __le__(self, other: "_BaseVersion") -> bool: + if not isinstance(other, _BaseVersion): + return NotImplemented -class LegacyVersion(_BaseVersion): - def __init__(self, version): - self._version = str(version) - self._key = _legacy_cmpkey(self._version) + return self._key <= other._key - def __str__(self): - return self._version + def __eq__(self, other: object) -> bool: + if not isinstance(other, _BaseVersion): + return NotImplemented - def __repr__(self): - return "".format(repr(str(self))) + return self._key == other._key - @property - def public(self): - return self._version + def __ge__(self, other: "_BaseVersion") -> bool: + if not isinstance(other, _BaseVersion): + return NotImplemented - @property - def base_version(self): - return self._version + return self._key >= other._key - @property - def epoch(self): - return -1 + def __gt__(self, other: "_BaseVersion") -> bool: + if not isinstance(other, _BaseVersion): + return NotImplemented - @property - def release(self): - return None + return self._key > other._key - @property - def pre(self): - return None + def __ne__(self, other: object) -> bool: + if not isinstance(other, _BaseVersion): + return NotImplemented - @property - def post(self): - return None - - @property - def dev(self): - return None - - @property - def local(self): - return None - - @property - def is_prerelease(self): - return False - - @property - def is_postrelease(self): - return False - - @property - def is_devrelease(self): - return False - - -_legacy_version_component_re = re.compile(r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE) - -_legacy_version_replacement_map = { - "pre": "c", - "preview": "c", - "-": "final-", - "rc": "c", - "dev": "@", -} - - -def _parse_version_parts(s): - for part in _legacy_version_component_re.split(s): - part = _legacy_version_replacement_map.get(part, part) - - if not part or part == ".": - continue - - if part[:1] in "0123456789": - # pad for numeric comparison - yield part.zfill(8) - else: - yield "*" + part - - # ensure that alpha/beta/candidate are before final - yield "*final" - - -def _legacy_cmpkey(version): - # We hardcode an epoch of -1 here. A PEP 440 version can only have a epoch - # greater than or equal to 0. This will effectively put the LegacyVersion, - # which uses the defacto standard originally implemented by setuptools, - # as before all PEP 440 versions. - epoch = -1 - - # This scheme is taken from pkg_resources.parse_version setuptools prior to - # it's adoption of the packaging library. - parts = [] - for part in _parse_version_parts(version.lower()): - if part.startswith("*"): - # remove "-" before a prerelease tag - if part < "*final": - while parts and parts[-1] == "*final-": - parts.pop() - - # remove trailing zeros from each series of numeric parts - while parts and parts[-1] == "00000000": - parts.pop() - - parts.append(part) - parts = tuple(parts) - - return epoch, parts + return self._key != other._key # Deliberately not anchored to the start and end of the string, to make it # easier for 3rd party code to reuse -VERSION_PATTERN = r""" +_VERSION_PATTERN = r""" v? (?: (?:(?P[0-9]+)!)? # epoch @@ -209,16 +141,61 @@ VERSION_PATTERN = r""" (?:\+(?P[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version """ +VERSION_PATTERN = _VERSION_PATTERN +""" +A string containing the regular expression used to match a valid version. + +The pattern is not anchored at either end, and is intended for embedding in larger +expressions (for example, matching a version number as part of a file name). The +regular expression should be compiled with the ``re.VERBOSE`` and ``re.IGNORECASE`` +flags set. + +:meta hide-value: +""" + class Version(_BaseVersion): + """This class abstracts handling of a project's versions. + + A :class:`Version` instance is comparison aware and can be compared and + sorted using the standard Python interfaces. + + >>> v1 = Version("1.0a5") + >>> v2 = Version("1.0") + >>> v1 + + >>> v2 + + >>> v1 < v2 + True + >>> v1 == v2 + False + >>> v1 > v2 + False + >>> v1 >= v2 + False + >>> v1 <= v2 + True + """ _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE) + _key: CmpKey + + def __init__(self, version: str) -> None: + """Initialize a Version object. + + :param version: + The string representation of a version which will be parsed and normalized + before use. + :raises InvalidVersion: + If the ``version`` does not conform to PEP 440 in any way then this + exception will be raised. + """ - def __init__(self, version): # Validate the version and parse it into pieces match = self._regex.search(version) if not match: - raise InvalidVersion("Invalid version: '{0}'".format(version)) + raise InvalidVersion(f"Invalid version: '{version}'") # Store the parsed out pieces of the version self._version = _Version( @@ -242,15 +219,25 @@ class Version(_BaseVersion): self._version.local, ) - def __repr__(self): - return "".format(repr(str(self))) + def __repr__(self) -> str: + """A representation of the Version that shows all internal state. - def __str__(self): + >>> Version('1.0.0') + + """ + return f"" + + def __str__(self) -> str: + """A string representation of the version that can be rounded-tripped. + + >>> str(Version("1.0a5")) + '1.0a5' + """ parts = [] # Epoch if self.epoch != 0: - parts.append("{0}!".format(self.epoch)) + parts.append(f"{self.epoch}!") # Release segment parts.append(".".join(str(x) for x in self.release)) @@ -261,56 +248,131 @@ class Version(_BaseVersion): # Post-release if self.post is not None: - parts.append(".post{0}".format(self.post)) + parts.append(f".post{self.post}") # Development release if self.dev is not None: - parts.append(".dev{0}".format(self.dev)) + parts.append(f".dev{self.dev}") # Local version segment if self.local is not None: - parts.append("+{0}".format(self.local)) + parts.append(f"+{self.local}") return "".join(parts) @property - def epoch(self): - return self._version.epoch + def epoch(self) -> int: + """The epoch of the version. + + >>> Version("2.0.0").epoch + 0 + >>> Version("1!2.0.0").epoch + 1 + """ + _epoch: int = self._version.epoch + return _epoch @property - def release(self): - return self._version.release + def release(self) -> Tuple[int, ...]: + """The components of the "release" segment of the version. + + >>> Version("1.2.3").release + (1, 2, 3) + >>> Version("2.0.0").release + (2, 0, 0) + >>> Version("1!2.0.0.post0").release + (2, 0, 0) + + Includes trailing zeroes but not the epoch or any pre-release / development / + post-release suffixes. + """ + _release: Tuple[int, ...] = self._version.release + return _release @property - def pre(self): - return self._version.pre + def pre(self) -> Optional[Tuple[str, int]]: + """The pre-release segment of the version. + + >>> print(Version("1.2.3").pre) + None + >>> Version("1.2.3a1").pre + ('a', 1) + >>> Version("1.2.3b1").pre + ('b', 1) + >>> Version("1.2.3rc1").pre + ('rc', 1) + """ + _pre: Optional[Tuple[str, int]] = self._version.pre + return _pre @property - def post(self): + def post(self) -> Optional[int]: + """The post-release number of the version. + + >>> print(Version("1.2.3").post) + None + >>> Version("1.2.3.post1").post + 1 + """ return self._version.post[1] if self._version.post else None @property - def dev(self): + def dev(self) -> Optional[int]: + """The development number of the version. + + >>> print(Version("1.2.3").dev) + None + >>> Version("1.2.3.dev1").dev + 1 + """ return self._version.dev[1] if self._version.dev else None @property - def local(self): + def local(self) -> Optional[str]: + """The local version segment of the version. + + >>> print(Version("1.2.3").local) + None + >>> Version("1.2.3+abc").local + 'abc' + """ if self._version.local: return ".".join(str(x) for x in self._version.local) else: return None @property - def public(self): + def public(self) -> str: + """The public portion of the version. + + >>> Version("1.2.3").public + '1.2.3' + >>> Version("1.2.3+abc").public + '1.2.3' + >>> Version("1.2.3+abc.dev1").public + '1.2.3' + """ return str(self).split("+", 1)[0] @property - def base_version(self): + def base_version(self) -> str: + """The "base version" of the version. + + >>> Version("1.2.3").base_version + '1.2.3' + >>> Version("1.2.3+abc").base_version + '1.2.3' + >>> Version("1!1.2.3+abc.dev1").base_version + '1!1.2.3' + + The "base version" is the public version of the project without any pre or post + release markers. + """ parts = [] # Epoch if self.epoch != 0: - parts.append("{0}!".format(self.epoch)) + parts.append(f"{self.epoch}!") # Release segment parts.append(".".join(str(x) for x in self.release)) @@ -318,19 +380,80 @@ class Version(_BaseVersion): return "".join(parts) @property - def is_prerelease(self): + def is_prerelease(self) -> bool: + """Whether this version is a pre-release. + + >>> Version("1.2.3").is_prerelease + False + >>> Version("1.2.3a1").is_prerelease + True + >>> Version("1.2.3b1").is_prerelease + True + >>> Version("1.2.3rc1").is_prerelease + True + >>> Version("1.2.3dev1").is_prerelease + True + """ return self.dev is not None or self.pre is not None @property - def is_postrelease(self): + def is_postrelease(self) -> bool: + """Whether this version is a post-release. + + >>> Version("1.2.3").is_postrelease + False + >>> Version("1.2.3.post1").is_postrelease + True + """ return self.post is not None @property - def is_devrelease(self): + def is_devrelease(self) -> bool: + """Whether this version is a development release. + + >>> Version("1.2.3").is_devrelease + False + >>> Version("1.2.3.dev1").is_devrelease + True + """ return self.dev is not None + @property + def major(self) -> int: + """The first item of :attr:`release` or ``0`` if unavailable. + + >>> Version("1.2.3").major + 1 + """ + return self.release[0] if len(self.release) >= 1 else 0 + + @property + def minor(self) -> int: + """The second item of :attr:`release` or ``0`` if unavailable. + + >>> Version("1.2.3").minor + 2 + >>> Version("1").minor + 0 + """ + return self.release[1] if len(self.release) >= 2 else 0 + + @property + def micro(self) -> int: + """The third item of :attr:`release` or ``0`` if unavailable. + + >>> Version("1.2.3").micro + 3 + >>> Version("1").micro + 0 + """ + return self.release[2] if len(self.release) >= 3 else 0 + + +def _parse_letter_version( + letter: str, number: Union[str, bytes, SupportsInt] +) -> Optional[Tuple[str, int]]: -def _parse_letter_version(letter, number): if letter: # We consider there to be an implicit 0 in a pre-release if there is # not a numeral associated with it. @@ -360,11 +483,13 @@ def _parse_letter_version(letter, number): return letter, int(number) + return None + _local_version_separators = re.compile(r"[\._-]") -def _parse_local_version(local): +def _parse_local_version(local: str) -> Optional[LocalType]: """ Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve"). """ @@ -373,15 +498,24 @@ def _parse_local_version(local): part.lower() if not part.isdigit() else int(part) for part in _local_version_separators.split(local) ) + return None -def _cmpkey(epoch, release, pre, post, dev, local): +def _cmpkey( + epoch: int, + release: Tuple[int, ...], + pre: Optional[Tuple[str, int]], + post: Optional[Tuple[str, int]], + dev: Optional[Tuple[str, int]], + local: Optional[Tuple[SubLocalType]], +) -> CmpKey: + # When we compare a release version, we want to compare it with all of the # trailing zeros removed. So we'll use a reverse the list, drop all the now # leading zeros until we come to something non zero, then take the rest # re-reverse it back into the correct order and make it a tuple and use # that for our sorting key. - release = tuple( + _release = tuple( reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release)))) ) @@ -390,23 +524,31 @@ def _cmpkey(epoch, release, pre, post, dev, local): # if there is not a pre or a post segment. If we have one of those then # the normal sorting rules will handle this case correctly. if pre is None and post is None and dev is not None: - pre = -Infinity + _pre: PrePostDevType = NegativeInfinity # Versions without a pre-release (except as noted above) should sort after # those with one. elif pre is None: - pre = Infinity + _pre = Infinity + else: + _pre = pre # Versions without a post segment should sort before those with one. if post is None: - post = -Infinity + _post: PrePostDevType = NegativeInfinity + + else: + _post = post # Versions without a development segment should sort after those with one. if dev is None: - dev = Infinity + _dev: PrePostDevType = Infinity + + else: + _dev = dev if local is None: # Versions without a local segment should sort before those with one. - local = -Infinity + _local: LocalType = NegativeInfinity else: # Versions with a local segment need that segment parsed to implement # the sorting rules in PEP440. @@ -415,6 +557,8 @@ def _cmpkey(epoch, release, pre, post, dev, local): # - Numeric segments sort numerically # - Shorter versions sort before longer versions when the prefixes # match exactly - local = tuple((i, "") if isinstance(i, int) else (-Infinity, i) for i in local) + _local = tuple( + (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local + ) - return epoch, release, pre, post, dev, local + return epoch, _release, _pre, _post, _dev, _local diff --git a/libs/common/setuptools/_vendor/pyparsing.py b/libs/common/setuptools/_vendor/pyparsing.py deleted file mode 100644 index 4aa30ee6..00000000 --- a/libs/common/setuptools/_vendor/pyparsing.py +++ /dev/null @@ -1,5742 +0,0 @@ -# module pyparsing.py -# -# Copyright (c) 2003-2018 Paul T. McGuire -# -# Permission is hereby granted, free of charge, to any person obtaining -# a copy of this software and associated documentation files (the -# "Software"), to deal in the Software without restriction, including -# without limitation the rights to use, copy, modify, merge, publish, -# distribute, sublicense, and/or sell copies of the Software, and to -# permit persons to whom the Software is furnished to do so, subject to -# the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -# - -__doc__ = \ -""" -pyparsing module - Classes and methods to define and execute parsing grammars -============================================================================= - -The pyparsing module is an alternative approach to creating and executing simple grammars, -vs. the traditional lex/yacc approach, or the use of regular expressions. With pyparsing, you -don't need to learn a new syntax for defining grammars or matching expressions - the parsing module -provides a library of classes that you use to construct the grammar directly in Python. - -Here is a program to parse "Hello, World!" (or any greeting of the form -C{", !"}), built up using L{Word}, L{Literal}, and L{And} elements -(L{'+'} operator gives L{And} expressions, strings are auto-converted to -L{Literal} expressions):: - - from pyparsing import Word, alphas - - # define grammar of a greeting - greet = Word(alphas) + "," + Word(alphas) + "!" - - hello = "Hello, World!" - print (hello, "->", greet.parseString(hello)) - -The program outputs the following:: - - Hello, World! -> ['Hello', ',', 'World', '!'] - -The Python representation of the grammar is quite readable, owing to the self-explanatory -class names, and the use of '+', '|' and '^' operators. - -The L{ParseResults} object returned from L{ParserElement.parseString} can be accessed as a nested list, a dictionary, or an -object with named attributes. - -The pyparsing module handles some of the problems that are typically vexing when writing text parsers: - - extra or missing whitespace (the above program will also handle "Hello,World!", "Hello , World !", etc.) - - quoted strings - - embedded comments - - -Getting Started - ------------------ -Visit the classes L{ParserElement} and L{ParseResults} to see the base classes that most other pyparsing -classes inherit from. Use the docstrings for examples of how to: - - construct literal match expressions from L{Literal} and L{CaselessLiteral} classes - - construct character word-group expressions using the L{Word} class - - see how to create repetitive expressions using L{ZeroOrMore} and L{OneOrMore} classes - - use L{'+'}, L{'|'}, L{'^'}, and L{'&'} operators to combine simple expressions into more complex ones - - associate names with your parsed results using L{ParserElement.setResultsName} - - find some helpful expression short-cuts like L{delimitedList} and L{oneOf} - - find more useful common expressions in the L{pyparsing_common} namespace class -""" - -__version__ = "2.2.1" -__versionTime__ = "18 Sep 2018 00:49 UTC" -__author__ = "Paul McGuire " - -import string -from weakref import ref as wkref -import copy -import sys -import warnings -import re -import sre_constants -import collections -import pprint -import traceback -import types -from datetime import datetime - -try: - from _thread import RLock -except ImportError: - from threading import RLock - -try: - # Python 3 - from collections.abc import Iterable - from collections.abc import MutableMapping -except ImportError: - # Python 2.7 - from collections import Iterable - from collections import MutableMapping - -try: - from collections import OrderedDict as _OrderedDict -except ImportError: - try: - from ordereddict import OrderedDict as _OrderedDict - except ImportError: - _OrderedDict = None - -#~ sys.stderr.write( "testing pyparsing module, version %s, %s\n" % (__version__,__versionTime__ ) ) - -__all__ = [ -'And', 'CaselessKeyword', 'CaselessLiteral', 'CharsNotIn', 'Combine', 'Dict', 'Each', 'Empty', -'FollowedBy', 'Forward', 'GoToColumn', 'Group', 'Keyword', 'LineEnd', 'LineStart', 'Literal', -'MatchFirst', 'NoMatch', 'NotAny', 'OneOrMore', 'OnlyOnce', 'Optional', 'Or', -'ParseBaseException', 'ParseElementEnhance', 'ParseException', 'ParseExpression', 'ParseFatalException', -'ParseResults', 'ParseSyntaxException', 'ParserElement', 'QuotedString', 'RecursiveGrammarException', -'Regex', 'SkipTo', 'StringEnd', 'StringStart', 'Suppress', 'Token', 'TokenConverter', -'White', 'Word', 'WordEnd', 'WordStart', 'ZeroOrMore', -'alphanums', 'alphas', 'alphas8bit', 'anyCloseTag', 'anyOpenTag', 'cStyleComment', 'col', -'commaSeparatedList', 'commonHTMLEntity', 'countedArray', 'cppStyleComment', 'dblQuotedString', -'dblSlashComment', 'delimitedList', 'dictOf', 'downcaseTokens', 'empty', 'hexnums', -'htmlComment', 'javaStyleComment', 'line', 'lineEnd', 'lineStart', 'lineno', -'makeHTMLTags', 'makeXMLTags', 'matchOnlyAtCol', 'matchPreviousExpr', 'matchPreviousLiteral', -'nestedExpr', 'nullDebugAction', 'nums', 'oneOf', 'opAssoc', 'operatorPrecedence', 'printables', -'punc8bit', 'pythonStyleComment', 'quotedString', 'removeQuotes', 'replaceHTMLEntity', -'replaceWith', 'restOfLine', 'sglQuotedString', 'srange', 'stringEnd', -'stringStart', 'traceParseAction', 'unicodeString', 'upcaseTokens', 'withAttribute', -'indentedBlock', 'originalTextFor', 'ungroup', 'infixNotation','locatedExpr', 'withClass', -'CloseMatch', 'tokenMap', 'pyparsing_common', -] - -system_version = tuple(sys.version_info)[:3] -PY_3 = system_version[0] == 3 -if PY_3: - _MAX_INT = sys.maxsize - basestring = str - unichr = chr - _ustr = str - - # build list of single arg builtins, that can be used as parse actions - singleArgBuiltins = [sum, len, sorted, reversed, list, tuple, set, any, all, min, max] - -else: - _MAX_INT = sys.maxint - range = xrange - - def _ustr(obj): - """Drop-in replacement for str(obj) that tries to be Unicode friendly. It first tries - str(obj). If that fails with a UnicodeEncodeError, then it tries unicode(obj). It - then < returns the unicode object | encodes it with the default encoding | ... >. - """ - if isinstance(obj,unicode): - return obj - - try: - # If this works, then _ustr(obj) has the same behaviour as str(obj), so - # it won't break any existing code. - return str(obj) - - except UnicodeEncodeError: - # Else encode it - ret = unicode(obj).encode(sys.getdefaultencoding(), 'xmlcharrefreplace') - xmlcharref = Regex(r'&#\d+;') - xmlcharref.setParseAction(lambda t: '\\u' + hex(int(t[0][2:-1]))[2:]) - return xmlcharref.transformString(ret) - - # build list of single arg builtins, tolerant of Python version, that can be used as parse actions - singleArgBuiltins = [] - import __builtin__ - for fname in "sum len sorted reversed list tuple set any all min max".split(): - try: - singleArgBuiltins.append(getattr(__builtin__,fname)) - except AttributeError: - continue - -_generatorType = type((y for y in range(1))) - -def _xml_escape(data): - """Escape &, <, >, ", ', etc. in a string of data.""" - - # ampersand must be replaced first - from_symbols = '&><"\'' - to_symbols = ('&'+s+';' for s in "amp gt lt quot apos".split()) - for from_,to_ in zip(from_symbols, to_symbols): - data = data.replace(from_, to_) - return data - -class _Constants(object): - pass - -alphas = string.ascii_uppercase + string.ascii_lowercase -nums = "0123456789" -hexnums = nums + "ABCDEFabcdef" -alphanums = alphas + nums -_bslash = chr(92) -printables = "".join(c for c in string.printable if c not in string.whitespace) - -class ParseBaseException(Exception): - """base exception class for all parsing runtime exceptions""" - # Performance tuning: we construct a *lot* of these, so keep this - # constructor as small and fast as possible - def __init__( self, pstr, loc=0, msg=None, elem=None ): - self.loc = loc - if msg is None: - self.msg = pstr - self.pstr = "" - else: - self.msg = msg - self.pstr = pstr - self.parserElement = elem - self.args = (pstr, loc, msg) - - @classmethod - def _from_exception(cls, pe): - """ - internal factory method to simplify creating one type of ParseException - from another - avoids having __init__ signature conflicts among subclasses - """ - return cls(pe.pstr, pe.loc, pe.msg, pe.parserElement) - - def __getattr__( self, aname ): - """supported attributes by name are: - - lineno - returns the line number of the exception text - - col - returns the column number of the exception text - - line - returns the line containing the exception text - """ - if( aname == "lineno" ): - return lineno( self.loc, self.pstr ) - elif( aname in ("col", "column") ): - return col( self.loc, self.pstr ) - elif( aname == "line" ): - return line( self.loc, self.pstr ) - else: - raise AttributeError(aname) - - def __str__( self ): - return "%s (at char %d), (line:%d, col:%d)" % \ - ( self.msg, self.loc, self.lineno, self.column ) - def __repr__( self ): - return _ustr(self) - def markInputline( self, markerString = ">!<" ): - """Extracts the exception line from the input string, and marks - the location of the exception with a special symbol. - """ - line_str = self.line - line_column = self.column - 1 - if markerString: - line_str = "".join((line_str[:line_column], - markerString, line_str[line_column:])) - return line_str.strip() - def __dir__(self): - return "lineno col line".split() + dir(type(self)) - -class ParseException(ParseBaseException): - """ - Exception thrown when parse expressions don't match class; - supported attributes by name are: - - lineno - returns the line number of the exception text - - col - returns the column number of the exception text - - line - returns the line containing the exception text - - Example:: - try: - Word(nums).setName("integer").parseString("ABC") - except ParseException as pe: - print(pe) - print("column: {}".format(pe.col)) - - prints:: - Expected integer (at char 0), (line:1, col:1) - column: 1 - """ - pass - -class ParseFatalException(ParseBaseException): - """user-throwable exception thrown when inconsistent parse content - is found; stops all parsing immediately""" - pass - -class ParseSyntaxException(ParseFatalException): - """just like L{ParseFatalException}, but thrown internally when an - L{ErrorStop} ('-' operator) indicates that parsing is to stop - immediately because an unbacktrackable syntax error has been found""" - pass - -#~ class ReparseException(ParseBaseException): - #~ """Experimental class - parse actions can raise this exception to cause - #~ pyparsing to reparse the input string: - #~ - with a modified input string, and/or - #~ - with a modified start location - #~ Set the values of the ReparseException in the constructor, and raise the - #~ exception in a parse action to cause pyparsing to use the new string/location. - #~ Setting the values as None causes no change to be made. - #~ """ - #~ def __init_( self, newstring, restartLoc ): - #~ self.newParseText = newstring - #~ self.reparseLoc = restartLoc - -class RecursiveGrammarException(Exception): - """exception thrown by L{ParserElement.validate} if the grammar could be improperly recursive""" - def __init__( self, parseElementList ): - self.parseElementTrace = parseElementList - - def __str__( self ): - return "RecursiveGrammarException: %s" % self.parseElementTrace - -class _ParseResultsWithOffset(object): - def __init__(self,p1,p2): - self.tup = (p1,p2) - def __getitem__(self,i): - return self.tup[i] - def __repr__(self): - return repr(self.tup[0]) - def setOffset(self,i): - self.tup = (self.tup[0],i) - -class ParseResults(object): - """ - Structured parse results, to provide multiple means of access to the parsed data: - - as a list (C{len(results)}) - - by list index (C{results[0], results[1]}, etc.) - - by attribute (C{results.} - see L{ParserElement.setResultsName}) - - Example:: - integer = Word(nums) - date_str = (integer.setResultsName("year") + '/' - + integer.setResultsName("month") + '/' - + integer.setResultsName("day")) - # equivalent form: - # date_str = integer("year") + '/' + integer("month") + '/' + integer("day") - - # parseString returns a ParseResults object - result = date_str.parseString("1999/12/31") - - def test(s, fn=repr): - print("%s -> %s" % (s, fn(eval(s)))) - test("list(result)") - test("result[0]") - test("result['month']") - test("result.day") - test("'month' in result") - test("'minutes' in result") - test("result.dump()", str) - prints:: - list(result) -> ['1999', '/', '12', '/', '31'] - result[0] -> '1999' - result['month'] -> '12' - result.day -> '31' - 'month' in result -> True - 'minutes' in result -> False - result.dump() -> ['1999', '/', '12', '/', '31'] - - day: 31 - - month: 12 - - year: 1999 - """ - def __new__(cls, toklist=None, name=None, asList=True, modal=True ): - if isinstance(toklist, cls): - return toklist - retobj = object.__new__(cls) - retobj.__doinit = True - return retobj - - # Performance tuning: we construct a *lot* of these, so keep this - # constructor as small and fast as possible - def __init__( self, toklist=None, name=None, asList=True, modal=True, isinstance=isinstance ): - if self.__doinit: - self.__doinit = False - self.__name = None - self.__parent = None - self.__accumNames = {} - self.__asList = asList - self.__modal = modal - if toklist is None: - toklist = [] - if isinstance(toklist, list): - self.__toklist = toklist[:] - elif isinstance(toklist, _generatorType): - self.__toklist = list(toklist) - else: - self.__toklist = [toklist] - self.__tokdict = dict() - - if name is not None and name: - if not modal: - self.__accumNames[name] = 0 - if isinstance(name,int): - name = _ustr(name) # will always return a str, but use _ustr for consistency - self.__name = name - if not (isinstance(toklist, (type(None), basestring, list)) and toklist in (None,'',[])): - if isinstance(toklist,basestring): - toklist = [ toklist ] - if asList: - if isinstance(toklist,ParseResults): - self[name] = _ParseResultsWithOffset(toklist.copy(),0) - else: - self[name] = _ParseResultsWithOffset(ParseResults(toklist[0]),0) - self[name].__name = name - else: - try: - self[name] = toklist[0] - except (KeyError,TypeError,IndexError): - self[name] = toklist - - def __getitem__( self, i ): - if isinstance( i, (int,slice) ): - return self.__toklist[i] - else: - if i not in self.__accumNames: - return self.__tokdict[i][-1][0] - else: - return ParseResults([ v[0] for v in self.__tokdict[i] ]) - - def __setitem__( self, k, v, isinstance=isinstance ): - if isinstance(v,_ParseResultsWithOffset): - self.__tokdict[k] = self.__tokdict.get(k,list()) + [v] - sub = v[0] - elif isinstance(k,(int,slice)): - self.__toklist[k] = v - sub = v - else: - self.__tokdict[k] = self.__tokdict.get(k,list()) + [_ParseResultsWithOffset(v,0)] - sub = v - if isinstance(sub,ParseResults): - sub.__parent = wkref(self) - - def __delitem__( self, i ): - if isinstance(i,(int,slice)): - mylen = len( self.__toklist ) - del self.__toklist[i] - - # convert int to slice - if isinstance(i, int): - if i < 0: - i += mylen - i = slice(i, i+1) - # get removed indices - removed = list(range(*i.indices(mylen))) - removed.reverse() - # fixup indices in token dictionary - for name,occurrences in self.__tokdict.items(): - for j in removed: - for k, (value, position) in enumerate(occurrences): - occurrences[k] = _ParseResultsWithOffset(value, position - (position > j)) - else: - del self.__tokdict[i] - - def __contains__( self, k ): - return k in self.__tokdict - - def __len__( self ): return len( self.__toklist ) - def __bool__(self): return ( not not self.__toklist ) - __nonzero__ = __bool__ - def __iter__( self ): return iter( self.__toklist ) - def __reversed__( self ): return iter( self.__toklist[::-1] ) - def _iterkeys( self ): - if hasattr(self.__tokdict, "iterkeys"): - return self.__tokdict.iterkeys() - else: - return iter(self.__tokdict) - - def _itervalues( self ): - return (self[k] for k in self._iterkeys()) - - def _iteritems( self ): - return ((k, self[k]) for k in self._iterkeys()) - - if PY_3: - keys = _iterkeys - """Returns an iterator of all named result keys (Python 3.x only).""" - - values = _itervalues - """Returns an iterator of all named result values (Python 3.x only).""" - - items = _iteritems - """Returns an iterator of all named result key-value tuples (Python 3.x only).""" - - else: - iterkeys = _iterkeys - """Returns an iterator of all named result keys (Python 2.x only).""" - - itervalues = _itervalues - """Returns an iterator of all named result values (Python 2.x only).""" - - iteritems = _iteritems - """Returns an iterator of all named result key-value tuples (Python 2.x only).""" - - def keys( self ): - """Returns all named result keys (as a list in Python 2.x, as an iterator in Python 3.x).""" - return list(self.iterkeys()) - - def values( self ): - """Returns all named result values (as a list in Python 2.x, as an iterator in Python 3.x).""" - return list(self.itervalues()) - - def items( self ): - """Returns all named result key-values (as a list of tuples in Python 2.x, as an iterator in Python 3.x).""" - return list(self.iteritems()) - - def haskeys( self ): - """Since keys() returns an iterator, this method is helpful in bypassing - code that looks for the existence of any defined results names.""" - return bool(self.__tokdict) - - def pop( self, *args, **kwargs): - """ - Removes and returns item at specified index (default=C{last}). - Supports both C{list} and C{dict} semantics for C{pop()}. If passed no - argument or an integer argument, it will use C{list} semantics - and pop tokens from the list of parsed tokens. If passed a - non-integer argument (most likely a string), it will use C{dict} - semantics and pop the corresponding value from any defined - results names. A second default return value argument is - supported, just as in C{dict.pop()}. - - Example:: - def remove_first(tokens): - tokens.pop(0) - print(OneOrMore(Word(nums)).parseString("0 123 321")) # -> ['0', '123', '321'] - print(OneOrMore(Word(nums)).addParseAction(remove_first).parseString("0 123 321")) # -> ['123', '321'] - - label = Word(alphas) - patt = label("LABEL") + OneOrMore(Word(nums)) - print(patt.parseString("AAB 123 321").dump()) - - # Use pop() in a parse action to remove named result (note that corresponding value is not - # removed from list form of results) - def remove_LABEL(tokens): - tokens.pop("LABEL") - return tokens - patt.addParseAction(remove_LABEL) - print(patt.parseString("AAB 123 321").dump()) - prints:: - ['AAB', '123', '321'] - - LABEL: AAB - - ['AAB', '123', '321'] - """ - if not args: - args = [-1] - for k,v in kwargs.items(): - if k == 'default': - args = (args[0], v) - else: - raise TypeError("pop() got an unexpected keyword argument '%s'" % k) - if (isinstance(args[0], int) or - len(args) == 1 or - args[0] in self): - index = args[0] - ret = self[index] - del self[index] - return ret - else: - defaultvalue = args[1] - return defaultvalue - - def get(self, key, defaultValue=None): - """ - Returns named result matching the given key, or if there is no - such name, then returns the given C{defaultValue} or C{None} if no - C{defaultValue} is specified. - - Similar to C{dict.get()}. - - Example:: - integer = Word(nums) - date_str = integer("year") + '/' + integer("month") + '/' + integer("day") - - result = date_str.parseString("1999/12/31") - print(result.get("year")) # -> '1999' - print(result.get("hour", "not specified")) # -> 'not specified' - print(result.get("hour")) # -> None - """ - if key in self: - return self[key] - else: - return defaultValue - - def insert( self, index, insStr ): - """ - Inserts new element at location index in the list of parsed tokens. - - Similar to C{list.insert()}. - - Example:: - print(OneOrMore(Word(nums)).parseString("0 123 321")) # -> ['0', '123', '321'] - - # use a parse action to insert the parse location in the front of the parsed results - def insert_locn(locn, tokens): - tokens.insert(0, locn) - print(OneOrMore(Word(nums)).addParseAction(insert_locn).parseString("0 123 321")) # -> [0, '0', '123', '321'] - """ - self.__toklist.insert(index, insStr) - # fixup indices in token dictionary - for name,occurrences in self.__tokdict.items(): - for k, (value, position) in enumerate(occurrences): - occurrences[k] = _ParseResultsWithOffset(value, position + (position > index)) - - def append( self, item ): - """ - Add single element to end of ParseResults list of elements. - - Example:: - print(OneOrMore(Word(nums)).parseString("0 123 321")) # -> ['0', '123', '321'] - - # use a parse action to compute the sum of the parsed integers, and add it to the end - def append_sum(tokens): - tokens.append(sum(map(int, tokens))) - print(OneOrMore(Word(nums)).addParseAction(append_sum).parseString("0 123 321")) # -> ['0', '123', '321', 444] - """ - self.__toklist.append(item) - - def extend( self, itemseq ): - """ - Add sequence of elements to end of ParseResults list of elements. - - Example:: - patt = OneOrMore(Word(alphas)) - - # use a parse action to append the reverse of the matched strings, to make a palindrome - def make_palindrome(tokens): - tokens.extend(reversed([t[::-1] for t in tokens])) - return ''.join(tokens) - print(patt.addParseAction(make_palindrome).parseString("lskdj sdlkjf lksd")) # -> 'lskdjsdlkjflksddsklfjkldsjdksl' - """ - if isinstance(itemseq, ParseResults): - self += itemseq - else: - self.__toklist.extend(itemseq) - - def clear( self ): - """ - Clear all elements and results names. - """ - del self.__toklist[:] - self.__tokdict.clear() - - def __getattr__( self, name ): - try: - return self[name] - except KeyError: - return "" - - if name in self.__tokdict: - if name not in self.__accumNames: - return self.__tokdict[name][-1][0] - else: - return ParseResults([ v[0] for v in self.__tokdict[name] ]) - else: - return "" - - def __add__( self, other ): - ret = self.copy() - ret += other - return ret - - def __iadd__( self, other ): - if other.__tokdict: - offset = len(self.__toklist) - addoffset = lambda a: offset if a<0 else a+offset - otheritems = other.__tokdict.items() - otherdictitems = [(k, _ParseResultsWithOffset(v[0],addoffset(v[1])) ) - for (k,vlist) in otheritems for v in vlist] - for k,v in otherdictitems: - self[k] = v - if isinstance(v[0],ParseResults): - v[0].__parent = wkref(self) - - self.__toklist += other.__toklist - self.__accumNames.update( other.__accumNames ) - return self - - def __radd__(self, other): - if isinstance(other,int) and other == 0: - # useful for merging many ParseResults using sum() builtin - return self.copy() - else: - # this may raise a TypeError - so be it - return other + self - - def __repr__( self ): - return "(%s, %s)" % ( repr( self.__toklist ), repr( self.__tokdict ) ) - - def __str__( self ): - return '[' + ', '.join(_ustr(i) if isinstance(i, ParseResults) else repr(i) for i in self.__toklist) + ']' - - def _asStringList( self, sep='' ): - out = [] - for item in self.__toklist: - if out and sep: - out.append(sep) - if isinstance( item, ParseResults ): - out += item._asStringList() - else: - out.append( _ustr(item) ) - return out - - def asList( self ): - """ - Returns the parse results as a nested list of matching tokens, all converted to strings. - - Example:: - patt = OneOrMore(Word(alphas)) - result = patt.parseString("sldkj lsdkj sldkj") - # even though the result prints in string-like form, it is actually a pyparsing ParseResults - print(type(result), result) # -> ['sldkj', 'lsdkj', 'sldkj'] - - # Use asList() to create an actual list - result_list = result.asList() - print(type(result_list), result_list) # -> ['sldkj', 'lsdkj', 'sldkj'] - """ - return [res.asList() if isinstance(res,ParseResults) else res for res in self.__toklist] - - def asDict( self ): - """ - Returns the named parse results as a nested dictionary. - - Example:: - integer = Word(nums) - date_str = integer("year") + '/' + integer("month") + '/' + integer("day") - - result = date_str.parseString('12/31/1999') - print(type(result), repr(result)) # -> (['12', '/', '31', '/', '1999'], {'day': [('1999', 4)], 'year': [('12', 0)], 'month': [('31', 2)]}) - - result_dict = result.asDict() - print(type(result_dict), repr(result_dict)) # -> {'day': '1999', 'year': '12', 'month': '31'} - - # even though a ParseResults supports dict-like access, sometime you just need to have a dict - import json - print(json.dumps(result)) # -> Exception: TypeError: ... is not JSON serializable - print(json.dumps(result.asDict())) # -> {"month": "31", "day": "1999", "year": "12"} - """ - if PY_3: - item_fn = self.items - else: - item_fn = self.iteritems - - def toItem(obj): - if isinstance(obj, ParseResults): - if obj.haskeys(): - return obj.asDict() - else: - return [toItem(v) for v in obj] - else: - return obj - - return dict((k,toItem(v)) for k,v in item_fn()) - - def copy( self ): - """ - Returns a new copy of a C{ParseResults} object. - """ - ret = ParseResults( self.__toklist ) - ret.__tokdict = self.__tokdict.copy() - ret.__parent = self.__parent - ret.__accumNames.update( self.__accumNames ) - ret.__name = self.__name - return ret - - def asXML( self, doctag=None, namedItemsOnly=False, indent="", formatted=True ): - """ - (Deprecated) Returns the parse results as XML. Tags are created for tokens and lists that have defined results names. - """ - nl = "\n" - out = [] - namedItems = dict((v[1],k) for (k,vlist) in self.__tokdict.items() - for v in vlist) - nextLevelIndent = indent + " " - - # collapse out indents if formatting is not desired - if not formatted: - indent = "" - nextLevelIndent = "" - nl = "" - - selfTag = None - if doctag is not None: - selfTag = doctag - else: - if self.__name: - selfTag = self.__name - - if not selfTag: - if namedItemsOnly: - return "" - else: - selfTag = "ITEM" - - out += [ nl, indent, "<", selfTag, ">" ] - - for i,res in enumerate(self.__toklist): - if isinstance(res,ParseResults): - if i in namedItems: - out += [ res.asXML(namedItems[i], - namedItemsOnly and doctag is None, - nextLevelIndent, - formatted)] - else: - out += [ res.asXML(None, - namedItemsOnly and doctag is None, - nextLevelIndent, - formatted)] - else: - # individual token, see if there is a name for it - resTag = None - if i in namedItems: - resTag = namedItems[i] - if not resTag: - if namedItemsOnly: - continue - else: - resTag = "ITEM" - xmlBodyText = _xml_escape(_ustr(res)) - out += [ nl, nextLevelIndent, "<", resTag, ">", - xmlBodyText, - "" ] - - out += [ nl, indent, "" ] - return "".join(out) - - def __lookup(self,sub): - for k,vlist in self.__tokdict.items(): - for v,loc in vlist: - if sub is v: - return k - return None - - def getName(self): - r""" - Returns the results name for this token expression. Useful when several - different expressions might match at a particular location. - - Example:: - integer = Word(nums) - ssn_expr = Regex(r"\d\d\d-\d\d-\d\d\d\d") - house_number_expr = Suppress('#') + Word(nums, alphanums) - user_data = (Group(house_number_expr)("house_number") - | Group(ssn_expr)("ssn") - | Group(integer)("age")) - user_info = OneOrMore(user_data) - - result = user_info.parseString("22 111-22-3333 #221B") - for item in result: - print(item.getName(), ':', item[0]) - prints:: - age : 22 - ssn : 111-22-3333 - house_number : 221B - """ - if self.__name: - return self.__name - elif self.__parent: - par = self.__parent() - if par: - return par.__lookup(self) - else: - return None - elif (len(self) == 1 and - len(self.__tokdict) == 1 and - next(iter(self.__tokdict.values()))[0][1] in (0,-1)): - return next(iter(self.__tokdict.keys())) - else: - return None - - def dump(self, indent='', depth=0, full=True): - """ - Diagnostic method for listing out the contents of a C{ParseResults}. - Accepts an optional C{indent} argument so that this string can be embedded - in a nested display of other data. - - Example:: - integer = Word(nums) - date_str = integer("year") + '/' + integer("month") + '/' + integer("day") - - result = date_str.parseString('12/31/1999') - print(result.dump()) - prints:: - ['12', '/', '31', '/', '1999'] - - day: 1999 - - month: 31 - - year: 12 - """ - out = [] - NL = '\n' - out.append( indent+_ustr(self.asList()) ) - if full: - if self.haskeys(): - items = sorted((str(k), v) for k,v in self.items()) - for k,v in items: - if out: - out.append(NL) - out.append( "%s%s- %s: " % (indent,(' '*depth), k) ) - if isinstance(v,ParseResults): - if v: - out.append( v.dump(indent,depth+1) ) - else: - out.append(_ustr(v)) - else: - out.append(repr(v)) - elif any(isinstance(vv,ParseResults) for vv in self): - v = self - for i,vv in enumerate(v): - if isinstance(vv,ParseResults): - out.append("\n%s%s[%d]:\n%s%s%s" % (indent,(' '*(depth)),i,indent,(' '*(depth+1)),vv.dump(indent,depth+1) )) - else: - out.append("\n%s%s[%d]:\n%s%s%s" % (indent,(' '*(depth)),i,indent,(' '*(depth+1)),_ustr(vv))) - - return "".join(out) - - def pprint(self, *args, **kwargs): - """ - Pretty-printer for parsed results as a list, using the C{pprint} module. - Accepts additional positional or keyword args as defined for the - C{pprint.pprint} method. (U{http://docs.python.org/3/library/pprint.html#pprint.pprint}) - - Example:: - ident = Word(alphas, alphanums) - num = Word(nums) - func = Forward() - term = ident | num | Group('(' + func + ')') - func <<= ident + Group(Optional(delimitedList(term))) - result = func.parseString("fna a,b,(fnb c,d,200),100") - result.pprint(width=40) - prints:: - ['fna', - ['a', - 'b', - ['(', 'fnb', ['c', 'd', '200'], ')'], - '100']] - """ - pprint.pprint(self.asList(), *args, **kwargs) - - # add support for pickle protocol - def __getstate__(self): - return ( self.__toklist, - ( self.__tokdict.copy(), - self.__parent is not None and self.__parent() or None, - self.__accumNames, - self.__name ) ) - - def __setstate__(self,state): - self.__toklist = state[0] - (self.__tokdict, - par, - inAccumNames, - self.__name) = state[1] - self.__accumNames = {} - self.__accumNames.update(inAccumNames) - if par is not None: - self.__parent = wkref(par) - else: - self.__parent = None - - def __getnewargs__(self): - return self.__toklist, self.__name, self.__asList, self.__modal - - def __dir__(self): - return (dir(type(self)) + list(self.keys())) - -MutableMapping.register(ParseResults) - -def col (loc,strg): - """Returns current column within a string, counting newlines as line separators. - The first column is number 1. - - Note: the default parsing behavior is to expand tabs in the input string - before starting the parsing process. See L{I{ParserElement.parseString}} for more information - on parsing strings containing C{}s, and suggested methods to maintain a - consistent view of the parsed string, the parse location, and line and column - positions within the parsed string. - """ - s = strg - return 1 if 0} for more information - on parsing strings containing C{}s, and suggested methods to maintain a - consistent view of the parsed string, the parse location, and line and column - positions within the parsed string. - """ - return strg.count("\n",0,loc) + 1 - -def line( loc, strg ): - """Returns the line of text containing loc within a string, counting newlines as line separators. - """ - lastCR = strg.rfind("\n", 0, loc) - nextCR = strg.find("\n", loc) - if nextCR >= 0: - return strg[lastCR+1:nextCR] - else: - return strg[lastCR+1:] - -def _defaultStartDebugAction( instring, loc, expr ): - print (("Match " + _ustr(expr) + " at loc " + _ustr(loc) + "(%d,%d)" % ( lineno(loc,instring), col(loc,instring) ))) - -def _defaultSuccessDebugAction( instring, startloc, endloc, expr, toks ): - print ("Matched " + _ustr(expr) + " -> " + str(toks.asList())) - -def _defaultExceptionDebugAction( instring, loc, expr, exc ): - print ("Exception raised:" + _ustr(exc)) - -def nullDebugAction(*args): - """'Do-nothing' debug action, to suppress debugging output during parsing.""" - pass - -# Only works on Python 3.x - nonlocal is toxic to Python 2 installs -#~ 'decorator to trim function calls to match the arity of the target' -#~ def _trim_arity(func, maxargs=3): - #~ if func in singleArgBuiltins: - #~ return lambda s,l,t: func(t) - #~ limit = 0 - #~ foundArity = False - #~ def wrapper(*args): - #~ nonlocal limit,foundArity - #~ while 1: - #~ try: - #~ ret = func(*args[limit:]) - #~ foundArity = True - #~ return ret - #~ except TypeError: - #~ if limit == maxargs or foundArity: - #~ raise - #~ limit += 1 - #~ continue - #~ return wrapper - -# this version is Python 2.x-3.x cross-compatible -'decorator to trim function calls to match the arity of the target' -def _trim_arity(func, maxargs=2): - if func in singleArgBuiltins: - return lambda s,l,t: func(t) - limit = [0] - foundArity = [False] - - # traceback return data structure changed in Py3.5 - normalize back to plain tuples - if system_version[:2] >= (3,5): - def extract_stack(limit=0): - # special handling for Python 3.5.0 - extra deep call stack by 1 - offset = -3 if system_version == (3,5,0) else -2 - frame_summary = traceback.extract_stack(limit=-offset+limit-1)[offset] - return [frame_summary[:2]] - def extract_tb(tb, limit=0): - frames = traceback.extract_tb(tb, limit=limit) - frame_summary = frames[-1] - return [frame_summary[:2]] - else: - extract_stack = traceback.extract_stack - extract_tb = traceback.extract_tb - - # synthesize what would be returned by traceback.extract_stack at the call to - # user's parse action 'func', so that we don't incur call penalty at parse time - - LINE_DIFF = 6 - # IF ANY CODE CHANGES, EVEN JUST COMMENTS OR BLANK LINES, BETWEEN THE NEXT LINE AND - # THE CALL TO FUNC INSIDE WRAPPER, LINE_DIFF MUST BE MODIFIED!!!! - this_line = extract_stack(limit=2)[-1] - pa_call_line_synth = (this_line[0], this_line[1]+LINE_DIFF) - - def wrapper(*args): - while 1: - try: - ret = func(*args[limit[0]:]) - foundArity[0] = True - return ret - except TypeError: - # re-raise TypeErrors if they did not come from our arity testing - if foundArity[0]: - raise - else: - try: - tb = sys.exc_info()[-1] - if not extract_tb(tb, limit=2)[-1][:2] == pa_call_line_synth: - raise - finally: - del tb - - if limit[0] <= maxargs: - limit[0] += 1 - continue - raise - - # copy func name to wrapper for sensible debug output - func_name = "" - try: - func_name = getattr(func, '__name__', - getattr(func, '__class__').__name__) - except Exception: - func_name = str(func) - wrapper.__name__ = func_name - - return wrapper - -class ParserElement(object): - """Abstract base level parser element class.""" - DEFAULT_WHITE_CHARS = " \n\t\r" - verbose_stacktrace = False - - @staticmethod - def setDefaultWhitespaceChars( chars ): - r""" - Overrides the default whitespace chars - - Example:: - # default whitespace chars are space, and newline - OneOrMore(Word(alphas)).parseString("abc def\nghi jkl") # -> ['abc', 'def', 'ghi', 'jkl'] - - # change to just treat newline as significant - ParserElement.setDefaultWhitespaceChars(" \t") - OneOrMore(Word(alphas)).parseString("abc def\nghi jkl") # -> ['abc', 'def'] - """ - ParserElement.DEFAULT_WHITE_CHARS = chars - - @staticmethod - def inlineLiteralsUsing(cls): - """ - Set class to be used for inclusion of string literals into a parser. - - Example:: - # default literal class used is Literal - integer = Word(nums) - date_str = integer("year") + '/' + integer("month") + '/' + integer("day") - - date_str.parseString("1999/12/31") # -> ['1999', '/', '12', '/', '31'] - - - # change to Suppress - ParserElement.inlineLiteralsUsing(Suppress) - date_str = integer("year") + '/' + integer("month") + '/' + integer("day") - - date_str.parseString("1999/12/31") # -> ['1999', '12', '31'] - """ - ParserElement._literalStringClass = cls - - def __init__( self, savelist=False ): - self.parseAction = list() - self.failAction = None - #~ self.name = "" # don't define self.name, let subclasses try/except upcall - self.strRepr = None - self.resultsName = None - self.saveAsList = savelist - self.skipWhitespace = True - self.whiteChars = ParserElement.DEFAULT_WHITE_CHARS - self.copyDefaultWhiteChars = True - self.mayReturnEmpty = False # used when checking for left-recursion - self.keepTabs = False - self.ignoreExprs = list() - self.debug = False - self.streamlined = False - self.mayIndexError = True # used to optimize exception handling for subclasses that don't advance parse index - self.errmsg = "" - self.modalResults = True # used to mark results names as modal (report only last) or cumulative (list all) - self.debugActions = ( None, None, None ) #custom debug actions - self.re = None - self.callPreparse = True # used to avoid redundant calls to preParse - self.callDuringTry = False - - def copy( self ): - """ - Make a copy of this C{ParserElement}. Useful for defining different parse actions - for the same parsing pattern, using copies of the original parse element. - - Example:: - integer = Word(nums).setParseAction(lambda toks: int(toks[0])) - integerK = integer.copy().addParseAction(lambda toks: toks[0]*1024) + Suppress("K") - integerM = integer.copy().addParseAction(lambda toks: toks[0]*1024*1024) + Suppress("M") - - print(OneOrMore(integerK | integerM | integer).parseString("5K 100 640K 256M")) - prints:: - [5120, 100, 655360, 268435456] - Equivalent form of C{expr.copy()} is just C{expr()}:: - integerM = integer().addParseAction(lambda toks: toks[0]*1024*1024) + Suppress("M") - """ - cpy = copy.copy( self ) - cpy.parseAction = self.parseAction[:] - cpy.ignoreExprs = self.ignoreExprs[:] - if self.copyDefaultWhiteChars: - cpy.whiteChars = ParserElement.DEFAULT_WHITE_CHARS - return cpy - - def setName( self, name ): - """ - Define name for this expression, makes debugging and exception messages clearer. - - Example:: - Word(nums).parseString("ABC") # -> Exception: Expected W:(0123...) (at char 0), (line:1, col:1) - Word(nums).setName("integer").parseString("ABC") # -> Exception: Expected integer (at char 0), (line:1, col:1) - """ - self.name = name - self.errmsg = "Expected " + self.name - if hasattr(self,"exception"): - self.exception.msg = self.errmsg - return self - - def setResultsName( self, name, listAllMatches=False ): - """ - Define name for referencing matching tokens as a nested attribute - of the returned parse results. - NOTE: this returns a *copy* of the original C{ParserElement} object; - this is so that the client can define a basic element, such as an - integer, and reference it in multiple places with different names. - - You can also set results names using the abbreviated syntax, - C{expr("name")} in place of C{expr.setResultsName("name")} - - see L{I{__call__}<__call__>}. - - Example:: - date_str = (integer.setResultsName("year") + '/' - + integer.setResultsName("month") + '/' - + integer.setResultsName("day")) - - # equivalent form: - date_str = integer("year") + '/' + integer("month") + '/' + integer("day") - """ - newself = self.copy() - if name.endswith("*"): - name = name[:-1] - listAllMatches=True - newself.resultsName = name - newself.modalResults = not listAllMatches - return newself - - def setBreak(self,breakFlag = True): - """Method to invoke the Python pdb debugger when this element is - about to be parsed. Set C{breakFlag} to True to enable, False to - disable. - """ - if breakFlag: - _parseMethod = self._parse - def breaker(instring, loc, doActions=True, callPreParse=True): - import pdb - pdb.set_trace() - return _parseMethod( instring, loc, doActions, callPreParse ) - breaker._originalParseMethod = _parseMethod - self._parse = breaker - else: - if hasattr(self._parse,"_originalParseMethod"): - self._parse = self._parse._originalParseMethod - return self - - def setParseAction( self, *fns, **kwargs ): - """ - Define one or more actions to perform when successfully matching parse element definition. - Parse action fn is a callable method with 0-3 arguments, called as C{fn(s,loc,toks)}, - C{fn(loc,toks)}, C{fn(toks)}, or just C{fn()}, where: - - s = the original string being parsed (see note below) - - loc = the location of the matching substring - - toks = a list of the matched tokens, packaged as a C{L{ParseResults}} object - If the functions in fns modify the tokens, they can return them as the return - value from fn, and the modified list of tokens will replace the original. - Otherwise, fn does not need to return any value. - - Optional keyword arguments: - - callDuringTry = (default=C{False}) indicate if parse action should be run during lookaheads and alternate testing - - Note: the default parsing behavior is to expand tabs in the input string - before starting the parsing process. See L{I{parseString}} for more information - on parsing strings containing C{}s, and suggested methods to maintain a - consistent view of the parsed string, the parse location, and line and column - positions within the parsed string. - - Example:: - integer = Word(nums) - date_str = integer + '/' + integer + '/' + integer - - date_str.parseString("1999/12/31") # -> ['1999', '/', '12', '/', '31'] - - # use parse action to convert to ints at parse time - integer = Word(nums).setParseAction(lambda toks: int(toks[0])) - date_str = integer + '/' + integer + '/' + integer - - # note that integer fields are now ints, not strings - date_str.parseString("1999/12/31") # -> [1999, '/', 12, '/', 31] - """ - self.parseAction = list(map(_trim_arity, list(fns))) - self.callDuringTry = kwargs.get("callDuringTry", False) - return self - - def addParseAction( self, *fns, **kwargs ): - """ - Add one or more parse actions to expression's list of parse actions. See L{I{setParseAction}}. - - See examples in L{I{copy}}. - """ - self.parseAction += list(map(_trim_arity, list(fns))) - self.callDuringTry = self.callDuringTry or kwargs.get("callDuringTry", False) - return self - - def addCondition(self, *fns, **kwargs): - """Add a boolean predicate function to expression's list of parse actions. See - L{I{setParseAction}} for function call signatures. Unlike C{setParseAction}, - functions passed to C{addCondition} need to return boolean success/fail of the condition. - - Optional keyword arguments: - - message = define a custom message to be used in the raised exception - - fatal = if True, will raise ParseFatalException to stop parsing immediately; otherwise will raise ParseException - - Example:: - integer = Word(nums).setParseAction(lambda toks: int(toks[0])) - year_int = integer.copy() - year_int.addCondition(lambda toks: toks[0] >= 2000, message="Only support years 2000 and later") - date_str = year_int + '/' + integer + '/' + integer - - result = date_str.parseString("1999/12/31") # -> Exception: Only support years 2000 and later (at char 0), (line:1, col:1) - """ - msg = kwargs.get("message", "failed user-defined condition") - exc_type = ParseFatalException if kwargs.get("fatal", False) else ParseException - for fn in fns: - def pa(s,l,t): - if not bool(_trim_arity(fn)(s,l,t)): - raise exc_type(s,l,msg) - self.parseAction.append(pa) - self.callDuringTry = self.callDuringTry or kwargs.get("callDuringTry", False) - return self - - def setFailAction( self, fn ): - """Define action to perform if parsing fails at this expression. - Fail acton fn is a callable function that takes the arguments - C{fn(s,loc,expr,err)} where: - - s = string being parsed - - loc = location where expression match was attempted and failed - - expr = the parse expression that failed - - err = the exception thrown - The function returns no value. It may throw C{L{ParseFatalException}} - if it is desired to stop parsing immediately.""" - self.failAction = fn - return self - - def _skipIgnorables( self, instring, loc ): - exprsFound = True - while exprsFound: - exprsFound = False - for e in self.ignoreExprs: - try: - while 1: - loc,dummy = e._parse( instring, loc ) - exprsFound = True - except ParseException: - pass - return loc - - def preParse( self, instring, loc ): - if self.ignoreExprs: - loc = self._skipIgnorables( instring, loc ) - - if self.skipWhitespace: - wt = self.whiteChars - instrlen = len(instring) - while loc < instrlen and instring[loc] in wt: - loc += 1 - - return loc - - def parseImpl( self, instring, loc, doActions=True ): - return loc, [] - - def postParse( self, instring, loc, tokenlist ): - return tokenlist - - #~ @profile - def _parseNoCache( self, instring, loc, doActions=True, callPreParse=True ): - debugging = ( self.debug ) #and doActions ) - - if debugging or self.failAction: - #~ print ("Match",self,"at loc",loc,"(%d,%d)" % ( lineno(loc,instring), col(loc,instring) )) - if (self.debugActions[0] ): - self.debugActions[0]( instring, loc, self ) - if callPreParse and self.callPreparse: - preloc = self.preParse( instring, loc ) - else: - preloc = loc - tokensStart = preloc - try: - try: - loc,tokens = self.parseImpl( instring, preloc, doActions ) - except IndexError: - raise ParseException( instring, len(instring), self.errmsg, self ) - except ParseBaseException as err: - #~ print ("Exception raised:", err) - if self.debugActions[2]: - self.debugActions[2]( instring, tokensStart, self, err ) - if self.failAction: - self.failAction( instring, tokensStart, self, err ) - raise - else: - if callPreParse and self.callPreparse: - preloc = self.preParse( instring, loc ) - else: - preloc = loc - tokensStart = preloc - if self.mayIndexError or preloc >= len(instring): - try: - loc,tokens = self.parseImpl( instring, preloc, doActions ) - except IndexError: - raise ParseException( instring, len(instring), self.errmsg, self ) - else: - loc,tokens = self.parseImpl( instring, preloc, doActions ) - - tokens = self.postParse( instring, loc, tokens ) - - retTokens = ParseResults( tokens, self.resultsName, asList=self.saveAsList, modal=self.modalResults ) - if self.parseAction and (doActions or self.callDuringTry): - if debugging: - try: - for fn in self.parseAction: - tokens = fn( instring, tokensStart, retTokens ) - if tokens is not None: - retTokens = ParseResults( tokens, - self.resultsName, - asList=self.saveAsList and isinstance(tokens,(ParseResults,list)), - modal=self.modalResults ) - except ParseBaseException as err: - #~ print "Exception raised in user parse action:", err - if (self.debugActions[2] ): - self.debugActions[2]( instring, tokensStart, self, err ) - raise - else: - for fn in self.parseAction: - tokens = fn( instring, tokensStart, retTokens ) - if tokens is not None: - retTokens = ParseResults( tokens, - self.resultsName, - asList=self.saveAsList and isinstance(tokens,(ParseResults,list)), - modal=self.modalResults ) - if debugging: - #~ print ("Matched",self,"->",retTokens.asList()) - if (self.debugActions[1] ): - self.debugActions[1]( instring, tokensStart, loc, self, retTokens ) - - return loc, retTokens - - def tryParse( self, instring, loc ): - try: - return self._parse( instring, loc, doActions=False )[0] - except ParseFatalException: - raise ParseException( instring, loc, self.errmsg, self) - - def canParseNext(self, instring, loc): - try: - self.tryParse(instring, loc) - except (ParseException, IndexError): - return False - else: - return True - - class _UnboundedCache(object): - def __init__(self): - cache = {} - self.not_in_cache = not_in_cache = object() - - def get(self, key): - return cache.get(key, not_in_cache) - - def set(self, key, value): - cache[key] = value - - def clear(self): - cache.clear() - - def cache_len(self): - return len(cache) - - self.get = types.MethodType(get, self) - self.set = types.MethodType(set, self) - self.clear = types.MethodType(clear, self) - self.__len__ = types.MethodType(cache_len, self) - - if _OrderedDict is not None: - class _FifoCache(object): - def __init__(self, size): - self.not_in_cache = not_in_cache = object() - - cache = _OrderedDict() - - def get(self, key): - return cache.get(key, not_in_cache) - - def set(self, key, value): - cache[key] = value - while len(cache) > size: - try: - cache.popitem(False) - except KeyError: - pass - - def clear(self): - cache.clear() - - def cache_len(self): - return len(cache) - - self.get = types.MethodType(get, self) - self.set = types.MethodType(set, self) - self.clear = types.MethodType(clear, self) - self.__len__ = types.MethodType(cache_len, self) - - else: - class _FifoCache(object): - def __init__(self, size): - self.not_in_cache = not_in_cache = object() - - cache = {} - key_fifo = collections.deque([], size) - - def get(self, key): - return cache.get(key, not_in_cache) - - def set(self, key, value): - cache[key] = value - while len(key_fifo) > size: - cache.pop(key_fifo.popleft(), None) - key_fifo.append(key) - - def clear(self): - cache.clear() - key_fifo.clear() - - def cache_len(self): - return len(cache) - - self.get = types.MethodType(get, self) - self.set = types.MethodType(set, self) - self.clear = types.MethodType(clear, self) - self.__len__ = types.MethodType(cache_len, self) - - # argument cache for optimizing repeated calls when backtracking through recursive expressions - packrat_cache = {} # this is set later by enabledPackrat(); this is here so that resetCache() doesn't fail - packrat_cache_lock = RLock() - packrat_cache_stats = [0, 0] - - # this method gets repeatedly called during backtracking with the same arguments - - # we can cache these arguments and save ourselves the trouble of re-parsing the contained expression - def _parseCache( self, instring, loc, doActions=True, callPreParse=True ): - HIT, MISS = 0, 1 - lookup = (self, instring, loc, callPreParse, doActions) - with ParserElement.packrat_cache_lock: - cache = ParserElement.packrat_cache - value = cache.get(lookup) - if value is cache.not_in_cache: - ParserElement.packrat_cache_stats[MISS] += 1 - try: - value = self._parseNoCache(instring, loc, doActions, callPreParse) - except ParseBaseException as pe: - # cache a copy of the exception, without the traceback - cache.set(lookup, pe.__class__(*pe.args)) - raise - else: - cache.set(lookup, (value[0], value[1].copy())) - return value - else: - ParserElement.packrat_cache_stats[HIT] += 1 - if isinstance(value, Exception): - raise value - return (value[0], value[1].copy()) - - _parse = _parseNoCache - - @staticmethod - def resetCache(): - ParserElement.packrat_cache.clear() - ParserElement.packrat_cache_stats[:] = [0] * len(ParserElement.packrat_cache_stats) - - _packratEnabled = False - @staticmethod - def enablePackrat(cache_size_limit=128): - """Enables "packrat" parsing, which adds memoizing to the parsing logic. - Repeated parse attempts at the same string location (which happens - often in many complex grammars) can immediately return a cached value, - instead of re-executing parsing/validating code. Memoizing is done of - both valid results and parsing exceptions. - - Parameters: - - cache_size_limit - (default=C{128}) - if an integer value is provided - will limit the size of the packrat cache; if None is passed, then - the cache size will be unbounded; if 0 is passed, the cache will - be effectively disabled. - - This speedup may break existing programs that use parse actions that - have side-effects. For this reason, packrat parsing is disabled when - you first import pyparsing. To activate the packrat feature, your - program must call the class method C{ParserElement.enablePackrat()}. If - your program uses C{psyco} to "compile as you go", you must call - C{enablePackrat} before calling C{psyco.full()}. If you do not do this, - Python will crash. For best results, call C{enablePackrat()} immediately - after importing pyparsing. - - Example:: - import pyparsing - pyparsing.ParserElement.enablePackrat() - """ - if not ParserElement._packratEnabled: - ParserElement._packratEnabled = True - if cache_size_limit is None: - ParserElement.packrat_cache = ParserElement._UnboundedCache() - else: - ParserElement.packrat_cache = ParserElement._FifoCache(cache_size_limit) - ParserElement._parse = ParserElement._parseCache - - def parseString( self, instring, parseAll=False ): - """ - Execute the parse expression with the given string. - This is the main interface to the client code, once the complete - expression has been built. - - If you want the grammar to require that the entire input string be - successfully parsed, then set C{parseAll} to True (equivalent to ending - the grammar with C{L{StringEnd()}}). - - Note: C{parseString} implicitly calls C{expandtabs()} on the input string, - in order to report proper column numbers in parse actions. - If the input string contains tabs and - the grammar uses parse actions that use the C{loc} argument to index into the - string being parsed, you can ensure you have a consistent view of the input - string by: - - calling C{parseWithTabs} on your grammar before calling C{parseString} - (see L{I{parseWithTabs}}) - - define your parse action using the full C{(s,loc,toks)} signature, and - reference the input string using the parse action's C{s} argument - - explictly expand the tabs in your input string before calling - C{parseString} - - Example:: - Word('a').parseString('aaaaabaaa') # -> ['aaaaa'] - Word('a').parseString('aaaaabaaa', parseAll=True) # -> Exception: Expected end of text - """ - ParserElement.resetCache() - if not self.streamlined: - self.streamline() - #~ self.saveAsList = True - for e in self.ignoreExprs: - e.streamline() - if not self.keepTabs: - instring = instring.expandtabs() - try: - loc, tokens = self._parse( instring, 0 ) - if parseAll: - loc = self.preParse( instring, loc ) - se = Empty() + StringEnd() - se._parse( instring, loc ) - except ParseBaseException as exc: - if ParserElement.verbose_stacktrace: - raise - else: - # catch and re-raise exception from here, clears out pyparsing internal stack trace - raise exc - else: - return tokens - - def scanString( self, instring, maxMatches=_MAX_INT, overlap=False ): - """ - Scan the input string for expression matches. Each match will return the - matching tokens, start location, and end location. May be called with optional - C{maxMatches} argument, to clip scanning after 'n' matches are found. If - C{overlap} is specified, then overlapping matches will be reported. - - Note that the start and end locations are reported relative to the string - being parsed. See L{I{parseString}} for more information on parsing - strings with embedded tabs. - - Example:: - source = "sldjf123lsdjjkf345sldkjf879lkjsfd987" - print(source) - for tokens,start,end in Word(alphas).scanString(source): - print(' '*start + '^'*(end-start)) - print(' '*start + tokens[0]) - - prints:: - - sldjf123lsdjjkf345sldkjf879lkjsfd987 - ^^^^^ - sldjf - ^^^^^^^ - lsdjjkf - ^^^^^^ - sldkjf - ^^^^^^ - lkjsfd - """ - if not self.streamlined: - self.streamline() - for e in self.ignoreExprs: - e.streamline() - - if not self.keepTabs: - instring = _ustr(instring).expandtabs() - instrlen = len(instring) - loc = 0 - preparseFn = self.preParse - parseFn = self._parse - ParserElement.resetCache() - matches = 0 - try: - while loc <= instrlen and matches < maxMatches: - try: - preloc = preparseFn( instring, loc ) - nextLoc,tokens = parseFn( instring, preloc, callPreParse=False ) - except ParseException: - loc = preloc+1 - else: - if nextLoc > loc: - matches += 1 - yield tokens, preloc, nextLoc - if overlap: - nextloc = preparseFn( instring, loc ) - if nextloc > loc: - loc = nextLoc - else: - loc += 1 - else: - loc = nextLoc - else: - loc = preloc+1 - except ParseBaseException as exc: - if ParserElement.verbose_stacktrace: - raise - else: - # catch and re-raise exception from here, clears out pyparsing internal stack trace - raise exc - - def transformString( self, instring ): - """ - Extension to C{L{scanString}}, to modify matching text with modified tokens that may - be returned from a parse action. To use C{transformString}, define a grammar and - attach a parse action to it that modifies the returned token list. - Invoking C{transformString()} on a target string will then scan for matches, - and replace the matched text patterns according to the logic in the parse - action. C{transformString()} returns the resulting transformed string. - - Example:: - wd = Word(alphas) - wd.setParseAction(lambda toks: toks[0].title()) - - print(wd.transformString("now is the winter of our discontent made glorious summer by this sun of york.")) - Prints:: - Now Is The Winter Of Our Discontent Made Glorious Summer By This Sun Of York. - """ - out = [] - lastE = 0 - # force preservation of s, to minimize unwanted transformation of string, and to - # keep string locs straight between transformString and scanString - self.keepTabs = True - try: - for t,s,e in self.scanString( instring ): - out.append( instring[lastE:s] ) - if t: - if isinstance(t,ParseResults): - out += t.asList() - elif isinstance(t,list): - out += t - else: - out.append(t) - lastE = e - out.append(instring[lastE:]) - out = [o for o in out if o] - return "".join(map(_ustr,_flatten(out))) - except ParseBaseException as exc: - if ParserElement.verbose_stacktrace: - raise - else: - # catch and re-raise exception from here, clears out pyparsing internal stack trace - raise exc - - def searchString( self, instring, maxMatches=_MAX_INT ): - """ - Another extension to C{L{scanString}}, simplifying the access to the tokens found - to match the given parse expression. May be called with optional - C{maxMatches} argument, to clip searching after 'n' matches are found. - - Example:: - # a capitalized word starts with an uppercase letter, followed by zero or more lowercase letters - cap_word = Word(alphas.upper(), alphas.lower()) - - print(cap_word.searchString("More than Iron, more than Lead, more than Gold I need Electricity")) - - # the sum() builtin can be used to merge results into a single ParseResults object - print(sum(cap_word.searchString("More than Iron, more than Lead, more than Gold I need Electricity"))) - prints:: - [['More'], ['Iron'], ['Lead'], ['Gold'], ['I'], ['Electricity']] - ['More', 'Iron', 'Lead', 'Gold', 'I', 'Electricity'] - """ - try: - return ParseResults([ t for t,s,e in self.scanString( instring, maxMatches ) ]) - except ParseBaseException as exc: - if ParserElement.verbose_stacktrace: - raise - else: - # catch and re-raise exception from here, clears out pyparsing internal stack trace - raise exc - - def split(self, instring, maxsplit=_MAX_INT, includeSeparators=False): - """ - Generator method to split a string using the given expression as a separator. - May be called with optional C{maxsplit} argument, to limit the number of splits; - and the optional C{includeSeparators} argument (default=C{False}), if the separating - matching text should be included in the split results. - - Example:: - punc = oneOf(list(".,;:/-!?")) - print(list(punc.split("This, this?, this sentence, is badly punctuated!"))) - prints:: - ['This', ' this', '', ' this sentence', ' is badly punctuated', ''] - """ - splits = 0 - last = 0 - for t,s,e in self.scanString(instring, maxMatches=maxsplit): - yield instring[last:s] - if includeSeparators: - yield t[0] - last = e - yield instring[last:] - - def __add__(self, other ): - """ - Implementation of + operator - returns C{L{And}}. Adding strings to a ParserElement - converts them to L{Literal}s by default. - - Example:: - greet = Word(alphas) + "," + Word(alphas) + "!" - hello = "Hello, World!" - print (hello, "->", greet.parseString(hello)) - Prints:: - Hello, World! -> ['Hello', ',', 'World', '!'] - """ - if isinstance( other, basestring ): - other = ParserElement._literalStringClass( other ) - if not isinstance( other, ParserElement ): - warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), - SyntaxWarning, stacklevel=2) - return None - return And( [ self, other ] ) - - def __radd__(self, other ): - """ - Implementation of + operator when left operand is not a C{L{ParserElement}} - """ - if isinstance( other, basestring ): - other = ParserElement._literalStringClass( other ) - if not isinstance( other, ParserElement ): - warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), - SyntaxWarning, stacklevel=2) - return None - return other + self - - def __sub__(self, other): - """ - Implementation of - operator, returns C{L{And}} with error stop - """ - if isinstance( other, basestring ): - other = ParserElement._literalStringClass( other ) - if not isinstance( other, ParserElement ): - warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), - SyntaxWarning, stacklevel=2) - return None - return self + And._ErrorStop() + other - - def __rsub__(self, other ): - """ - Implementation of - operator when left operand is not a C{L{ParserElement}} - """ - if isinstance( other, basestring ): - other = ParserElement._literalStringClass( other ) - if not isinstance( other, ParserElement ): - warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), - SyntaxWarning, stacklevel=2) - return None - return other - self - - def __mul__(self,other): - """ - Implementation of * operator, allows use of C{expr * 3} in place of - C{expr + expr + expr}. Expressions may also me multiplied by a 2-integer - tuple, similar to C{{min,max}} multipliers in regular expressions. Tuples - may also include C{None} as in: - - C{expr*(n,None)} or C{expr*(n,)} is equivalent - to C{expr*n + L{ZeroOrMore}(expr)} - (read as "at least n instances of C{expr}") - - C{expr*(None,n)} is equivalent to C{expr*(0,n)} - (read as "0 to n instances of C{expr}") - - C{expr*(None,None)} is equivalent to C{L{ZeroOrMore}(expr)} - - C{expr*(1,None)} is equivalent to C{L{OneOrMore}(expr)} - - Note that C{expr*(None,n)} does not raise an exception if - more than n exprs exist in the input stream; that is, - C{expr*(None,n)} does not enforce a maximum number of expr - occurrences. If this behavior is desired, then write - C{expr*(None,n) + ~expr} - """ - if isinstance(other,int): - minElements, optElements = other,0 - elif isinstance(other,tuple): - other = (other + (None, None))[:2] - if other[0] is None: - other = (0, other[1]) - if isinstance(other[0],int) and other[1] is None: - if other[0] == 0: - return ZeroOrMore(self) - if other[0] == 1: - return OneOrMore(self) - else: - return self*other[0] + ZeroOrMore(self) - elif isinstance(other[0],int) and isinstance(other[1],int): - minElements, optElements = other - optElements -= minElements - else: - raise TypeError("cannot multiply 'ParserElement' and ('%s','%s') objects", type(other[0]),type(other[1])) - else: - raise TypeError("cannot multiply 'ParserElement' and '%s' objects", type(other)) - - if minElements < 0: - raise ValueError("cannot multiply ParserElement by negative value") - if optElements < 0: - raise ValueError("second tuple value must be greater or equal to first tuple value") - if minElements == optElements == 0: - raise ValueError("cannot multiply ParserElement by 0 or (0,0)") - - if (optElements): - def makeOptionalList(n): - if n>1: - return Optional(self + makeOptionalList(n-1)) - else: - return Optional(self) - if minElements: - if minElements == 1: - ret = self + makeOptionalList(optElements) - else: - ret = And([self]*minElements) + makeOptionalList(optElements) - else: - ret = makeOptionalList(optElements) - else: - if minElements == 1: - ret = self - else: - ret = And([self]*minElements) - return ret - - def __rmul__(self, other): - return self.__mul__(other) - - def __or__(self, other ): - """ - Implementation of | operator - returns C{L{MatchFirst}} - """ - if isinstance( other, basestring ): - other = ParserElement._literalStringClass( other ) - if not isinstance( other, ParserElement ): - warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), - SyntaxWarning, stacklevel=2) - return None - return MatchFirst( [ self, other ] ) - - def __ror__(self, other ): - """ - Implementation of | operator when left operand is not a C{L{ParserElement}} - """ - if isinstance( other, basestring ): - other = ParserElement._literalStringClass( other ) - if not isinstance( other, ParserElement ): - warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), - SyntaxWarning, stacklevel=2) - return None - return other | self - - def __xor__(self, other ): - """ - Implementation of ^ operator - returns C{L{Or}} - """ - if isinstance( other, basestring ): - other = ParserElement._literalStringClass( other ) - if not isinstance( other, ParserElement ): - warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), - SyntaxWarning, stacklevel=2) - return None - return Or( [ self, other ] ) - - def __rxor__(self, other ): - """ - Implementation of ^ operator when left operand is not a C{L{ParserElement}} - """ - if isinstance( other, basestring ): - other = ParserElement._literalStringClass( other ) - if not isinstance( other, ParserElement ): - warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), - SyntaxWarning, stacklevel=2) - return None - return other ^ self - - def __and__(self, other ): - """ - Implementation of & operator - returns C{L{Each}} - """ - if isinstance( other, basestring ): - other = ParserElement._literalStringClass( other ) - if not isinstance( other, ParserElement ): - warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), - SyntaxWarning, stacklevel=2) - return None - return Each( [ self, other ] ) - - def __rand__(self, other ): - """ - Implementation of & operator when left operand is not a C{L{ParserElement}} - """ - if isinstance( other, basestring ): - other = ParserElement._literalStringClass( other ) - if not isinstance( other, ParserElement ): - warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), - SyntaxWarning, stacklevel=2) - return None - return other & self - - def __invert__( self ): - """ - Implementation of ~ operator - returns C{L{NotAny}} - """ - return NotAny( self ) - - def __call__(self, name=None): - """ - Shortcut for C{L{setResultsName}}, with C{listAllMatches=False}. - - If C{name} is given with a trailing C{'*'} character, then C{listAllMatches} will be - passed as C{True}. - - If C{name} is omitted, same as calling C{L{copy}}. - - Example:: - # these are equivalent - userdata = Word(alphas).setResultsName("name") + Word(nums+"-").setResultsName("socsecno") - userdata = Word(alphas)("name") + Word(nums+"-")("socsecno") - """ - if name is not None: - return self.setResultsName(name) - else: - return self.copy() - - def suppress( self ): - """ - Suppresses the output of this C{ParserElement}; useful to keep punctuation from - cluttering up returned output. - """ - return Suppress( self ) - - def leaveWhitespace( self ): - """ - Disables the skipping of whitespace before matching the characters in the - C{ParserElement}'s defined pattern. This is normally only used internally by - the pyparsing module, but may be needed in some whitespace-sensitive grammars. - """ - self.skipWhitespace = False - return self - - def setWhitespaceChars( self, chars ): - """ - Overrides the default whitespace chars - """ - self.skipWhitespace = True - self.whiteChars = chars - self.copyDefaultWhiteChars = False - return self - - def parseWithTabs( self ): - """ - Overrides default behavior to expand C{}s to spaces before parsing the input string. - Must be called before C{parseString} when the input grammar contains elements that - match C{} characters. - """ - self.keepTabs = True - return self - - def ignore( self, other ): - """ - Define expression to be ignored (e.g., comments) while doing pattern - matching; may be called repeatedly, to define multiple comment or other - ignorable patterns. - - Example:: - patt = OneOrMore(Word(alphas)) - patt.parseString('ablaj /* comment */ lskjd') # -> ['ablaj'] - - patt.ignore(cStyleComment) - patt.parseString('ablaj /* comment */ lskjd') # -> ['ablaj', 'lskjd'] - """ - if isinstance(other, basestring): - other = Suppress(other) - - if isinstance( other, Suppress ): - if other not in self.ignoreExprs: - self.ignoreExprs.append(other) - else: - self.ignoreExprs.append( Suppress( other.copy() ) ) - return self - - def setDebugActions( self, startAction, successAction, exceptionAction ): - """ - Enable display of debugging messages while doing pattern matching. - """ - self.debugActions = (startAction or _defaultStartDebugAction, - successAction or _defaultSuccessDebugAction, - exceptionAction or _defaultExceptionDebugAction) - self.debug = True - return self - - def setDebug( self, flag=True ): - """ - Enable display of debugging messages while doing pattern matching. - Set C{flag} to True to enable, False to disable. - - Example:: - wd = Word(alphas).setName("alphaword") - integer = Word(nums).setName("numword") - term = wd | integer - - # turn on debugging for wd - wd.setDebug() - - OneOrMore(term).parseString("abc 123 xyz 890") - - prints:: - Match alphaword at loc 0(1,1) - Matched alphaword -> ['abc'] - Match alphaword at loc 3(1,4) - Exception raised:Expected alphaword (at char 4), (line:1, col:5) - Match alphaword at loc 7(1,8) - Matched alphaword -> ['xyz'] - Match alphaword at loc 11(1,12) - Exception raised:Expected alphaword (at char 12), (line:1, col:13) - Match alphaword at loc 15(1,16) - Exception raised:Expected alphaword (at char 15), (line:1, col:16) - - The output shown is that produced by the default debug actions - custom debug actions can be - specified using L{setDebugActions}. Prior to attempting - to match the C{wd} expression, the debugging message C{"Match at loc (,)"} - is shown. Then if the parse succeeds, a C{"Matched"} message is shown, or an C{"Exception raised"} - message is shown. Also note the use of L{setName} to assign a human-readable name to the expression, - which makes debugging and exception messages easier to understand - for instance, the default - name created for the C{Word} expression without calling C{setName} is C{"W:(ABCD...)"}. - """ - if flag: - self.setDebugActions( _defaultStartDebugAction, _defaultSuccessDebugAction, _defaultExceptionDebugAction ) - else: - self.debug = False - return self - - def __str__( self ): - return self.name - - def __repr__( self ): - return _ustr(self) - - def streamline( self ): - self.streamlined = True - self.strRepr = None - return self - - def checkRecursion( self, parseElementList ): - pass - - def validate( self, validateTrace=[] ): - """ - Check defined expressions for valid structure, check for infinite recursive definitions. - """ - self.checkRecursion( [] ) - - def parseFile( self, file_or_filename, parseAll=False ): - """ - Execute the parse expression on the given file or filename. - If a filename is specified (instead of a file object), - the entire file is opened, read, and closed before parsing. - """ - try: - file_contents = file_or_filename.read() - except AttributeError: - with open(file_or_filename, "r") as f: - file_contents = f.read() - try: - return self.parseString(file_contents, parseAll) - except ParseBaseException as exc: - if ParserElement.verbose_stacktrace: - raise - else: - # catch and re-raise exception from here, clears out pyparsing internal stack trace - raise exc - - def __eq__(self,other): - if isinstance(other, ParserElement): - return self is other or vars(self) == vars(other) - elif isinstance(other, basestring): - return self.matches(other) - else: - return super(ParserElement,self)==other - - def __ne__(self,other): - return not (self == other) - - def __hash__(self): - return hash(id(self)) - - def __req__(self,other): - return self == other - - def __rne__(self,other): - return not (self == other) - - def matches(self, testString, parseAll=True): - """ - Method for quick testing of a parser against a test string. Good for simple - inline microtests of sub expressions while building up larger parser. - - Parameters: - - testString - to test against this expression for a match - - parseAll - (default=C{True}) - flag to pass to C{L{parseString}} when running tests - - Example:: - expr = Word(nums) - assert expr.matches("100") - """ - try: - self.parseString(_ustr(testString), parseAll=parseAll) - return True - except ParseBaseException: - return False - - def runTests(self, tests, parseAll=True, comment='#', fullDump=True, printResults=True, failureTests=False): - """ - Execute the parse expression on a series of test strings, showing each - test, the parsed results or where the parse failed. Quick and easy way to - run a parse expression against a list of sample strings. - - Parameters: - - tests - a list of separate test strings, or a multiline string of test strings - - parseAll - (default=C{True}) - flag to pass to C{L{parseString}} when running tests - - comment - (default=C{'#'}) - expression for indicating embedded comments in the test - string; pass None to disable comment filtering - - fullDump - (default=C{True}) - dump results as list followed by results names in nested outline; - if False, only dump nested list - - printResults - (default=C{True}) prints test output to stdout - - failureTests - (default=C{False}) indicates if these tests are expected to fail parsing - - Returns: a (success, results) tuple, where success indicates that all tests succeeded - (or failed if C{failureTests} is True), and the results contain a list of lines of each - test's output - - Example:: - number_expr = pyparsing_common.number.copy() - - result = number_expr.runTests(''' - # unsigned integer - 100 - # negative integer - -100 - # float with scientific notation - 6.02e23 - # integer with scientific notation - 1e-12 - ''') - print("Success" if result[0] else "Failed!") - - result = number_expr.runTests(''' - # stray character - 100Z - # missing leading digit before '.' - -.100 - # too many '.' - 3.14.159 - ''', failureTests=True) - print("Success" if result[0] else "Failed!") - prints:: - # unsigned integer - 100 - [100] - - # negative integer - -100 - [-100] - - # float with scientific notation - 6.02e23 - [6.02e+23] - - # integer with scientific notation - 1e-12 - [1e-12] - - Success - - # stray character - 100Z - ^ - FAIL: Expected end of text (at char 3), (line:1, col:4) - - # missing leading digit before '.' - -.100 - ^ - FAIL: Expected {real number with scientific notation | real number | signed integer} (at char 0), (line:1, col:1) - - # too many '.' - 3.14.159 - ^ - FAIL: Expected end of text (at char 4), (line:1, col:5) - - Success - - Each test string must be on a single line. If you want to test a string that spans multiple - lines, create a test like this:: - - expr.runTest(r"this is a test\\n of strings that spans \\n 3 lines") - - (Note that this is a raw string literal, you must include the leading 'r'.) - """ - if isinstance(tests, basestring): - tests = list(map(str.strip, tests.rstrip().splitlines())) - if isinstance(comment, basestring): - comment = Literal(comment) - allResults = [] - comments = [] - success = True - for t in tests: - if comment is not None and comment.matches(t, False) or comments and not t: - comments.append(t) - continue - if not t: - continue - out = ['\n'.join(comments), t] - comments = [] - try: - t = t.replace(r'\n','\n') - result = self.parseString(t, parseAll=parseAll) - out.append(result.dump(full=fullDump)) - success = success and not failureTests - except ParseBaseException as pe: - fatal = "(FATAL)" if isinstance(pe, ParseFatalException) else "" - if '\n' in t: - out.append(line(pe.loc, t)) - out.append(' '*(col(pe.loc,t)-1) + '^' + fatal) - else: - out.append(' '*pe.loc + '^' + fatal) - out.append("FAIL: " + str(pe)) - success = success and failureTests - result = pe - except Exception as exc: - out.append("FAIL-EXCEPTION: " + str(exc)) - success = success and failureTests - result = exc - - if printResults: - if fullDump: - out.append('') - print('\n'.join(out)) - - allResults.append((t, result)) - - return success, allResults - - -class Token(ParserElement): - """ - Abstract C{ParserElement} subclass, for defining atomic matching patterns. - """ - def __init__( self ): - super(Token,self).__init__( savelist=False ) - - -class Empty(Token): - """ - An empty token, will always match. - """ - def __init__( self ): - super(Empty,self).__init__() - self.name = "Empty" - self.mayReturnEmpty = True - self.mayIndexError = False - - -class NoMatch(Token): - """ - A token that will never match. - """ - def __init__( self ): - super(NoMatch,self).__init__() - self.name = "NoMatch" - self.mayReturnEmpty = True - self.mayIndexError = False - self.errmsg = "Unmatchable token" - - def parseImpl( self, instring, loc, doActions=True ): - raise ParseException(instring, loc, self.errmsg, self) - - -class Literal(Token): - """ - Token to exactly match a specified string. - - Example:: - Literal('blah').parseString('blah') # -> ['blah'] - Literal('blah').parseString('blahfooblah') # -> ['blah'] - Literal('blah').parseString('bla') # -> Exception: Expected "blah" - - For case-insensitive matching, use L{CaselessLiteral}. - - For keyword matching (force word break before and after the matched string), - use L{Keyword} or L{CaselessKeyword}. - """ - def __init__( self, matchString ): - super(Literal,self).__init__() - self.match = matchString - self.matchLen = len(matchString) - try: - self.firstMatchChar = matchString[0] - except IndexError: - warnings.warn("null string passed to Literal; use Empty() instead", - SyntaxWarning, stacklevel=2) - self.__class__ = Empty - self.name = '"%s"' % _ustr(self.match) - self.errmsg = "Expected " + self.name - self.mayReturnEmpty = False - self.mayIndexError = False - - # Performance tuning: this routine gets called a *lot* - # if this is a single character match string and the first character matches, - # short-circuit as quickly as possible, and avoid calling startswith - #~ @profile - def parseImpl( self, instring, loc, doActions=True ): - if (instring[loc] == self.firstMatchChar and - (self.matchLen==1 or instring.startswith(self.match,loc)) ): - return loc+self.matchLen, self.match - raise ParseException(instring, loc, self.errmsg, self) -_L = Literal -ParserElement._literalStringClass = Literal - -class Keyword(Token): - """ - Token to exactly match a specified string as a keyword, that is, it must be - immediately followed by a non-keyword character. Compare with C{L{Literal}}: - - C{Literal("if")} will match the leading C{'if'} in C{'ifAndOnlyIf'}. - - C{Keyword("if")} will not; it will only match the leading C{'if'} in C{'if x=1'}, or C{'if(y==2)'} - Accepts two optional constructor arguments in addition to the keyword string: - - C{identChars} is a string of characters that would be valid identifier characters, - defaulting to all alphanumerics + "_" and "$" - - C{caseless} allows case-insensitive matching, default is C{False}. - - Example:: - Keyword("start").parseString("start") # -> ['start'] - Keyword("start").parseString("starting") # -> Exception - - For case-insensitive matching, use L{CaselessKeyword}. - """ - DEFAULT_KEYWORD_CHARS = alphanums+"_$" - - def __init__( self, matchString, identChars=None, caseless=False ): - super(Keyword,self).__init__() - if identChars is None: - identChars = Keyword.DEFAULT_KEYWORD_CHARS - self.match = matchString - self.matchLen = len(matchString) - try: - self.firstMatchChar = matchString[0] - except IndexError: - warnings.warn("null string passed to Keyword; use Empty() instead", - SyntaxWarning, stacklevel=2) - self.name = '"%s"' % self.match - self.errmsg = "Expected " + self.name - self.mayReturnEmpty = False - self.mayIndexError = False - self.caseless = caseless - if caseless: - self.caselessmatch = matchString.upper() - identChars = identChars.upper() - self.identChars = set(identChars) - - def parseImpl( self, instring, loc, doActions=True ): - if self.caseless: - if ( (instring[ loc:loc+self.matchLen ].upper() == self.caselessmatch) and - (loc >= len(instring)-self.matchLen or instring[loc+self.matchLen].upper() not in self.identChars) and - (loc == 0 or instring[loc-1].upper() not in self.identChars) ): - return loc+self.matchLen, self.match - else: - if (instring[loc] == self.firstMatchChar and - (self.matchLen==1 or instring.startswith(self.match,loc)) and - (loc >= len(instring)-self.matchLen or instring[loc+self.matchLen] not in self.identChars) and - (loc == 0 or instring[loc-1] not in self.identChars) ): - return loc+self.matchLen, self.match - raise ParseException(instring, loc, self.errmsg, self) - - def copy(self): - c = super(Keyword,self).copy() - c.identChars = Keyword.DEFAULT_KEYWORD_CHARS - return c - - @staticmethod - def setDefaultKeywordChars( chars ): - """Overrides the default Keyword chars - """ - Keyword.DEFAULT_KEYWORD_CHARS = chars - -class CaselessLiteral(Literal): - """ - Token to match a specified string, ignoring case of letters. - Note: the matched results will always be in the case of the given - match string, NOT the case of the input text. - - Example:: - OneOrMore(CaselessLiteral("CMD")).parseString("cmd CMD Cmd10") # -> ['CMD', 'CMD', 'CMD'] - - (Contrast with example for L{CaselessKeyword}.) - """ - def __init__( self, matchString ): - super(CaselessLiteral,self).__init__( matchString.upper() ) - # Preserve the defining literal. - self.returnString = matchString - self.name = "'%s'" % self.returnString - self.errmsg = "Expected " + self.name - - def parseImpl( self, instring, loc, doActions=True ): - if instring[ loc:loc+self.matchLen ].upper() == self.match: - return loc+self.matchLen, self.returnString - raise ParseException(instring, loc, self.errmsg, self) - -class CaselessKeyword(Keyword): - """ - Caseless version of L{Keyword}. - - Example:: - OneOrMore(CaselessKeyword("CMD")).parseString("cmd CMD Cmd10") # -> ['CMD', 'CMD'] - - (Contrast with example for L{CaselessLiteral}.) - """ - def __init__( self, matchString, identChars=None ): - super(CaselessKeyword,self).__init__( matchString, identChars, caseless=True ) - - def parseImpl( self, instring, loc, doActions=True ): - if ( (instring[ loc:loc+self.matchLen ].upper() == self.caselessmatch) and - (loc >= len(instring)-self.matchLen or instring[loc+self.matchLen].upper() not in self.identChars) ): - return loc+self.matchLen, self.match - raise ParseException(instring, loc, self.errmsg, self) - -class CloseMatch(Token): - """ - A variation on L{Literal} which matches "close" matches, that is, - strings with at most 'n' mismatching characters. C{CloseMatch} takes parameters: - - C{match_string} - string to be matched - - C{maxMismatches} - (C{default=1}) maximum number of mismatches allowed to count as a match - - The results from a successful parse will contain the matched text from the input string and the following named results: - - C{mismatches} - a list of the positions within the match_string where mismatches were found - - C{original} - the original match_string used to compare against the input string - - If C{mismatches} is an empty list, then the match was an exact match. - - Example:: - patt = CloseMatch("ATCATCGAATGGA") - patt.parseString("ATCATCGAAXGGA") # -> (['ATCATCGAAXGGA'], {'mismatches': [[9]], 'original': ['ATCATCGAATGGA']}) - patt.parseString("ATCAXCGAAXGGA") # -> Exception: Expected 'ATCATCGAATGGA' (with up to 1 mismatches) (at char 0), (line:1, col:1) - - # exact match - patt.parseString("ATCATCGAATGGA") # -> (['ATCATCGAATGGA'], {'mismatches': [[]], 'original': ['ATCATCGAATGGA']}) - - # close match allowing up to 2 mismatches - patt = CloseMatch("ATCATCGAATGGA", maxMismatches=2) - patt.parseString("ATCAXCGAAXGGA") # -> (['ATCAXCGAAXGGA'], {'mismatches': [[4, 9]], 'original': ['ATCATCGAATGGA']}) - """ - def __init__(self, match_string, maxMismatches=1): - super(CloseMatch,self).__init__() - self.name = match_string - self.match_string = match_string - self.maxMismatches = maxMismatches - self.errmsg = "Expected %r (with up to %d mismatches)" % (self.match_string, self.maxMismatches) - self.mayIndexError = False - self.mayReturnEmpty = False - - def parseImpl( self, instring, loc, doActions=True ): - start = loc - instrlen = len(instring) - maxloc = start + len(self.match_string) - - if maxloc <= instrlen: - match_string = self.match_string - match_stringloc = 0 - mismatches = [] - maxMismatches = self.maxMismatches - - for match_stringloc,s_m in enumerate(zip(instring[loc:maxloc], self.match_string)): - src,mat = s_m - if src != mat: - mismatches.append(match_stringloc) - if len(mismatches) > maxMismatches: - break - else: - loc = match_stringloc + 1 - results = ParseResults([instring[start:loc]]) - results['original'] = self.match_string - results['mismatches'] = mismatches - return loc, results - - raise ParseException(instring, loc, self.errmsg, self) - - -class Word(Token): - """ - Token for matching words composed of allowed character sets. - Defined with string containing all allowed initial characters, - an optional string containing allowed body characters (if omitted, - defaults to the initial character set), and an optional minimum, - maximum, and/or exact length. The default value for C{min} is 1 (a - minimum value < 1 is not valid); the default values for C{max} and C{exact} - are 0, meaning no maximum or exact length restriction. An optional - C{excludeChars} parameter can list characters that might be found in - the input C{bodyChars} string; useful to define a word of all printables - except for one or two characters, for instance. - - L{srange} is useful for defining custom character set strings for defining - C{Word} expressions, using range notation from regular expression character sets. - - A common mistake is to use C{Word} to match a specific literal string, as in - C{Word("Address")}. Remember that C{Word} uses the string argument to define - I{sets} of matchable characters. This expression would match "Add", "AAA", - "dAred", or any other word made up of the characters 'A', 'd', 'r', 'e', and 's'. - To match an exact literal string, use L{Literal} or L{Keyword}. - - pyparsing includes helper strings for building Words: - - L{alphas} - - L{nums} - - L{alphanums} - - L{hexnums} - - L{alphas8bit} (alphabetic characters in ASCII range 128-255 - accented, tilded, umlauted, etc.) - - L{punc8bit} (non-alphabetic characters in ASCII range 128-255 - currency, symbols, superscripts, diacriticals, etc.) - - L{printables} (any non-whitespace character) - - Example:: - # a word composed of digits - integer = Word(nums) # equivalent to Word("0123456789") or Word(srange("0-9")) - - # a word with a leading capital, and zero or more lowercase - capital_word = Word(alphas.upper(), alphas.lower()) - - # hostnames are alphanumeric, with leading alpha, and '-' - hostname = Word(alphas, alphanums+'-') - - # roman numeral (not a strict parser, accepts invalid mix of characters) - roman = Word("IVXLCDM") - - # any string of non-whitespace characters, except for ',' - csv_value = Word(printables, excludeChars=",") - """ - def __init__( self, initChars, bodyChars=None, min=1, max=0, exact=0, asKeyword=False, excludeChars=None ): - super(Word,self).__init__() - if excludeChars: - initChars = ''.join(c for c in initChars if c not in excludeChars) - if bodyChars: - bodyChars = ''.join(c for c in bodyChars if c not in excludeChars) - self.initCharsOrig = initChars - self.initChars = set(initChars) - if bodyChars : - self.bodyCharsOrig = bodyChars - self.bodyChars = set(bodyChars) - else: - self.bodyCharsOrig = initChars - self.bodyChars = set(initChars) - - self.maxSpecified = max > 0 - - if min < 1: - raise ValueError("cannot specify a minimum length < 1; use Optional(Word()) if zero-length word is permitted") - - self.minLen = min - - if max > 0: - self.maxLen = max - else: - self.maxLen = _MAX_INT - - if exact > 0: - self.maxLen = exact - self.minLen = exact - - self.name = _ustr(self) - self.errmsg = "Expected " + self.name - self.mayIndexError = False - self.asKeyword = asKeyword - - if ' ' not in self.initCharsOrig+self.bodyCharsOrig and (min==1 and max==0 and exact==0): - if self.bodyCharsOrig == self.initCharsOrig: - self.reString = "[%s]+" % _escapeRegexRangeChars(self.initCharsOrig) - elif len(self.initCharsOrig) == 1: - self.reString = "%s[%s]*" % \ - (re.escape(self.initCharsOrig), - _escapeRegexRangeChars(self.bodyCharsOrig),) - else: - self.reString = "[%s][%s]*" % \ - (_escapeRegexRangeChars(self.initCharsOrig), - _escapeRegexRangeChars(self.bodyCharsOrig),) - if self.asKeyword: - self.reString = r"\b"+self.reString+r"\b" - try: - self.re = re.compile( self.reString ) - except Exception: - self.re = None - - def parseImpl( self, instring, loc, doActions=True ): - if self.re: - result = self.re.match(instring,loc) - if not result: - raise ParseException(instring, loc, self.errmsg, self) - - loc = result.end() - return loc, result.group() - - if not(instring[ loc ] in self.initChars): - raise ParseException(instring, loc, self.errmsg, self) - - start = loc - loc += 1 - instrlen = len(instring) - bodychars = self.bodyChars - maxloc = start + self.maxLen - maxloc = min( maxloc, instrlen ) - while loc < maxloc and instring[loc] in bodychars: - loc += 1 - - throwException = False - if loc - start < self.minLen: - throwException = True - if self.maxSpecified and loc < instrlen and instring[loc] in bodychars: - throwException = True - if self.asKeyword: - if (start>0 and instring[start-1] in bodychars) or (loc4: - return s[:4]+"..." - else: - return s - - if ( self.initCharsOrig != self.bodyCharsOrig ): - self.strRepr = "W:(%s,%s)" % ( charsAsStr(self.initCharsOrig), charsAsStr(self.bodyCharsOrig) ) - else: - self.strRepr = "W:(%s)" % charsAsStr(self.initCharsOrig) - - return self.strRepr - - -class Regex(Token): - r""" - Token for matching strings that match a given regular expression. - Defined with string specifying the regular expression in a form recognized by the inbuilt Python re module. - If the given regex contains named groups (defined using C{(?P...)}), these will be preserved as - named parse results. - - Example:: - realnum = Regex(r"[+-]?\d+\.\d*") - date = Regex(r'(?P\d{4})-(?P\d\d?)-(?P\d\d?)') - # ref: http://stackoverflow.com/questions/267399/how-do-you-match-only-valid-roman-numerals-with-a-regular-expression - roman = Regex(r"M{0,4}(CM|CD|D?C{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})") - """ - compiledREtype = type(re.compile("[A-Z]")) - def __init__( self, pattern, flags=0): - """The parameters C{pattern} and C{flags} are passed to the C{re.compile()} function as-is. See the Python C{re} module for an explanation of the acceptable patterns and flags.""" - super(Regex,self).__init__() - - if isinstance(pattern, basestring): - if not pattern: - warnings.warn("null string passed to Regex; use Empty() instead", - SyntaxWarning, stacklevel=2) - - self.pattern = pattern - self.flags = flags - - try: - self.re = re.compile(self.pattern, self.flags) - self.reString = self.pattern - except sre_constants.error: - warnings.warn("invalid pattern (%s) passed to Regex" % pattern, - SyntaxWarning, stacklevel=2) - raise - - elif isinstance(pattern, Regex.compiledREtype): - self.re = pattern - self.pattern = \ - self.reString = str(pattern) - self.flags = flags - - else: - raise ValueError("Regex may only be constructed with a string or a compiled RE object") - - self.name = _ustr(self) - self.errmsg = "Expected " + self.name - self.mayIndexError = False - self.mayReturnEmpty = True - - def parseImpl( self, instring, loc, doActions=True ): - result = self.re.match(instring,loc) - if not result: - raise ParseException(instring, loc, self.errmsg, self) - - loc = result.end() - d = result.groupdict() - ret = ParseResults(result.group()) - if d: - for k in d: - ret[k] = d[k] - return loc,ret - - def __str__( self ): - try: - return super(Regex,self).__str__() - except Exception: - pass - - if self.strRepr is None: - self.strRepr = "Re:(%s)" % repr(self.pattern) - - return self.strRepr - - -class QuotedString(Token): - r""" - Token for matching strings that are delimited by quoting characters. - - Defined with the following parameters: - - quoteChar - string of one or more characters defining the quote delimiting string - - escChar - character to escape quotes, typically backslash (default=C{None}) - - escQuote - special quote sequence to escape an embedded quote string (such as SQL's "" to escape an embedded ") (default=C{None}) - - multiline - boolean indicating whether quotes can span multiple lines (default=C{False}) - - unquoteResults - boolean indicating whether the matched text should be unquoted (default=C{True}) - - endQuoteChar - string of one or more characters defining the end of the quote delimited string (default=C{None} => same as quoteChar) - - convertWhitespaceEscapes - convert escaped whitespace (C{'\t'}, C{'\n'}, etc.) to actual whitespace (default=C{True}) - - Example:: - qs = QuotedString('"') - print(qs.searchString('lsjdf "This is the quote" sldjf')) - complex_qs = QuotedString('{{', endQuoteChar='}}') - print(complex_qs.searchString('lsjdf {{This is the "quote"}} sldjf')) - sql_qs = QuotedString('"', escQuote='""') - print(sql_qs.searchString('lsjdf "This is the quote with ""embedded"" quotes" sldjf')) - prints:: - [['This is the quote']] - [['This is the "quote"']] - [['This is the quote with "embedded" quotes']] - """ - def __init__( self, quoteChar, escChar=None, escQuote=None, multiline=False, unquoteResults=True, endQuoteChar=None, convertWhitespaceEscapes=True): - super(QuotedString,self).__init__() - - # remove white space from quote chars - wont work anyway - quoteChar = quoteChar.strip() - if not quoteChar: - warnings.warn("quoteChar cannot be the empty string",SyntaxWarning,stacklevel=2) - raise SyntaxError() - - if endQuoteChar is None: - endQuoteChar = quoteChar - else: - endQuoteChar = endQuoteChar.strip() - if not endQuoteChar: - warnings.warn("endQuoteChar cannot be the empty string",SyntaxWarning,stacklevel=2) - raise SyntaxError() - - self.quoteChar = quoteChar - self.quoteCharLen = len(quoteChar) - self.firstQuoteChar = quoteChar[0] - self.endQuoteChar = endQuoteChar - self.endQuoteCharLen = len(endQuoteChar) - self.escChar = escChar - self.escQuote = escQuote - self.unquoteResults = unquoteResults - self.convertWhitespaceEscapes = convertWhitespaceEscapes - - if multiline: - self.flags = re.MULTILINE | re.DOTALL - self.pattern = r'%s(?:[^%s%s]' % \ - ( re.escape(self.quoteChar), - _escapeRegexRangeChars(self.endQuoteChar[0]), - (escChar is not None and _escapeRegexRangeChars(escChar) or '') ) - else: - self.flags = 0 - self.pattern = r'%s(?:[^%s\n\r%s]' % \ - ( re.escape(self.quoteChar), - _escapeRegexRangeChars(self.endQuoteChar[0]), - (escChar is not None and _escapeRegexRangeChars(escChar) or '') ) - if len(self.endQuoteChar) > 1: - self.pattern += ( - '|(?:' + ')|(?:'.join("%s[^%s]" % (re.escape(self.endQuoteChar[:i]), - _escapeRegexRangeChars(self.endQuoteChar[i])) - for i in range(len(self.endQuoteChar)-1,0,-1)) + ')' - ) - if escQuote: - self.pattern += (r'|(?:%s)' % re.escape(escQuote)) - if escChar: - self.pattern += (r'|(?:%s.)' % re.escape(escChar)) - self.escCharReplacePattern = re.escape(self.escChar)+"(.)" - self.pattern += (r')*%s' % re.escape(self.endQuoteChar)) - - try: - self.re = re.compile(self.pattern, self.flags) - self.reString = self.pattern - except sre_constants.error: - warnings.warn("invalid pattern (%s) passed to Regex" % self.pattern, - SyntaxWarning, stacklevel=2) - raise - - self.name = _ustr(self) - self.errmsg = "Expected " + self.name - self.mayIndexError = False - self.mayReturnEmpty = True - - def parseImpl( self, instring, loc, doActions=True ): - result = instring[loc] == self.firstQuoteChar and self.re.match(instring,loc) or None - if not result: - raise ParseException(instring, loc, self.errmsg, self) - - loc = result.end() - ret = result.group() - - if self.unquoteResults: - - # strip off quotes - ret = ret[self.quoteCharLen:-self.endQuoteCharLen] - - if isinstance(ret,basestring): - # replace escaped whitespace - if '\\' in ret and self.convertWhitespaceEscapes: - ws_map = { - r'\t' : '\t', - r'\n' : '\n', - r'\f' : '\f', - r'\r' : '\r', - } - for wslit,wschar in ws_map.items(): - ret = ret.replace(wslit, wschar) - - # replace escaped characters - if self.escChar: - ret = re.sub(self.escCharReplacePattern, r"\g<1>", ret) - - # replace escaped quotes - if self.escQuote: - ret = ret.replace(self.escQuote, self.endQuoteChar) - - return loc, ret - - def __str__( self ): - try: - return super(QuotedString,self).__str__() - except Exception: - pass - - if self.strRepr is None: - self.strRepr = "quoted string, starting with %s ending with %s" % (self.quoteChar, self.endQuoteChar) - - return self.strRepr - - -class CharsNotIn(Token): - """ - Token for matching words composed of characters I{not} in a given set (will - include whitespace in matched characters if not listed in the provided exclusion set - see example). - Defined with string containing all disallowed characters, and an optional - minimum, maximum, and/or exact length. The default value for C{min} is 1 (a - minimum value < 1 is not valid); the default values for C{max} and C{exact} - are 0, meaning no maximum or exact length restriction. - - Example:: - # define a comma-separated-value as anything that is not a ',' - csv_value = CharsNotIn(',') - print(delimitedList(csv_value).parseString("dkls,lsdkjf,s12 34,@!#,213")) - prints:: - ['dkls', 'lsdkjf', 's12 34', '@!#', '213'] - """ - def __init__( self, notChars, min=1, max=0, exact=0 ): - super(CharsNotIn,self).__init__() - self.skipWhitespace = False - self.notChars = notChars - - if min < 1: - raise ValueError("cannot specify a minimum length < 1; use Optional(CharsNotIn()) if zero-length char group is permitted") - - self.minLen = min - - if max > 0: - self.maxLen = max - else: - self.maxLen = _MAX_INT - - if exact > 0: - self.maxLen = exact - self.minLen = exact - - self.name = _ustr(self) - self.errmsg = "Expected " + self.name - self.mayReturnEmpty = ( self.minLen == 0 ) - self.mayIndexError = False - - def parseImpl( self, instring, loc, doActions=True ): - if instring[loc] in self.notChars: - raise ParseException(instring, loc, self.errmsg, self) - - start = loc - loc += 1 - notchars = self.notChars - maxlen = min( start+self.maxLen, len(instring) ) - while loc < maxlen and \ - (instring[loc] not in notchars): - loc += 1 - - if loc - start < self.minLen: - raise ParseException(instring, loc, self.errmsg, self) - - return loc, instring[start:loc] - - def __str__( self ): - try: - return super(CharsNotIn, self).__str__() - except Exception: - pass - - if self.strRepr is None: - if len(self.notChars) > 4: - self.strRepr = "!W:(%s...)" % self.notChars[:4] - else: - self.strRepr = "!W:(%s)" % self.notChars - - return self.strRepr - -class White(Token): - """ - Special matching class for matching whitespace. Normally, whitespace is ignored - by pyparsing grammars. This class is included when some whitespace structures - are significant. Define with a string containing the whitespace characters to be - matched; default is C{" \\t\\r\\n"}. Also takes optional C{min}, C{max}, and C{exact} arguments, - as defined for the C{L{Word}} class. - """ - whiteStrs = { - " " : "", - "\t": "", - "\n": "", - "\r": "", - "\f": "", - } - def __init__(self, ws=" \t\r\n", min=1, max=0, exact=0): - super(White,self).__init__() - self.matchWhite = ws - self.setWhitespaceChars( "".join(c for c in self.whiteChars if c not in self.matchWhite) ) - #~ self.leaveWhitespace() - self.name = ("".join(White.whiteStrs[c] for c in self.matchWhite)) - self.mayReturnEmpty = True - self.errmsg = "Expected " + self.name - - self.minLen = min - - if max > 0: - self.maxLen = max - else: - self.maxLen = _MAX_INT - - if exact > 0: - self.maxLen = exact - self.minLen = exact - - def parseImpl( self, instring, loc, doActions=True ): - if not(instring[ loc ] in self.matchWhite): - raise ParseException(instring, loc, self.errmsg, self) - start = loc - loc += 1 - maxloc = start + self.maxLen - maxloc = min( maxloc, len(instring) ) - while loc < maxloc and instring[loc] in self.matchWhite: - loc += 1 - - if loc - start < self.minLen: - raise ParseException(instring, loc, self.errmsg, self) - - return loc, instring[start:loc] - - -class _PositionToken(Token): - def __init__( self ): - super(_PositionToken,self).__init__() - self.name=self.__class__.__name__ - self.mayReturnEmpty = True - self.mayIndexError = False - -class GoToColumn(_PositionToken): - """ - Token to advance to a specific column of input text; useful for tabular report scraping. - """ - def __init__( self, colno ): - super(GoToColumn,self).__init__() - self.col = colno - - def preParse( self, instring, loc ): - if col(loc,instring) != self.col: - instrlen = len(instring) - if self.ignoreExprs: - loc = self._skipIgnorables( instring, loc ) - while loc < instrlen and instring[loc].isspace() and col( loc, instring ) != self.col : - loc += 1 - return loc - - def parseImpl( self, instring, loc, doActions=True ): - thiscol = col( loc, instring ) - if thiscol > self.col: - raise ParseException( instring, loc, "Text not in expected column", self ) - newloc = loc + self.col - thiscol - ret = instring[ loc: newloc ] - return newloc, ret - - -class LineStart(_PositionToken): - """ - Matches if current position is at the beginning of a line within the parse string - - Example:: - - test = '''\ - AAA this line - AAA and this line - AAA but not this one - B AAA and definitely not this one - ''' - - for t in (LineStart() + 'AAA' + restOfLine).searchString(test): - print(t) - - Prints:: - ['AAA', ' this line'] - ['AAA', ' and this line'] - - """ - def __init__( self ): - super(LineStart,self).__init__() - self.errmsg = "Expected start of line" - - def parseImpl( self, instring, loc, doActions=True ): - if col(loc, instring) == 1: - return loc, [] - raise ParseException(instring, loc, self.errmsg, self) - -class LineEnd(_PositionToken): - """ - Matches if current position is at the end of a line within the parse string - """ - def __init__( self ): - super(LineEnd,self).__init__() - self.setWhitespaceChars( ParserElement.DEFAULT_WHITE_CHARS.replace("\n","") ) - self.errmsg = "Expected end of line" - - def parseImpl( self, instring, loc, doActions=True ): - if loc len(instring): - return loc, [] - else: - raise ParseException(instring, loc, self.errmsg, self) - -class WordStart(_PositionToken): - """ - Matches if the current position is at the beginning of a Word, and - is not preceded by any character in a given set of C{wordChars} - (default=C{printables}). To emulate the C{\b} behavior of regular expressions, - use C{WordStart(alphanums)}. C{WordStart} will also match at the beginning of - the string being parsed, or at the beginning of a line. - """ - def __init__(self, wordChars = printables): - super(WordStart,self).__init__() - self.wordChars = set(wordChars) - self.errmsg = "Not at the start of a word" - - def parseImpl(self, instring, loc, doActions=True ): - if loc != 0: - if (instring[loc-1] in self.wordChars or - instring[loc] not in self.wordChars): - raise ParseException(instring, loc, self.errmsg, self) - return loc, [] - -class WordEnd(_PositionToken): - """ - Matches if the current position is at the end of a Word, and - is not followed by any character in a given set of C{wordChars} - (default=C{printables}). To emulate the C{\b} behavior of regular expressions, - use C{WordEnd(alphanums)}. C{WordEnd} will also match at the end of - the string being parsed, or at the end of a line. - """ - def __init__(self, wordChars = printables): - super(WordEnd,self).__init__() - self.wordChars = set(wordChars) - self.skipWhitespace = False - self.errmsg = "Not at the end of a word" - - def parseImpl(self, instring, loc, doActions=True ): - instrlen = len(instring) - if instrlen>0 and loc maxExcLoc: - maxException = err - maxExcLoc = err.loc - except IndexError: - if len(instring) > maxExcLoc: - maxException = ParseException(instring,len(instring),e.errmsg,self) - maxExcLoc = len(instring) - else: - # save match among all matches, to retry longest to shortest - matches.append((loc2, e)) - - if matches: - matches.sort(key=lambda x: -x[0]) - for _,e in matches: - try: - return e._parse( instring, loc, doActions ) - except ParseException as err: - err.__traceback__ = None - if err.loc > maxExcLoc: - maxException = err - maxExcLoc = err.loc - - if maxException is not None: - maxException.msg = self.errmsg - raise maxException - else: - raise ParseException(instring, loc, "no defined alternatives to match", self) - - - def __ixor__(self, other ): - if isinstance( other, basestring ): - other = ParserElement._literalStringClass( other ) - return self.append( other ) #Or( [ self, other ] ) - - def __str__( self ): - if hasattr(self,"name"): - return self.name - - if self.strRepr is None: - self.strRepr = "{" + " ^ ".join(_ustr(e) for e in self.exprs) + "}" - - return self.strRepr - - def checkRecursion( self, parseElementList ): - subRecCheckList = parseElementList[:] + [ self ] - for e in self.exprs: - e.checkRecursion( subRecCheckList ) - - -class MatchFirst(ParseExpression): - """ - Requires that at least one C{ParseExpression} is found. - If two expressions match, the first one listed is the one that will match. - May be constructed using the C{'|'} operator. - - Example:: - # construct MatchFirst using '|' operator - - # watch the order of expressions to match - number = Word(nums) | Combine(Word(nums) + '.' + Word(nums)) - print(number.searchString("123 3.1416 789")) # Fail! -> [['123'], ['3'], ['1416'], ['789']] - - # put more selective expression first - number = Combine(Word(nums) + '.' + Word(nums)) | Word(nums) - print(number.searchString("123 3.1416 789")) # Better -> [['123'], ['3.1416'], ['789']] - """ - def __init__( self, exprs, savelist = False ): - super(MatchFirst,self).__init__(exprs, savelist) - if self.exprs: - self.mayReturnEmpty = any(e.mayReturnEmpty for e in self.exprs) - else: - self.mayReturnEmpty = True - - def parseImpl( self, instring, loc, doActions=True ): - maxExcLoc = -1 - maxException = None - for e in self.exprs: - try: - ret = e._parse( instring, loc, doActions ) - return ret - except ParseException as err: - if err.loc > maxExcLoc: - maxException = err - maxExcLoc = err.loc - except IndexError: - if len(instring) > maxExcLoc: - maxException = ParseException(instring,len(instring),e.errmsg,self) - maxExcLoc = len(instring) - - # only got here if no expression matched, raise exception for match that made it the furthest - else: - if maxException is not None: - maxException.msg = self.errmsg - raise maxException - else: - raise ParseException(instring, loc, "no defined alternatives to match", self) - - def __ior__(self, other ): - if isinstance( other, basestring ): - other = ParserElement._literalStringClass( other ) - return self.append( other ) #MatchFirst( [ self, other ] ) - - def __str__( self ): - if hasattr(self,"name"): - return self.name - - if self.strRepr is None: - self.strRepr = "{" + " | ".join(_ustr(e) for e in self.exprs) + "}" - - return self.strRepr - - def checkRecursion( self, parseElementList ): - subRecCheckList = parseElementList[:] + [ self ] - for e in self.exprs: - e.checkRecursion( subRecCheckList ) - - -class Each(ParseExpression): - """ - Requires all given C{ParseExpression}s to be found, but in any order. - Expressions may be separated by whitespace. - May be constructed using the C{'&'} operator. - - Example:: - color = oneOf("RED ORANGE YELLOW GREEN BLUE PURPLE BLACK WHITE BROWN") - shape_type = oneOf("SQUARE CIRCLE TRIANGLE STAR HEXAGON OCTAGON") - integer = Word(nums) - shape_attr = "shape:" + shape_type("shape") - posn_attr = "posn:" + Group(integer("x") + ',' + integer("y"))("posn") - color_attr = "color:" + color("color") - size_attr = "size:" + integer("size") - - # use Each (using operator '&') to accept attributes in any order - # (shape and posn are required, color and size are optional) - shape_spec = shape_attr & posn_attr & Optional(color_attr) & Optional(size_attr) - - shape_spec.runTests(''' - shape: SQUARE color: BLACK posn: 100, 120 - shape: CIRCLE size: 50 color: BLUE posn: 50,80 - color:GREEN size:20 shape:TRIANGLE posn:20,40 - ''' - ) - prints:: - shape: SQUARE color: BLACK posn: 100, 120 - ['shape:', 'SQUARE', 'color:', 'BLACK', 'posn:', ['100', ',', '120']] - - color: BLACK - - posn: ['100', ',', '120'] - - x: 100 - - y: 120 - - shape: SQUARE - - - shape: CIRCLE size: 50 color: BLUE posn: 50,80 - ['shape:', 'CIRCLE', 'size:', '50', 'color:', 'BLUE', 'posn:', ['50', ',', '80']] - - color: BLUE - - posn: ['50', ',', '80'] - - x: 50 - - y: 80 - - shape: CIRCLE - - size: 50 - - - color: GREEN size: 20 shape: TRIANGLE posn: 20,40 - ['color:', 'GREEN', 'size:', '20', 'shape:', 'TRIANGLE', 'posn:', ['20', ',', '40']] - - color: GREEN - - posn: ['20', ',', '40'] - - x: 20 - - y: 40 - - shape: TRIANGLE - - size: 20 - """ - def __init__( self, exprs, savelist = True ): - super(Each,self).__init__(exprs, savelist) - self.mayReturnEmpty = all(e.mayReturnEmpty for e in self.exprs) - self.skipWhitespace = True - self.initExprGroups = True - - def parseImpl( self, instring, loc, doActions=True ): - if self.initExprGroups: - self.opt1map = dict((id(e.expr),e) for e in self.exprs if isinstance(e,Optional)) - opt1 = [ e.expr for e in self.exprs if isinstance(e,Optional) ] - opt2 = [ e for e in self.exprs if e.mayReturnEmpty and not isinstance(e,Optional)] - self.optionals = opt1 + opt2 - self.multioptionals = [ e.expr for e in self.exprs if isinstance(e,ZeroOrMore) ] - self.multirequired = [ e.expr for e in self.exprs if isinstance(e,OneOrMore) ] - self.required = [ e for e in self.exprs if not isinstance(e,(Optional,ZeroOrMore,OneOrMore)) ] - self.required += self.multirequired - self.initExprGroups = False - tmpLoc = loc - tmpReqd = self.required[:] - tmpOpt = self.optionals[:] - matchOrder = [] - - keepMatching = True - while keepMatching: - tmpExprs = tmpReqd + tmpOpt + self.multioptionals + self.multirequired - failed = [] - for e in tmpExprs: - try: - tmpLoc = e.tryParse( instring, tmpLoc ) - except ParseException: - failed.append(e) - else: - matchOrder.append(self.opt1map.get(id(e),e)) - if e in tmpReqd: - tmpReqd.remove(e) - elif e in tmpOpt: - tmpOpt.remove(e) - if len(failed) == len(tmpExprs): - keepMatching = False - - if tmpReqd: - missing = ", ".join(_ustr(e) for e in tmpReqd) - raise ParseException(instring,loc,"Missing one or more required elements (%s)" % missing ) - - # add any unmatched Optionals, in case they have default values defined - matchOrder += [e for e in self.exprs if isinstance(e,Optional) and e.expr in tmpOpt] - - resultlist = [] - for e in matchOrder: - loc,results = e._parse(instring,loc,doActions) - resultlist.append(results) - - finalResults = sum(resultlist, ParseResults([])) - return loc, finalResults - - def __str__( self ): - if hasattr(self,"name"): - return self.name - - if self.strRepr is None: - self.strRepr = "{" + " & ".join(_ustr(e) for e in self.exprs) + "}" - - return self.strRepr - - def checkRecursion( self, parseElementList ): - subRecCheckList = parseElementList[:] + [ self ] - for e in self.exprs: - e.checkRecursion( subRecCheckList ) - - -class ParseElementEnhance(ParserElement): - """ - Abstract subclass of C{ParserElement}, for combining and post-processing parsed tokens. - """ - def __init__( self, expr, savelist=False ): - super(ParseElementEnhance,self).__init__(savelist) - if isinstance( expr, basestring ): - if issubclass(ParserElement._literalStringClass, Token): - expr = ParserElement._literalStringClass(expr) - else: - expr = ParserElement._literalStringClass(Literal(expr)) - self.expr = expr - self.strRepr = None - if expr is not None: - self.mayIndexError = expr.mayIndexError - self.mayReturnEmpty = expr.mayReturnEmpty - self.setWhitespaceChars( expr.whiteChars ) - self.skipWhitespace = expr.skipWhitespace - self.saveAsList = expr.saveAsList - self.callPreparse = expr.callPreparse - self.ignoreExprs.extend(expr.ignoreExprs) - - def parseImpl( self, instring, loc, doActions=True ): - if self.expr is not None: - return self.expr._parse( instring, loc, doActions, callPreParse=False ) - else: - raise ParseException("",loc,self.errmsg,self) - - def leaveWhitespace( self ): - self.skipWhitespace = False - self.expr = self.expr.copy() - if self.expr is not None: - self.expr.leaveWhitespace() - return self - - def ignore( self, other ): - if isinstance( other, Suppress ): - if other not in self.ignoreExprs: - super( ParseElementEnhance, self).ignore( other ) - if self.expr is not None: - self.expr.ignore( self.ignoreExprs[-1] ) - else: - super( ParseElementEnhance, self).ignore( other ) - if self.expr is not None: - self.expr.ignore( self.ignoreExprs[-1] ) - return self - - def streamline( self ): - super(ParseElementEnhance,self).streamline() - if self.expr is not None: - self.expr.streamline() - return self - - def checkRecursion( self, parseElementList ): - if self in parseElementList: - raise RecursiveGrammarException( parseElementList+[self] ) - subRecCheckList = parseElementList[:] + [ self ] - if self.expr is not None: - self.expr.checkRecursion( subRecCheckList ) - - def validate( self, validateTrace=[] ): - tmp = validateTrace[:]+[self] - if self.expr is not None: - self.expr.validate(tmp) - self.checkRecursion( [] ) - - def __str__( self ): - try: - return super(ParseElementEnhance,self).__str__() - except Exception: - pass - - if self.strRepr is None and self.expr is not None: - self.strRepr = "%s:(%s)" % ( self.__class__.__name__, _ustr(self.expr) ) - return self.strRepr - - -class FollowedBy(ParseElementEnhance): - """ - Lookahead matching of the given parse expression. C{FollowedBy} - does I{not} advance the parsing position within the input string, it only - verifies that the specified parse expression matches at the current - position. C{FollowedBy} always returns a null token list. - - Example:: - # use FollowedBy to match a label only if it is followed by a ':' - data_word = Word(alphas) - label = data_word + FollowedBy(':') - attr_expr = Group(label + Suppress(':') + OneOrMore(data_word, stopOn=label).setParseAction(' '.join)) - - OneOrMore(attr_expr).parseString("shape: SQUARE color: BLACK posn: upper left").pprint() - prints:: - [['shape', 'SQUARE'], ['color', 'BLACK'], ['posn', 'upper left']] - """ - def __init__( self, expr ): - super(FollowedBy,self).__init__(expr) - self.mayReturnEmpty = True - - def parseImpl( self, instring, loc, doActions=True ): - self.expr.tryParse( instring, loc ) - return loc, [] - - -class NotAny(ParseElementEnhance): - """ - Lookahead to disallow matching with the given parse expression. C{NotAny} - does I{not} advance the parsing position within the input string, it only - verifies that the specified parse expression does I{not} match at the current - position. Also, C{NotAny} does I{not} skip over leading whitespace. C{NotAny} - always returns a null token list. May be constructed using the '~' operator. - - Example:: - - """ - def __init__( self, expr ): - super(NotAny,self).__init__(expr) - #~ self.leaveWhitespace() - self.skipWhitespace = False # do NOT use self.leaveWhitespace(), don't want to propagate to exprs - self.mayReturnEmpty = True - self.errmsg = "Found unwanted token, "+_ustr(self.expr) - - def parseImpl( self, instring, loc, doActions=True ): - if self.expr.canParseNext(instring, loc): - raise ParseException(instring, loc, self.errmsg, self) - return loc, [] - - def __str__( self ): - if hasattr(self,"name"): - return self.name - - if self.strRepr is None: - self.strRepr = "~{" + _ustr(self.expr) + "}" - - return self.strRepr - -class _MultipleMatch(ParseElementEnhance): - def __init__( self, expr, stopOn=None): - super(_MultipleMatch, self).__init__(expr) - self.saveAsList = True - ender = stopOn - if isinstance(ender, basestring): - ender = ParserElement._literalStringClass(ender) - self.not_ender = ~ender if ender is not None else None - - def parseImpl( self, instring, loc, doActions=True ): - self_expr_parse = self.expr._parse - self_skip_ignorables = self._skipIgnorables - check_ender = self.not_ender is not None - if check_ender: - try_not_ender = self.not_ender.tryParse - - # must be at least one (but first see if we are the stopOn sentinel; - # if so, fail) - if check_ender: - try_not_ender(instring, loc) - loc, tokens = self_expr_parse( instring, loc, doActions, callPreParse=False ) - try: - hasIgnoreExprs = (not not self.ignoreExprs) - while 1: - if check_ender: - try_not_ender(instring, loc) - if hasIgnoreExprs: - preloc = self_skip_ignorables( instring, loc ) - else: - preloc = loc - loc, tmptokens = self_expr_parse( instring, preloc, doActions ) - if tmptokens or tmptokens.haskeys(): - tokens += tmptokens - except (ParseException,IndexError): - pass - - return loc, tokens - -class OneOrMore(_MultipleMatch): - """ - Repetition of one or more of the given expression. - - Parameters: - - expr - expression that must match one or more times - - stopOn - (default=C{None}) - expression for a terminating sentinel - (only required if the sentinel would ordinarily match the repetition - expression) - - Example:: - data_word = Word(alphas) - label = data_word + FollowedBy(':') - attr_expr = Group(label + Suppress(':') + OneOrMore(data_word).setParseAction(' '.join)) - - text = "shape: SQUARE posn: upper left color: BLACK" - OneOrMore(attr_expr).parseString(text).pprint() # Fail! read 'color' as data instead of next label -> [['shape', 'SQUARE color']] - - # use stopOn attribute for OneOrMore to avoid reading label string as part of the data - attr_expr = Group(label + Suppress(':') + OneOrMore(data_word, stopOn=label).setParseAction(' '.join)) - OneOrMore(attr_expr).parseString(text).pprint() # Better -> [['shape', 'SQUARE'], ['posn', 'upper left'], ['color', 'BLACK']] - - # could also be written as - (attr_expr * (1,)).parseString(text).pprint() - """ - - def __str__( self ): - if hasattr(self,"name"): - return self.name - - if self.strRepr is None: - self.strRepr = "{" + _ustr(self.expr) + "}..." - - return self.strRepr - -class ZeroOrMore(_MultipleMatch): - """ - Optional repetition of zero or more of the given expression. - - Parameters: - - expr - expression that must match zero or more times - - stopOn - (default=C{None}) - expression for a terminating sentinel - (only required if the sentinel would ordinarily match the repetition - expression) - - Example: similar to L{OneOrMore} - """ - def __init__( self, expr, stopOn=None): - super(ZeroOrMore,self).__init__(expr, stopOn=stopOn) - self.mayReturnEmpty = True - - def parseImpl( self, instring, loc, doActions=True ): - try: - return super(ZeroOrMore, self).parseImpl(instring, loc, doActions) - except (ParseException,IndexError): - return loc, [] - - def __str__( self ): - if hasattr(self,"name"): - return self.name - - if self.strRepr is None: - self.strRepr = "[" + _ustr(self.expr) + "]..." - - return self.strRepr - -class _NullToken(object): - def __bool__(self): - return False - __nonzero__ = __bool__ - def __str__(self): - return "" - -_optionalNotMatched = _NullToken() -class Optional(ParseElementEnhance): - """ - Optional matching of the given expression. - - Parameters: - - expr - expression that must match zero or more times - - default (optional) - value to be returned if the optional expression is not found. - - Example:: - # US postal code can be a 5-digit zip, plus optional 4-digit qualifier - zip = Combine(Word(nums, exact=5) + Optional('-' + Word(nums, exact=4))) - zip.runTests(''' - # traditional ZIP code - 12345 - - # ZIP+4 form - 12101-0001 - - # invalid ZIP - 98765- - ''') - prints:: - # traditional ZIP code - 12345 - ['12345'] - - # ZIP+4 form - 12101-0001 - ['12101-0001'] - - # invalid ZIP - 98765- - ^ - FAIL: Expected end of text (at char 5), (line:1, col:6) - """ - def __init__( self, expr, default=_optionalNotMatched ): - super(Optional,self).__init__( expr, savelist=False ) - self.saveAsList = self.expr.saveAsList - self.defaultValue = default - self.mayReturnEmpty = True - - def parseImpl( self, instring, loc, doActions=True ): - try: - loc, tokens = self.expr._parse( instring, loc, doActions, callPreParse=False ) - except (ParseException,IndexError): - if self.defaultValue is not _optionalNotMatched: - if self.expr.resultsName: - tokens = ParseResults([ self.defaultValue ]) - tokens[self.expr.resultsName] = self.defaultValue - else: - tokens = [ self.defaultValue ] - else: - tokens = [] - return loc, tokens - - def __str__( self ): - if hasattr(self,"name"): - return self.name - - if self.strRepr is None: - self.strRepr = "[" + _ustr(self.expr) + "]" - - return self.strRepr - -class SkipTo(ParseElementEnhance): - """ - Token for skipping over all undefined text until the matched expression is found. - - Parameters: - - expr - target expression marking the end of the data to be skipped - - include - (default=C{False}) if True, the target expression is also parsed - (the skipped text and target expression are returned as a 2-element list). - - ignore - (default=C{None}) used to define grammars (typically quoted strings and - comments) that might contain false matches to the target expression - - failOn - (default=C{None}) define expressions that are not allowed to be - included in the skipped test; if found before the target expression is found, - the SkipTo is not a match - - Example:: - report = ''' - Outstanding Issues Report - 1 Jan 2000 - - # | Severity | Description | Days Open - -----+----------+-------------------------------------------+----------- - 101 | Critical | Intermittent system crash | 6 - 94 | Cosmetic | Spelling error on Login ('log|n') | 14 - 79 | Minor | System slow when running too many reports | 47 - ''' - integer = Word(nums) - SEP = Suppress('|') - # use SkipTo to simply match everything up until the next SEP - # - ignore quoted strings, so that a '|' character inside a quoted string does not match - # - parse action will call token.strip() for each matched token, i.e., the description body - string_data = SkipTo(SEP, ignore=quotedString) - string_data.setParseAction(tokenMap(str.strip)) - ticket_expr = (integer("issue_num") + SEP - + string_data("sev") + SEP - + string_data("desc") + SEP - + integer("days_open")) - - for tkt in ticket_expr.searchString(report): - print tkt.dump() - prints:: - ['101', 'Critical', 'Intermittent system crash', '6'] - - days_open: 6 - - desc: Intermittent system crash - - issue_num: 101 - - sev: Critical - ['94', 'Cosmetic', "Spelling error on Login ('log|n')", '14'] - - days_open: 14 - - desc: Spelling error on Login ('log|n') - - issue_num: 94 - - sev: Cosmetic - ['79', 'Minor', 'System slow when running too many reports', '47'] - - days_open: 47 - - desc: System slow when running too many reports - - issue_num: 79 - - sev: Minor - """ - def __init__( self, other, include=False, ignore=None, failOn=None ): - super( SkipTo, self ).__init__( other ) - self.ignoreExpr = ignore - self.mayReturnEmpty = True - self.mayIndexError = False - self.includeMatch = include - self.asList = False - if isinstance(failOn, basestring): - self.failOn = ParserElement._literalStringClass(failOn) - else: - self.failOn = failOn - self.errmsg = "No match found for "+_ustr(self.expr) - - def parseImpl( self, instring, loc, doActions=True ): - startloc = loc - instrlen = len(instring) - expr = self.expr - expr_parse = self.expr._parse - self_failOn_canParseNext = self.failOn.canParseNext if self.failOn is not None else None - self_ignoreExpr_tryParse = self.ignoreExpr.tryParse if self.ignoreExpr is not None else None - - tmploc = loc - while tmploc <= instrlen: - if self_failOn_canParseNext is not None: - # break if failOn expression matches - if self_failOn_canParseNext(instring, tmploc): - break - - if self_ignoreExpr_tryParse is not None: - # advance past ignore expressions - while 1: - try: - tmploc = self_ignoreExpr_tryParse(instring, tmploc) - except ParseBaseException: - break - - try: - expr_parse(instring, tmploc, doActions=False, callPreParse=False) - except (ParseException, IndexError): - # no match, advance loc in string - tmploc += 1 - else: - # matched skipto expr, done - break - - else: - # ran off the end of the input string without matching skipto expr, fail - raise ParseException(instring, loc, self.errmsg, self) - - # build up return values - loc = tmploc - skiptext = instring[startloc:loc] - skipresult = ParseResults(skiptext) - - if self.includeMatch: - loc, mat = expr_parse(instring,loc,doActions,callPreParse=False) - skipresult += mat - - return loc, skipresult - -class Forward(ParseElementEnhance): - """ - Forward declaration of an expression to be defined later - - used for recursive grammars, such as algebraic infix notation. - When the expression is known, it is assigned to the C{Forward} variable using the '<<' operator. - - Note: take care when assigning to C{Forward} not to overlook precedence of operators. - Specifically, '|' has a lower precedence than '<<', so that:: - fwdExpr << a | b | c - will actually be evaluated as:: - (fwdExpr << a) | b | c - thereby leaving b and c out as parseable alternatives. It is recommended that you - explicitly group the values inserted into the C{Forward}:: - fwdExpr << (a | b | c) - Converting to use the '<<=' operator instead will avoid this problem. - - See L{ParseResults.pprint} for an example of a recursive parser created using - C{Forward}. - """ - def __init__( self, other=None ): - super(Forward,self).__init__( other, savelist=False ) - - def __lshift__( self, other ): - if isinstance( other, basestring ): - other = ParserElement._literalStringClass(other) - self.expr = other - self.strRepr = None - self.mayIndexError = self.expr.mayIndexError - self.mayReturnEmpty = self.expr.mayReturnEmpty - self.setWhitespaceChars( self.expr.whiteChars ) - self.skipWhitespace = self.expr.skipWhitespace - self.saveAsList = self.expr.saveAsList - self.ignoreExprs.extend(self.expr.ignoreExprs) - return self - - def __ilshift__(self, other): - return self << other - - def leaveWhitespace( self ): - self.skipWhitespace = False - return self - - def streamline( self ): - if not self.streamlined: - self.streamlined = True - if self.expr is not None: - self.expr.streamline() - return self - - def validate( self, validateTrace=[] ): - if self not in validateTrace: - tmp = validateTrace[:]+[self] - if self.expr is not None: - self.expr.validate(tmp) - self.checkRecursion([]) - - def __str__( self ): - if hasattr(self,"name"): - return self.name - return self.__class__.__name__ + ": ..." - - # stubbed out for now - creates awful memory and perf issues - self._revertClass = self.__class__ - self.__class__ = _ForwardNoRecurse - try: - if self.expr is not None: - retString = _ustr(self.expr) - else: - retString = "None" - finally: - self.__class__ = self._revertClass - return self.__class__.__name__ + ": " + retString - - def copy(self): - if self.expr is not None: - return super(Forward,self).copy() - else: - ret = Forward() - ret <<= self - return ret - -class _ForwardNoRecurse(Forward): - def __str__( self ): - return "..." - -class TokenConverter(ParseElementEnhance): - """ - Abstract subclass of C{ParseExpression}, for converting parsed results. - """ - def __init__( self, expr, savelist=False ): - super(TokenConverter,self).__init__( expr )#, savelist ) - self.saveAsList = False - -class Combine(TokenConverter): - """ - Converter to concatenate all matching tokens to a single string. - By default, the matching patterns must also be contiguous in the input string; - this can be disabled by specifying C{'adjacent=False'} in the constructor. - - Example:: - real = Word(nums) + '.' + Word(nums) - print(real.parseString('3.1416')) # -> ['3', '.', '1416'] - # will also erroneously match the following - print(real.parseString('3. 1416')) # -> ['3', '.', '1416'] - - real = Combine(Word(nums) + '.' + Word(nums)) - print(real.parseString('3.1416')) # -> ['3.1416'] - # no match when there are internal spaces - print(real.parseString('3. 1416')) # -> Exception: Expected W:(0123...) - """ - def __init__( self, expr, joinString="", adjacent=True ): - super(Combine,self).__init__( expr ) - # suppress whitespace-stripping in contained parse expressions, but re-enable it on the Combine itself - if adjacent: - self.leaveWhitespace() - self.adjacent = adjacent - self.skipWhitespace = True - self.joinString = joinString - self.callPreparse = True - - def ignore( self, other ): - if self.adjacent: - ParserElement.ignore(self, other) - else: - super( Combine, self).ignore( other ) - return self - - def postParse( self, instring, loc, tokenlist ): - retToks = tokenlist.copy() - del retToks[:] - retToks += ParseResults([ "".join(tokenlist._asStringList(self.joinString)) ], modal=self.modalResults) - - if self.resultsName and retToks.haskeys(): - return [ retToks ] - else: - return retToks - -class Group(TokenConverter): - """ - Converter to return the matched tokens as a list - useful for returning tokens of C{L{ZeroOrMore}} and C{L{OneOrMore}} expressions. - - Example:: - ident = Word(alphas) - num = Word(nums) - term = ident | num - func = ident + Optional(delimitedList(term)) - print(func.parseString("fn a,b,100")) # -> ['fn', 'a', 'b', '100'] - - func = ident + Group(Optional(delimitedList(term))) - print(func.parseString("fn a,b,100")) # -> ['fn', ['a', 'b', '100']] - """ - def __init__( self, expr ): - super(Group,self).__init__( expr ) - self.saveAsList = True - - def postParse( self, instring, loc, tokenlist ): - return [ tokenlist ] - -class Dict(TokenConverter): - """ - Converter to return a repetitive expression as a list, but also as a dictionary. - Each element can also be referenced using the first token in the expression as its key. - Useful for tabular report scraping when the first column can be used as a item key. - - Example:: - data_word = Word(alphas) - label = data_word + FollowedBy(':') - attr_expr = Group(label + Suppress(':') + OneOrMore(data_word).setParseAction(' '.join)) - - text = "shape: SQUARE posn: upper left color: light blue texture: burlap" - attr_expr = (label + Suppress(':') + OneOrMore(data_word, stopOn=label).setParseAction(' '.join)) - - # print attributes as plain groups - print(OneOrMore(attr_expr).parseString(text).dump()) - - # instead of OneOrMore(expr), parse using Dict(OneOrMore(Group(expr))) - Dict will auto-assign names - result = Dict(OneOrMore(Group(attr_expr))).parseString(text) - print(result.dump()) - - # access named fields as dict entries, or output as dict - print(result['shape']) - print(result.asDict()) - prints:: - ['shape', 'SQUARE', 'posn', 'upper left', 'color', 'light blue', 'texture', 'burlap'] - - [['shape', 'SQUARE'], ['posn', 'upper left'], ['color', 'light blue'], ['texture', 'burlap']] - - color: light blue - - posn: upper left - - shape: SQUARE - - texture: burlap - SQUARE - {'color': 'light blue', 'posn': 'upper left', 'texture': 'burlap', 'shape': 'SQUARE'} - See more examples at L{ParseResults} of accessing fields by results name. - """ - def __init__( self, expr ): - super(Dict,self).__init__( expr ) - self.saveAsList = True - - def postParse( self, instring, loc, tokenlist ): - for i,tok in enumerate(tokenlist): - if len(tok) == 0: - continue - ikey = tok[0] - if isinstance(ikey,int): - ikey = _ustr(tok[0]).strip() - if len(tok)==1: - tokenlist[ikey] = _ParseResultsWithOffset("",i) - elif len(tok)==2 and not isinstance(tok[1],ParseResults): - tokenlist[ikey] = _ParseResultsWithOffset(tok[1],i) - else: - dictvalue = tok.copy() #ParseResults(i) - del dictvalue[0] - if len(dictvalue)!= 1 or (isinstance(dictvalue,ParseResults) and dictvalue.haskeys()): - tokenlist[ikey] = _ParseResultsWithOffset(dictvalue,i) - else: - tokenlist[ikey] = _ParseResultsWithOffset(dictvalue[0],i) - - if self.resultsName: - return [ tokenlist ] - else: - return tokenlist - - -class Suppress(TokenConverter): - """ - Converter for ignoring the results of a parsed expression. - - Example:: - source = "a, b, c,d" - wd = Word(alphas) - wd_list1 = wd + ZeroOrMore(',' + wd) - print(wd_list1.parseString(source)) - - # often, delimiters that are useful during parsing are just in the - # way afterward - use Suppress to keep them out of the parsed output - wd_list2 = wd + ZeroOrMore(Suppress(',') + wd) - print(wd_list2.parseString(source)) - prints:: - ['a', ',', 'b', ',', 'c', ',', 'd'] - ['a', 'b', 'c', 'd'] - (See also L{delimitedList}.) - """ - def postParse( self, instring, loc, tokenlist ): - return [] - - def suppress( self ): - return self - - -class OnlyOnce(object): - """ - Wrapper for parse actions, to ensure they are only called once. - """ - def __init__(self, methodCall): - self.callable = _trim_arity(methodCall) - self.called = False - def __call__(self,s,l,t): - if not self.called: - results = self.callable(s,l,t) - self.called = True - return results - raise ParseException(s,l,"") - def reset(self): - self.called = False - -def traceParseAction(f): - """ - Decorator for debugging parse actions. - - When the parse action is called, this decorator will print C{">> entering I{method-name}(line:I{current_source_line}, I{parse_location}, I{matched_tokens})".} - When the parse action completes, the decorator will print C{"<<"} followed by the returned value, or any exception that the parse action raised. - - Example:: - wd = Word(alphas) - - @traceParseAction - def remove_duplicate_chars(tokens): - return ''.join(sorted(set(''.join(tokens)))) - - wds = OneOrMore(wd).setParseAction(remove_duplicate_chars) - print(wds.parseString("slkdjs sld sldd sdlf sdljf")) - prints:: - >>entering remove_duplicate_chars(line: 'slkdjs sld sldd sdlf sdljf', 0, (['slkdjs', 'sld', 'sldd', 'sdlf', 'sdljf'], {})) - <3: - thisFunc = paArgs[0].__class__.__name__ + '.' + thisFunc - sys.stderr.write( ">>entering %s(line: '%s', %d, %r)\n" % (thisFunc,line(l,s),l,t) ) - try: - ret = f(*paArgs) - except Exception as exc: - sys.stderr.write( "< ['aa', 'bb', 'cc'] - delimitedList(Word(hexnums), delim=':', combine=True).parseString("AA:BB:CC:DD:EE") # -> ['AA:BB:CC:DD:EE'] - """ - dlName = _ustr(expr)+" ["+_ustr(delim)+" "+_ustr(expr)+"]..." - if combine: - return Combine( expr + ZeroOrMore( delim + expr ) ).setName(dlName) - else: - return ( expr + ZeroOrMore( Suppress( delim ) + expr ) ).setName(dlName) - -def countedArray( expr, intExpr=None ): - """ - Helper to define a counted list of expressions. - This helper defines a pattern of the form:: - integer expr expr expr... - where the leading integer tells how many expr expressions follow. - The matched tokens returns the array of expr tokens as a list - the leading count token is suppressed. - - If C{intExpr} is specified, it should be a pyparsing expression that produces an integer value. - - Example:: - countedArray(Word(alphas)).parseString('2 ab cd ef') # -> ['ab', 'cd'] - - # in this parser, the leading integer value is given in binary, - # '10' indicating that 2 values are in the array - binaryConstant = Word('01').setParseAction(lambda t: int(t[0], 2)) - countedArray(Word(alphas), intExpr=binaryConstant).parseString('10 ab cd ef') # -> ['ab', 'cd'] - """ - arrayExpr = Forward() - def countFieldParseAction(s,l,t): - n = t[0] - arrayExpr << (n and Group(And([expr]*n)) or Group(empty)) - return [] - if intExpr is None: - intExpr = Word(nums).setParseAction(lambda t:int(t[0])) - else: - intExpr = intExpr.copy() - intExpr.setName("arrayLen") - intExpr.addParseAction(countFieldParseAction, callDuringTry=True) - return ( intExpr + arrayExpr ).setName('(len) ' + _ustr(expr) + '...') - -def _flatten(L): - ret = [] - for i in L: - if isinstance(i,list): - ret.extend(_flatten(i)) - else: - ret.append(i) - return ret - -def matchPreviousLiteral(expr): - """ - Helper to define an expression that is indirectly defined from - the tokens matched in a previous expression, that is, it looks - for a 'repeat' of a previous expression. For example:: - first = Word(nums) - second = matchPreviousLiteral(first) - matchExpr = first + ":" + second - will match C{"1:1"}, but not C{"1:2"}. Because this matches a - previous literal, will also match the leading C{"1:1"} in C{"1:10"}. - If this is not desired, use C{matchPreviousExpr}. - Do I{not} use with packrat parsing enabled. - """ - rep = Forward() - def copyTokenToRepeater(s,l,t): - if t: - if len(t) == 1: - rep << t[0] - else: - # flatten t tokens - tflat = _flatten(t.asList()) - rep << And(Literal(tt) for tt in tflat) - else: - rep << Empty() - expr.addParseAction(copyTokenToRepeater, callDuringTry=True) - rep.setName('(prev) ' + _ustr(expr)) - return rep - -def matchPreviousExpr(expr): - """ - Helper to define an expression that is indirectly defined from - the tokens matched in a previous expression, that is, it looks - for a 'repeat' of a previous expression. For example:: - first = Word(nums) - second = matchPreviousExpr(first) - matchExpr = first + ":" + second - will match C{"1:1"}, but not C{"1:2"}. Because this matches by - expressions, will I{not} match the leading C{"1:1"} in C{"1:10"}; - the expressions are evaluated first, and then compared, so - C{"1"} is compared with C{"10"}. - Do I{not} use with packrat parsing enabled. - """ - rep = Forward() - e2 = expr.copy() - rep <<= e2 - def copyTokenToRepeater(s,l,t): - matchTokens = _flatten(t.asList()) - def mustMatchTheseTokens(s,l,t): - theseTokens = _flatten(t.asList()) - if theseTokens != matchTokens: - raise ParseException("",0,"") - rep.setParseAction( mustMatchTheseTokens, callDuringTry=True ) - expr.addParseAction(copyTokenToRepeater, callDuringTry=True) - rep.setName('(prev) ' + _ustr(expr)) - return rep - -def _escapeRegexRangeChars(s): - #~ escape these chars: ^-] - for c in r"\^-]": - s = s.replace(c,_bslash+c) - s = s.replace("\n",r"\n") - s = s.replace("\t",r"\t") - return _ustr(s) - -def oneOf( strs, caseless=False, useRegex=True ): - """ - Helper to quickly define a set of alternative Literals, and makes sure to do - longest-first testing when there is a conflict, regardless of the input order, - but returns a C{L{MatchFirst}} for best performance. - - Parameters: - - strs - a string of space-delimited literals, or a collection of string literals - - caseless - (default=C{False}) - treat all literals as caseless - - useRegex - (default=C{True}) - as an optimization, will generate a Regex - object; otherwise, will generate a C{MatchFirst} object (if C{caseless=True}, or - if creating a C{Regex} raises an exception) - - Example:: - comp_oper = oneOf("< = > <= >= !=") - var = Word(alphas) - number = Word(nums) - term = var | number - comparison_expr = term + comp_oper + term - print(comparison_expr.searchString("B = 12 AA=23 B<=AA AA>12")) - prints:: - [['B', '=', '12'], ['AA', '=', '23'], ['B', '<=', 'AA'], ['AA', '>', '12']] - """ - if caseless: - isequal = ( lambda a,b: a.upper() == b.upper() ) - masks = ( lambda a,b: b.upper().startswith(a.upper()) ) - parseElementClass = CaselessLiteral - else: - isequal = ( lambda a,b: a == b ) - masks = ( lambda a,b: b.startswith(a) ) - parseElementClass = Literal - - symbols = [] - if isinstance(strs,basestring): - symbols = strs.split() - elif isinstance(strs, Iterable): - symbols = list(strs) - else: - warnings.warn("Invalid argument to oneOf, expected string or iterable", - SyntaxWarning, stacklevel=2) - if not symbols: - return NoMatch() - - i = 0 - while i < len(symbols)-1: - cur = symbols[i] - for j,other in enumerate(symbols[i+1:]): - if ( isequal(other, cur) ): - del symbols[i+j+1] - break - elif ( masks(cur, other) ): - del symbols[i+j+1] - symbols.insert(i,other) - cur = other - break - else: - i += 1 - - if not caseless and useRegex: - #~ print (strs,"->", "|".join( [ _escapeRegexChars(sym) for sym in symbols] )) - try: - if len(symbols)==len("".join(symbols)): - return Regex( "[%s]" % "".join(_escapeRegexRangeChars(sym) for sym in symbols) ).setName(' | '.join(symbols)) - else: - return Regex( "|".join(re.escape(sym) for sym in symbols) ).setName(' | '.join(symbols)) - except Exception: - warnings.warn("Exception creating Regex for oneOf, building MatchFirst", - SyntaxWarning, stacklevel=2) - - - # last resort, just use MatchFirst - return MatchFirst(parseElementClass(sym) for sym in symbols).setName(' | '.join(symbols)) - -def dictOf( key, value ): - """ - Helper to easily and clearly define a dictionary by specifying the respective patterns - for the key and value. Takes care of defining the C{L{Dict}}, C{L{ZeroOrMore}}, and C{L{Group}} tokens - in the proper order. The key pattern can include delimiting markers or punctuation, - as long as they are suppressed, thereby leaving the significant key text. The value - pattern can include named results, so that the C{Dict} results can include named token - fields. - - Example:: - text = "shape: SQUARE posn: upper left color: light blue texture: burlap" - attr_expr = (label + Suppress(':') + OneOrMore(data_word, stopOn=label).setParseAction(' '.join)) - print(OneOrMore(attr_expr).parseString(text).dump()) - - attr_label = label - attr_value = Suppress(':') + OneOrMore(data_word, stopOn=label).setParseAction(' '.join) - - # similar to Dict, but simpler call format - result = dictOf(attr_label, attr_value).parseString(text) - print(result.dump()) - print(result['shape']) - print(result.shape) # object attribute access works too - print(result.asDict()) - prints:: - [['shape', 'SQUARE'], ['posn', 'upper left'], ['color', 'light blue'], ['texture', 'burlap']] - - color: light blue - - posn: upper left - - shape: SQUARE - - texture: burlap - SQUARE - SQUARE - {'color': 'light blue', 'shape': 'SQUARE', 'posn': 'upper left', 'texture': 'burlap'} - """ - return Dict( ZeroOrMore( Group ( key + value ) ) ) - -def originalTextFor(expr, asString=True): - """ - Helper to return the original, untokenized text for a given expression. Useful to - restore the parsed fields of an HTML start tag into the raw tag text itself, or to - revert separate tokens with intervening whitespace back to the original matching - input text. By default, returns astring containing the original parsed text. - - If the optional C{asString} argument is passed as C{False}, then the return value is a - C{L{ParseResults}} containing any results names that were originally matched, and a - single token containing the original matched text from the input string. So if - the expression passed to C{L{originalTextFor}} contains expressions with defined - results names, you must set C{asString} to C{False} if you want to preserve those - results name values. - - Example:: - src = "this is test bold text normal text " - for tag in ("b","i"): - opener,closer = makeHTMLTags(tag) - patt = originalTextFor(opener + SkipTo(closer) + closer) - print(patt.searchString(src)[0]) - prints:: - [' bold text '] - ['text'] - """ - locMarker = Empty().setParseAction(lambda s,loc,t: loc) - endlocMarker = locMarker.copy() - endlocMarker.callPreparse = False - matchExpr = locMarker("_original_start") + expr + endlocMarker("_original_end") - if asString: - extractText = lambda s,l,t: s[t._original_start:t._original_end] - else: - def extractText(s,l,t): - t[:] = [s[t.pop('_original_start'):t.pop('_original_end')]] - matchExpr.setParseAction(extractText) - matchExpr.ignoreExprs = expr.ignoreExprs - return matchExpr - -def ungroup(expr): - """ - Helper to undo pyparsing's default grouping of And expressions, even - if all but one are non-empty. - """ - return TokenConverter(expr).setParseAction(lambda t:t[0]) - -def locatedExpr(expr): - """ - Helper to decorate a returned token with its starting and ending locations in the input string. - This helper adds the following results names: - - locn_start = location where matched expression begins - - locn_end = location where matched expression ends - - value = the actual parsed results - - Be careful if the input text contains C{} characters, you may want to call - C{L{ParserElement.parseWithTabs}} - - Example:: - wd = Word(alphas) - for match in locatedExpr(wd).searchString("ljsdf123lksdjjf123lkkjj1222"): - print(match) - prints:: - [[0, 'ljsdf', 5]] - [[8, 'lksdjjf', 15]] - [[18, 'lkkjj', 23]] - """ - locator = Empty().setParseAction(lambda s,l,t: l) - return Group(locator("locn_start") + expr("value") + locator.copy().leaveWhitespace()("locn_end")) - - -# convenience constants for positional expressions -empty = Empty().setName("empty") -lineStart = LineStart().setName("lineStart") -lineEnd = LineEnd().setName("lineEnd") -stringStart = StringStart().setName("stringStart") -stringEnd = StringEnd().setName("stringEnd") - -_escapedPunc = Word( _bslash, r"\[]-*.$+^?()~ ", exact=2 ).setParseAction(lambda s,l,t:t[0][1]) -_escapedHexChar = Regex(r"\\0?[xX][0-9a-fA-F]+").setParseAction(lambda s,l,t:unichr(int(t[0].lstrip(r'\0x'),16))) -_escapedOctChar = Regex(r"\\0[0-7]+").setParseAction(lambda s,l,t:unichr(int(t[0][1:],8))) -_singleChar = _escapedPunc | _escapedHexChar | _escapedOctChar | CharsNotIn(r'\]', exact=1) -_charRange = Group(_singleChar + Suppress("-") + _singleChar) -_reBracketExpr = Literal("[") + Optional("^").setResultsName("negate") + Group( OneOrMore( _charRange | _singleChar ) ).setResultsName("body") + "]" - -def srange(s): - r""" - Helper to easily define string ranges for use in Word construction. Borrows - syntax from regexp '[]' string range definitions:: - srange("[0-9]") -> "0123456789" - srange("[a-z]") -> "abcdefghijklmnopqrstuvwxyz" - srange("[a-z$_]") -> "abcdefghijklmnopqrstuvwxyz$_" - The input string must be enclosed in []'s, and the returned string is the expanded - character set joined into a single string. - The values enclosed in the []'s may be: - - a single character - - an escaped character with a leading backslash (such as C{\-} or C{\]}) - - an escaped hex character with a leading C{'\x'} (C{\x21}, which is a C{'!'} character) - (C{\0x##} is also supported for backwards compatibility) - - an escaped octal character with a leading C{'\0'} (C{\041}, which is a C{'!'} character) - - a range of any of the above, separated by a dash (C{'a-z'}, etc.) - - any combination of the above (C{'aeiouy'}, C{'a-zA-Z0-9_$'}, etc.) - """ - _expanded = lambda p: p if not isinstance(p,ParseResults) else ''.join(unichr(c) for c in range(ord(p[0]),ord(p[1])+1)) - try: - return "".join(_expanded(part) for part in _reBracketExpr.parseString(s).body) - except Exception: - return "" - -def matchOnlyAtCol(n): - """ - Helper method for defining parse actions that require matching at a specific - column in the input text. - """ - def verifyCol(strg,locn,toks): - if col(locn,strg) != n: - raise ParseException(strg,locn,"matched token not at column %d" % n) - return verifyCol - -def replaceWith(replStr): - """ - Helper method for common parse actions that simply return a literal value. Especially - useful when used with C{L{transformString}()}. - - Example:: - num = Word(nums).setParseAction(lambda toks: int(toks[0])) - na = oneOf("N/A NA").setParseAction(replaceWith(math.nan)) - term = na | num - - OneOrMore(term).parseString("324 234 N/A 234") # -> [324, 234, nan, 234] - """ - return lambda s,l,t: [replStr] - -def removeQuotes(s,l,t): - """ - Helper parse action for removing quotation marks from parsed quoted strings. - - Example:: - # by default, quotation marks are included in parsed results - quotedString.parseString("'Now is the Winter of our Discontent'") # -> ["'Now is the Winter of our Discontent'"] - - # use removeQuotes to strip quotation marks from parsed results - quotedString.setParseAction(removeQuotes) - quotedString.parseString("'Now is the Winter of our Discontent'") # -> ["Now is the Winter of our Discontent"] - """ - return t[0][1:-1] - -def tokenMap(func, *args): - """ - Helper to define a parse action by mapping a function to all elements of a ParseResults list.If any additional - args are passed, they are forwarded to the given function as additional arguments after - the token, as in C{hex_integer = Word(hexnums).setParseAction(tokenMap(int, 16))}, which will convert the - parsed data to an integer using base 16. - - Example (compare the last to example in L{ParserElement.transformString}:: - hex_ints = OneOrMore(Word(hexnums)).setParseAction(tokenMap(int, 16)) - hex_ints.runTests(''' - 00 11 22 aa FF 0a 0d 1a - ''') - - upperword = Word(alphas).setParseAction(tokenMap(str.upper)) - OneOrMore(upperword).runTests(''' - my kingdom for a horse - ''') - - wd = Word(alphas).setParseAction(tokenMap(str.title)) - OneOrMore(wd).setParseAction(' '.join).runTests(''' - now is the winter of our discontent made glorious summer by this sun of york - ''') - prints:: - 00 11 22 aa FF 0a 0d 1a - [0, 17, 34, 170, 255, 10, 13, 26] - - my kingdom for a horse - ['MY', 'KINGDOM', 'FOR', 'A', 'HORSE'] - - now is the winter of our discontent made glorious summer by this sun of york - ['Now Is The Winter Of Our Discontent Made Glorious Summer By This Sun Of York'] - """ - def pa(s,l,t): - return [func(tokn, *args) for tokn in t] - - try: - func_name = getattr(func, '__name__', - getattr(func, '__class__').__name__) - except Exception: - func_name = str(func) - pa.__name__ = func_name - - return pa - -upcaseTokens = tokenMap(lambda t: _ustr(t).upper()) -"""(Deprecated) Helper parse action to convert tokens to upper case. Deprecated in favor of L{pyparsing_common.upcaseTokens}""" - -downcaseTokens = tokenMap(lambda t: _ustr(t).lower()) -"""(Deprecated) Helper parse action to convert tokens to lower case. Deprecated in favor of L{pyparsing_common.downcaseTokens}""" - -def _makeTags(tagStr, xml): - """Internal helper to construct opening and closing tag expressions, given a tag name""" - if isinstance(tagStr,basestring): - resname = tagStr - tagStr = Keyword(tagStr, caseless=not xml) - else: - resname = tagStr.name - - tagAttrName = Word(alphas,alphanums+"_-:") - if (xml): - tagAttrValue = dblQuotedString.copy().setParseAction( removeQuotes ) - openTag = Suppress("<") + tagStr("tag") + \ - Dict(ZeroOrMore(Group( tagAttrName + Suppress("=") + tagAttrValue ))) + \ - Optional("/",default=[False]).setResultsName("empty").setParseAction(lambda s,l,t:t[0]=='/') + Suppress(">") - else: - printablesLessRAbrack = "".join(c for c in printables if c not in ">") - tagAttrValue = quotedString.copy().setParseAction( removeQuotes ) | Word(printablesLessRAbrack) - openTag = Suppress("<") + tagStr("tag") + \ - Dict(ZeroOrMore(Group( tagAttrName.setParseAction(downcaseTokens) + \ - Optional( Suppress("=") + tagAttrValue ) ))) + \ - Optional("/",default=[False]).setResultsName("empty").setParseAction(lambda s,l,t:t[0]=='/') + Suppress(">") - closeTag = Combine(_L("") - - openTag = openTag.setResultsName("start"+"".join(resname.replace(":"," ").title().split())).setName("<%s>" % resname) - closeTag = closeTag.setResultsName("end"+"".join(resname.replace(":"," ").title().split())).setName("" % resname) - openTag.tag = resname - closeTag.tag = resname - return openTag, closeTag - -def makeHTMLTags(tagStr): - """ - Helper to construct opening and closing tag expressions for HTML, given a tag name. Matches - tags in either upper or lower case, attributes with namespaces and with quoted or unquoted values. - - Example:: - text = 'More info at the pyparsing wiki page' - # makeHTMLTags returns pyparsing expressions for the opening and closing tags as a 2-tuple - a,a_end = makeHTMLTags("A") - link_expr = a + SkipTo(a_end)("link_text") + a_end - - for link in link_expr.searchString(text): - # attributes in the tag (like "href" shown here) are also accessible as named results - print(link.link_text, '->', link.href) - prints:: - pyparsing -> http://pyparsing.wikispaces.com - """ - return _makeTags( tagStr, False ) - -def makeXMLTags(tagStr): - """ - Helper to construct opening and closing tag expressions for XML, given a tag name. Matches - tags only in the given upper/lower case. - - Example: similar to L{makeHTMLTags} - """ - return _makeTags( tagStr, True ) - -def withAttribute(*args,**attrDict): - """ - Helper to create a validating parse action to be used with start tags created - with C{L{makeXMLTags}} or C{L{makeHTMLTags}}. Use C{withAttribute} to qualify a starting tag - with a required attribute value, to avoid false matches on common tags such as - C{} or C{
}. - - Call C{withAttribute} with a series of attribute names and values. Specify the list - of filter attributes names and values as: - - keyword arguments, as in C{(align="right")}, or - - as an explicit dict with C{**} operator, when an attribute name is also a Python - reserved word, as in C{**{"class":"Customer", "align":"right"}} - - a list of name-value tuples, as in ( ("ns1:class", "Customer"), ("ns2:align","right") ) - For attribute names with a namespace prefix, you must use the second form. Attribute - names are matched insensitive to upper/lower case. - - If just testing for C{class} (with or without a namespace), use C{L{withClass}}. - - To verify that the attribute exists, but without specifying a value, pass - C{withAttribute.ANY_VALUE} as the value. - - Example:: - html = ''' -
- Some text -
1 4 0 1 0
-
1,3 2,3 1,1
-
this has no type
-
- - ''' - div,div_end = makeHTMLTags("div") - - # only match div tag having a type attribute with value "grid" - div_grid = div().setParseAction(withAttribute(type="grid")) - grid_expr = div_grid + SkipTo(div | div_end)("body") - for grid_header in grid_expr.searchString(html): - print(grid_header.body) - - # construct a match with any div tag having a type attribute, regardless of the value - div_any_type = div().setParseAction(withAttribute(type=withAttribute.ANY_VALUE)) - div_expr = div_any_type + SkipTo(div | div_end)("body") - for div_header in div_expr.searchString(html): - print(div_header.body) - prints:: - 1 4 0 1 0 - - 1 4 0 1 0 - 1,3 2,3 1,1 - """ - if args: - attrs = args[:] - else: - attrs = attrDict.items() - attrs = [(k,v) for k,v in attrs] - def pa(s,l,tokens): - for attrName,attrValue in attrs: - if attrName not in tokens: - raise ParseException(s,l,"no matching attribute " + attrName) - if attrValue != withAttribute.ANY_VALUE and tokens[attrName] != attrValue: - raise ParseException(s,l,"attribute '%s' has value '%s', must be '%s'" % - (attrName, tokens[attrName], attrValue)) - return pa -withAttribute.ANY_VALUE = object() - -def withClass(classname, namespace=''): - """ - Simplified version of C{L{withAttribute}} when matching on a div class - made - difficult because C{class} is a reserved word in Python. - - Example:: - html = ''' -
- Some text -
1 4 0 1 0
-
1,3 2,3 1,1
-
this <div> has no class
-
- - ''' - div,div_end = makeHTMLTags("div") - div_grid = div().setParseAction(withClass("grid")) - - grid_expr = div_grid + SkipTo(div | div_end)("body") - for grid_header in grid_expr.searchString(html): - print(grid_header.body) - - div_any_type = div().setParseAction(withClass(withAttribute.ANY_VALUE)) - div_expr = div_any_type + SkipTo(div | div_end)("body") - for div_header in div_expr.searchString(html): - print(div_header.body) - prints:: - 1 4 0 1 0 - - 1 4 0 1 0 - 1,3 2,3 1,1 - """ - classattr = "%s:class" % namespace if namespace else "class" - return withAttribute(**{classattr : classname}) - -opAssoc = _Constants() -opAssoc.LEFT = object() -opAssoc.RIGHT = object() - -def infixNotation( baseExpr, opList, lpar=Suppress('('), rpar=Suppress(')') ): - """ - Helper method for constructing grammars of expressions made up of - operators working in a precedence hierarchy. Operators may be unary or - binary, left- or right-associative. Parse actions can also be attached - to operator expressions. The generated parser will also recognize the use - of parentheses to override operator precedences (see example below). - - Note: if you define a deep operator list, you may see performance issues - when using infixNotation. See L{ParserElement.enablePackrat} for a - mechanism to potentially improve your parser performance. - - Parameters: - - baseExpr - expression representing the most basic element for the nested - - opList - list of tuples, one for each operator precedence level in the - expression grammar; each tuple is of the form - (opExpr, numTerms, rightLeftAssoc, parseAction), where: - - opExpr is the pyparsing expression for the operator; - may also be a string, which will be converted to a Literal; - if numTerms is 3, opExpr is a tuple of two expressions, for the - two operators separating the 3 terms - - numTerms is the number of terms for this operator (must - be 1, 2, or 3) - - rightLeftAssoc is the indicator whether the operator is - right or left associative, using the pyparsing-defined - constants C{opAssoc.RIGHT} and C{opAssoc.LEFT}. - - parseAction is the parse action to be associated with - expressions matching this operator expression (the - parse action tuple member may be omitted); if the parse action - is passed a tuple or list of functions, this is equivalent to - calling C{setParseAction(*fn)} (L{ParserElement.setParseAction}) - - lpar - expression for matching left-parentheses (default=C{Suppress('(')}) - - rpar - expression for matching right-parentheses (default=C{Suppress(')')}) - - Example:: - # simple example of four-function arithmetic with ints and variable names - integer = pyparsing_common.signed_integer - varname = pyparsing_common.identifier - - arith_expr = infixNotation(integer | varname, - [ - ('-', 1, opAssoc.RIGHT), - (oneOf('* /'), 2, opAssoc.LEFT), - (oneOf('+ -'), 2, opAssoc.LEFT), - ]) - - arith_expr.runTests(''' - 5+3*6 - (5+3)*6 - -2--11 - ''', fullDump=False) - prints:: - 5+3*6 - [[5, '+', [3, '*', 6]]] - - (5+3)*6 - [[[5, '+', 3], '*', 6]] - - -2--11 - [[['-', 2], '-', ['-', 11]]] - """ - ret = Forward() - lastExpr = baseExpr | ( lpar + ret + rpar ) - for i,operDef in enumerate(opList): - opExpr,arity,rightLeftAssoc,pa = (operDef + (None,))[:4] - termName = "%s term" % opExpr if arity < 3 else "%s%s term" % opExpr - if arity == 3: - if opExpr is None or len(opExpr) != 2: - raise ValueError("if numterms=3, opExpr must be a tuple or list of two expressions") - opExpr1, opExpr2 = opExpr - thisExpr = Forward().setName(termName) - if rightLeftAssoc == opAssoc.LEFT: - if arity == 1: - matchExpr = FollowedBy(lastExpr + opExpr) + Group( lastExpr + OneOrMore( opExpr ) ) - elif arity == 2: - if opExpr is not None: - matchExpr = FollowedBy(lastExpr + opExpr + lastExpr) + Group( lastExpr + OneOrMore( opExpr + lastExpr ) ) - else: - matchExpr = FollowedBy(lastExpr+lastExpr) + Group( lastExpr + OneOrMore(lastExpr) ) - elif arity == 3: - matchExpr = FollowedBy(lastExpr + opExpr1 + lastExpr + opExpr2 + lastExpr) + \ - Group( lastExpr + opExpr1 + lastExpr + opExpr2 + lastExpr ) - else: - raise ValueError("operator must be unary (1), binary (2), or ternary (3)") - elif rightLeftAssoc == opAssoc.RIGHT: - if arity == 1: - # try to avoid LR with this extra test - if not isinstance(opExpr, Optional): - opExpr = Optional(opExpr) - matchExpr = FollowedBy(opExpr.expr + thisExpr) + Group( opExpr + thisExpr ) - elif arity == 2: - if opExpr is not None: - matchExpr = FollowedBy(lastExpr + opExpr + thisExpr) + Group( lastExpr + OneOrMore( opExpr + thisExpr ) ) - else: - matchExpr = FollowedBy(lastExpr + thisExpr) + Group( lastExpr + OneOrMore( thisExpr ) ) - elif arity == 3: - matchExpr = FollowedBy(lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr) + \ - Group( lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr ) - else: - raise ValueError("operator must be unary (1), binary (2), or ternary (3)") - else: - raise ValueError("operator must indicate right or left associativity") - if pa: - if isinstance(pa, (tuple, list)): - matchExpr.setParseAction(*pa) - else: - matchExpr.setParseAction(pa) - thisExpr <<= ( matchExpr.setName(termName) | lastExpr ) - lastExpr = thisExpr - ret <<= lastExpr - return ret - -operatorPrecedence = infixNotation -"""(Deprecated) Former name of C{L{infixNotation}}, will be dropped in a future release.""" - -dblQuotedString = Combine(Regex(r'"(?:[^"\n\r\\]|(?:"")|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*')+'"').setName("string enclosed in double quotes") -sglQuotedString = Combine(Regex(r"'(?:[^'\n\r\\]|(?:'')|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*")+"'").setName("string enclosed in single quotes") -quotedString = Combine(Regex(r'"(?:[^"\n\r\\]|(?:"")|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*')+'"'| - Regex(r"'(?:[^'\n\r\\]|(?:'')|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*")+"'").setName("quotedString using single or double quotes") -unicodeString = Combine(_L('u') + quotedString.copy()).setName("unicode string literal") - -def nestedExpr(opener="(", closer=")", content=None, ignoreExpr=quotedString.copy()): - """ - Helper method for defining nested lists enclosed in opening and closing - delimiters ("(" and ")" are the default). - - Parameters: - - opener - opening character for a nested list (default=C{"("}); can also be a pyparsing expression - - closer - closing character for a nested list (default=C{")"}); can also be a pyparsing expression - - content - expression for items within the nested lists (default=C{None}) - - ignoreExpr - expression for ignoring opening and closing delimiters (default=C{quotedString}) - - If an expression is not provided for the content argument, the nested - expression will capture all whitespace-delimited content between delimiters - as a list of separate values. - - Use the C{ignoreExpr} argument to define expressions that may contain - opening or closing characters that should not be treated as opening - or closing characters for nesting, such as quotedString or a comment - expression. Specify multiple expressions using an C{L{Or}} or C{L{MatchFirst}}. - The default is L{quotedString}, but if no expressions are to be ignored, - then pass C{None} for this argument. - - Example:: - data_type = oneOf("void int short long char float double") - decl_data_type = Combine(data_type + Optional(Word('*'))) - ident = Word(alphas+'_', alphanums+'_') - number = pyparsing_common.number - arg = Group(decl_data_type + ident) - LPAR,RPAR = map(Suppress, "()") - - code_body = nestedExpr('{', '}', ignoreExpr=(quotedString | cStyleComment)) - - c_function = (decl_data_type("type") - + ident("name") - + LPAR + Optional(delimitedList(arg), [])("args") + RPAR - + code_body("body")) - c_function.ignore(cStyleComment) - - source_code = ''' - int is_odd(int x) { - return (x%2); - } - - int dec_to_hex(char hchar) { - if (hchar >= '0' && hchar <= '9') { - return (ord(hchar)-ord('0')); - } else { - return (10+ord(hchar)-ord('A')); - } - } - ''' - for func in c_function.searchString(source_code): - print("%(name)s (%(type)s) args: %(args)s" % func) - - prints:: - is_odd (int) args: [['int', 'x']] - dec_to_hex (int) args: [['char', 'hchar']] - """ - if opener == closer: - raise ValueError("opening and closing strings cannot be the same") - if content is None: - if isinstance(opener,basestring) and isinstance(closer,basestring): - if len(opener) == 1 and len(closer)==1: - if ignoreExpr is not None: - content = (Combine(OneOrMore(~ignoreExpr + - CharsNotIn(opener+closer+ParserElement.DEFAULT_WHITE_CHARS,exact=1)) - ).setParseAction(lambda t:t[0].strip())) - else: - content = (empty.copy()+CharsNotIn(opener+closer+ParserElement.DEFAULT_WHITE_CHARS - ).setParseAction(lambda t:t[0].strip())) - else: - if ignoreExpr is not None: - content = (Combine(OneOrMore(~ignoreExpr + - ~Literal(opener) + ~Literal(closer) + - CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS,exact=1)) - ).setParseAction(lambda t:t[0].strip())) - else: - content = (Combine(OneOrMore(~Literal(opener) + ~Literal(closer) + - CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS,exact=1)) - ).setParseAction(lambda t:t[0].strip())) - else: - raise ValueError("opening and closing arguments must be strings if no content expression is given") - ret = Forward() - if ignoreExpr is not None: - ret <<= Group( Suppress(opener) + ZeroOrMore( ignoreExpr | ret | content ) + Suppress(closer) ) - else: - ret <<= Group( Suppress(opener) + ZeroOrMore( ret | content ) + Suppress(closer) ) - ret.setName('nested %s%s expression' % (opener,closer)) - return ret - -def indentedBlock(blockStatementExpr, indentStack, indent=True): - """ - Helper method for defining space-delimited indentation blocks, such as - those used to define block statements in Python source code. - - Parameters: - - blockStatementExpr - expression defining syntax of statement that - is repeated within the indented block - - indentStack - list created by caller to manage indentation stack - (multiple statementWithIndentedBlock expressions within a single grammar - should share a common indentStack) - - indent - boolean indicating whether block must be indented beyond the - the current level; set to False for block of left-most statements - (default=C{True}) - - A valid block must contain at least one C{blockStatement}. - - Example:: - data = ''' - def A(z): - A1 - B = 100 - G = A2 - A2 - A3 - B - def BB(a,b,c): - BB1 - def BBA(): - bba1 - bba2 - bba3 - C - D - def spam(x,y): - def eggs(z): - pass - ''' - - - indentStack = [1] - stmt = Forward() - - identifier = Word(alphas, alphanums) - funcDecl = ("def" + identifier + Group( "(" + Optional( delimitedList(identifier) ) + ")" ) + ":") - func_body = indentedBlock(stmt, indentStack) - funcDef = Group( funcDecl + func_body ) - - rvalue = Forward() - funcCall = Group(identifier + "(" + Optional(delimitedList(rvalue)) + ")") - rvalue << (funcCall | identifier | Word(nums)) - assignment = Group(identifier + "=" + rvalue) - stmt << ( funcDef | assignment | identifier ) - - module_body = OneOrMore(stmt) - - parseTree = module_body.parseString(data) - parseTree.pprint() - prints:: - [['def', - 'A', - ['(', 'z', ')'], - ':', - [['A1'], [['B', '=', '100']], [['G', '=', 'A2']], ['A2'], ['A3']]], - 'B', - ['def', - 'BB', - ['(', 'a', 'b', 'c', ')'], - ':', - [['BB1'], [['def', 'BBA', ['(', ')'], ':', [['bba1'], ['bba2'], ['bba3']]]]]], - 'C', - 'D', - ['def', - 'spam', - ['(', 'x', 'y', ')'], - ':', - [[['def', 'eggs', ['(', 'z', ')'], ':', [['pass']]]]]]] - """ - def checkPeerIndent(s,l,t): - if l >= len(s): return - curCol = col(l,s) - if curCol != indentStack[-1]: - if curCol > indentStack[-1]: - raise ParseFatalException(s,l,"illegal nesting") - raise ParseException(s,l,"not a peer entry") - - def checkSubIndent(s,l,t): - curCol = col(l,s) - if curCol > indentStack[-1]: - indentStack.append( curCol ) - else: - raise ParseException(s,l,"not a subentry") - - def checkUnindent(s,l,t): - if l >= len(s): return - curCol = col(l,s) - if not(indentStack and curCol < indentStack[-1] and curCol <= indentStack[-2]): - raise ParseException(s,l,"not an unindent") - indentStack.pop() - - NL = OneOrMore(LineEnd().setWhitespaceChars("\t ").suppress()) - INDENT = (Empty() + Empty().setParseAction(checkSubIndent)).setName('INDENT') - PEER = Empty().setParseAction(checkPeerIndent).setName('') - UNDENT = Empty().setParseAction(checkUnindent).setName('UNINDENT') - if indent: - smExpr = Group( Optional(NL) + - #~ FollowedBy(blockStatementExpr) + - INDENT + (OneOrMore( PEER + Group(blockStatementExpr) + Optional(NL) )) + UNDENT) - else: - smExpr = Group( Optional(NL) + - (OneOrMore( PEER + Group(blockStatementExpr) + Optional(NL) )) ) - blockStatementExpr.ignore(_bslash + LineEnd()) - return smExpr.setName('indented block') - -alphas8bit = srange(r"[\0xc0-\0xd6\0xd8-\0xf6\0xf8-\0xff]") -punc8bit = srange(r"[\0xa1-\0xbf\0xd7\0xf7]") - -anyOpenTag,anyCloseTag = makeHTMLTags(Word(alphas,alphanums+"_:").setName('any tag')) -_htmlEntityMap = dict(zip("gt lt amp nbsp quot apos".split(),'><& "\'')) -commonHTMLEntity = Regex('&(?P' + '|'.join(_htmlEntityMap.keys()) +");").setName("common HTML entity") -def replaceHTMLEntity(t): - """Helper parser action to replace common HTML entities with their special characters""" - return _htmlEntityMap.get(t.entity) - -# it's easy to get these comment structures wrong - they're very common, so may as well make them available -cStyleComment = Combine(Regex(r"/\*(?:[^*]|\*(?!/))*") + '*/').setName("C style comment") -"Comment of the form C{/* ... */}" - -htmlComment = Regex(r"").setName("HTML comment") -"Comment of the form C{}" - -restOfLine = Regex(r".*").leaveWhitespace().setName("rest of line") -dblSlashComment = Regex(r"//(?:\\\n|[^\n])*").setName("// comment") -"Comment of the form C{// ... (to end of line)}" - -cppStyleComment = Combine(Regex(r"/\*(?:[^*]|\*(?!/))*") + '*/'| dblSlashComment).setName("C++ style comment") -"Comment of either form C{L{cStyleComment}} or C{L{dblSlashComment}}" - -javaStyleComment = cppStyleComment -"Same as C{L{cppStyleComment}}" - -pythonStyleComment = Regex(r"#.*").setName("Python style comment") -"Comment of the form C{# ... (to end of line)}" - -_commasepitem = Combine(OneOrMore(Word(printables, excludeChars=',') + - Optional( Word(" \t") + - ~Literal(",") + ~LineEnd() ) ) ).streamline().setName("commaItem") -commaSeparatedList = delimitedList( Optional( quotedString.copy() | _commasepitem, default="") ).setName("commaSeparatedList") -"""(Deprecated) Predefined expression of 1 or more printable words or quoted strings, separated by commas. - This expression is deprecated in favor of L{pyparsing_common.comma_separated_list}.""" - -# some other useful expressions - using lower-case class name since we are really using this as a namespace -class pyparsing_common: - """ - Here are some common low-level expressions that may be useful in jump-starting parser development: - - numeric forms (L{integers}, L{reals}, L{scientific notation}) - - common L{programming identifiers} - - network addresses (L{MAC}, L{IPv4}, L{IPv6}) - - ISO8601 L{dates} and L{datetime} - - L{UUID} - - L{comma-separated list} - Parse actions: - - C{L{convertToInteger}} - - C{L{convertToFloat}} - - C{L{convertToDate}} - - C{L{convertToDatetime}} - - C{L{stripHTMLTags}} - - C{L{upcaseTokens}} - - C{L{downcaseTokens}} - - Example:: - pyparsing_common.number.runTests(''' - # any int or real number, returned as the appropriate type - 100 - -100 - +100 - 3.14159 - 6.02e23 - 1e-12 - ''') - - pyparsing_common.fnumber.runTests(''' - # any int or real number, returned as float - 100 - -100 - +100 - 3.14159 - 6.02e23 - 1e-12 - ''') - - pyparsing_common.hex_integer.runTests(''' - # hex numbers - 100 - FF - ''') - - pyparsing_common.fraction.runTests(''' - # fractions - 1/2 - -3/4 - ''') - - pyparsing_common.mixed_integer.runTests(''' - # mixed fractions - 1 - 1/2 - -3/4 - 1-3/4 - ''') - - import uuid - pyparsing_common.uuid.setParseAction(tokenMap(uuid.UUID)) - pyparsing_common.uuid.runTests(''' - # uuid - 12345678-1234-5678-1234-567812345678 - ''') - prints:: - # any int or real number, returned as the appropriate type - 100 - [100] - - -100 - [-100] - - +100 - [100] - - 3.14159 - [3.14159] - - 6.02e23 - [6.02e+23] - - 1e-12 - [1e-12] - - # any int or real number, returned as float - 100 - [100.0] - - -100 - [-100.0] - - +100 - [100.0] - - 3.14159 - [3.14159] - - 6.02e23 - [6.02e+23] - - 1e-12 - [1e-12] - - # hex numbers - 100 - [256] - - FF - [255] - - # fractions - 1/2 - [0.5] - - -3/4 - [-0.75] - - # mixed fractions - 1 - [1] - - 1/2 - [0.5] - - -3/4 - [-0.75] - - 1-3/4 - [1.75] - - # uuid - 12345678-1234-5678-1234-567812345678 - [UUID('12345678-1234-5678-1234-567812345678')] - """ - - convertToInteger = tokenMap(int) - """ - Parse action for converting parsed integers to Python int - """ - - convertToFloat = tokenMap(float) - """ - Parse action for converting parsed numbers to Python float - """ - - integer = Word(nums).setName("integer").setParseAction(convertToInteger) - """expression that parses an unsigned integer, returns an int""" - - hex_integer = Word(hexnums).setName("hex integer").setParseAction(tokenMap(int,16)) - """expression that parses a hexadecimal integer, returns an int""" - - signed_integer = Regex(r'[+-]?\d+').setName("signed integer").setParseAction(convertToInteger) - """expression that parses an integer with optional leading sign, returns an int""" - - fraction = (signed_integer().setParseAction(convertToFloat) + '/' + signed_integer().setParseAction(convertToFloat)).setName("fraction") - """fractional expression of an integer divided by an integer, returns a float""" - fraction.addParseAction(lambda t: t[0]/t[-1]) - - mixed_integer = (fraction | signed_integer + Optional(Optional('-').suppress() + fraction)).setName("fraction or mixed integer-fraction") - """mixed integer of the form 'integer - fraction', with optional leading integer, returns float""" - mixed_integer.addParseAction(sum) - - real = Regex(r'[+-]?\d+\.\d*').setName("real number").setParseAction(convertToFloat) - """expression that parses a floating point number and returns a float""" - - sci_real = Regex(r'[+-]?\d+([eE][+-]?\d+|\.\d*([eE][+-]?\d+)?)').setName("real number with scientific notation").setParseAction(convertToFloat) - """expression that parses a floating point number with optional scientific notation and returns a float""" - - # streamlining this expression makes the docs nicer-looking - number = (sci_real | real | signed_integer).streamline() - """any numeric expression, returns the corresponding Python type""" - - fnumber = Regex(r'[+-]?\d+\.?\d*([eE][+-]?\d+)?').setName("fnumber").setParseAction(convertToFloat) - """any int or real number, returned as float""" - - identifier = Word(alphas+'_', alphanums+'_').setName("identifier") - """typical code identifier (leading alpha or '_', followed by 0 or more alphas, nums, or '_')""" - - ipv4_address = Regex(r'(25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})(\.(25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})){3}').setName("IPv4 address") - "IPv4 address (C{0.0.0.0 - 255.255.255.255})" - - _ipv6_part = Regex(r'[0-9a-fA-F]{1,4}').setName("hex_integer") - _full_ipv6_address = (_ipv6_part + (':' + _ipv6_part)*7).setName("full IPv6 address") - _short_ipv6_address = (Optional(_ipv6_part + (':' + _ipv6_part)*(0,6)) + "::" + Optional(_ipv6_part + (':' + _ipv6_part)*(0,6))).setName("short IPv6 address") - _short_ipv6_address.addCondition(lambda t: sum(1 for tt in t if pyparsing_common._ipv6_part.matches(tt)) < 8) - _mixed_ipv6_address = ("::ffff:" + ipv4_address).setName("mixed IPv6 address") - ipv6_address = Combine((_full_ipv6_address | _mixed_ipv6_address | _short_ipv6_address).setName("IPv6 address")).setName("IPv6 address") - "IPv6 address (long, short, or mixed form)" - - mac_address = Regex(r'[0-9a-fA-F]{2}([:.-])[0-9a-fA-F]{2}(?:\1[0-9a-fA-F]{2}){4}').setName("MAC address") - "MAC address xx:xx:xx:xx:xx (may also have '-' or '.' delimiters)" - - @staticmethod - def convertToDate(fmt="%Y-%m-%d"): - """ - Helper to create a parse action for converting parsed date string to Python datetime.date - - Params - - - fmt - format to be passed to datetime.strptime (default=C{"%Y-%m-%d"}) - - Example:: - date_expr = pyparsing_common.iso8601_date.copy() - date_expr.setParseAction(pyparsing_common.convertToDate()) - print(date_expr.parseString("1999-12-31")) - prints:: - [datetime.date(1999, 12, 31)] - """ - def cvt_fn(s,l,t): - try: - return datetime.strptime(t[0], fmt).date() - except ValueError as ve: - raise ParseException(s, l, str(ve)) - return cvt_fn - - @staticmethod - def convertToDatetime(fmt="%Y-%m-%dT%H:%M:%S.%f"): - """ - Helper to create a parse action for converting parsed datetime string to Python datetime.datetime - - Params - - - fmt - format to be passed to datetime.strptime (default=C{"%Y-%m-%dT%H:%M:%S.%f"}) - - Example:: - dt_expr = pyparsing_common.iso8601_datetime.copy() - dt_expr.setParseAction(pyparsing_common.convertToDatetime()) - print(dt_expr.parseString("1999-12-31T23:59:59.999")) - prints:: - [datetime.datetime(1999, 12, 31, 23, 59, 59, 999000)] - """ - def cvt_fn(s,l,t): - try: - return datetime.strptime(t[0], fmt) - except ValueError as ve: - raise ParseException(s, l, str(ve)) - return cvt_fn - - iso8601_date = Regex(r'(?P\d{4})(?:-(?P\d\d)(?:-(?P\d\d))?)?').setName("ISO8601 date") - "ISO8601 date (C{yyyy-mm-dd})" - - iso8601_datetime = Regex(r'(?P\d{4})-(?P\d\d)-(?P\d\d)[T ](?P\d\d):(?P\d\d)(:(?P\d\d(\.\d*)?)?)?(?PZ|[+-]\d\d:?\d\d)?').setName("ISO8601 datetime") - "ISO8601 datetime (C{yyyy-mm-ddThh:mm:ss.s(Z|+-00:00)}) - trailing seconds, milliseconds, and timezone optional; accepts separating C{'T'} or C{' '}" - - uuid = Regex(r'[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}').setName("UUID") - "UUID (C{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx})" - - _html_stripper = anyOpenTag.suppress() | anyCloseTag.suppress() - @staticmethod - def stripHTMLTags(s, l, tokens): - """ - Parse action to remove HTML tags from web page HTML source - - Example:: - # strip HTML links from normal text - text = 'More info at the
pyparsing wiki page' - td,td_end = makeHTMLTags("TD") - table_text = td + SkipTo(td_end).setParseAction(pyparsing_common.stripHTMLTags)("body") + td_end - - print(table_text.parseString(text).body) # -> 'More info at the pyparsing wiki page' - """ - return pyparsing_common._html_stripper.transformString(tokens[0]) - - _commasepitem = Combine(OneOrMore(~Literal(",") + ~LineEnd() + Word(printables, excludeChars=',') - + Optional( White(" \t") ) ) ).streamline().setName("commaItem") - comma_separated_list = delimitedList( Optional( quotedString.copy() | _commasepitem, default="") ).setName("comma separated list") - """Predefined expression of 1 or more printable words or quoted strings, separated by commas.""" - - upcaseTokens = staticmethod(tokenMap(lambda t: _ustr(t).upper())) - """Parse action to convert tokens to upper case.""" - - downcaseTokens = staticmethod(tokenMap(lambda t: _ustr(t).lower())) - """Parse action to convert tokens to lower case.""" - - -if __name__ == "__main__": - - selectToken = CaselessLiteral("select") - fromToken = CaselessLiteral("from") - - ident = Word(alphas, alphanums + "_$") - - columnName = delimitedList(ident, ".", combine=True).setParseAction(upcaseTokens) - columnNameList = Group(delimitedList(columnName)).setName("columns") - columnSpec = ('*' | columnNameList) - - tableName = delimitedList(ident, ".", combine=True).setParseAction(upcaseTokens) - tableNameList = Group(delimitedList(tableName)).setName("tables") - - simpleSQL = selectToken("command") + columnSpec("columns") + fromToken + tableNameList("tables") - - # demo runTests method, including embedded comments in test string - simpleSQL.runTests(""" - # '*' as column list and dotted table name - select * from SYS.XYZZY - - # caseless match on "SELECT", and casts back to "select" - SELECT * from XYZZY, ABC - - # list of column names, and mixed case SELECT keyword - Select AA,BB,CC from Sys.dual - - # multiple tables - Select A, B, C from Sys.dual, Table2 - - # invalid SELECT keyword - should fail - Xelect A, B, C from Sys.dual - - # incomplete command - should fail - Select - - # invalid column name - should fail - Select ^^^ frox Sys.dual - - """) - - pyparsing_common.number.runTests(""" - 100 - -100 - +100 - 3.14159 - 6.02e23 - 1e-12 - """) - - # any int or real number, returned as float - pyparsing_common.fnumber.runTests(""" - 100 - -100 - +100 - 3.14159 - 6.02e23 - 1e-12 - """) - - pyparsing_common.hex_integer.runTests(""" - 100 - FF - """) - - import uuid - pyparsing_common.uuid.setParseAction(tokenMap(uuid.UUID)) - pyparsing_common.uuid.runTests(""" - 12345678-1234-5678-1234-567812345678 - """) diff --git a/libs/common/setuptools/_vendor/six.py b/libs/common/setuptools/_vendor/six.py deleted file mode 100644 index 190c0239..00000000 --- a/libs/common/setuptools/_vendor/six.py +++ /dev/null @@ -1,868 +0,0 @@ -"""Utilities for writing code that runs on Python 2 and 3""" - -# Copyright (c) 2010-2015 Benjamin Peterson -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in all -# copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from __future__ import absolute_import - -import functools -import itertools -import operator -import sys -import types - -__author__ = "Benjamin Peterson " -__version__ = "1.10.0" - - -# Useful for very coarse version differentiation. -PY2 = sys.version_info[0] == 2 -PY3 = sys.version_info[0] == 3 -PY34 = sys.version_info[0:2] >= (3, 4) - -if PY3: - string_types = str, - integer_types = int, - class_types = type, - text_type = str - binary_type = bytes - - MAXSIZE = sys.maxsize -else: - string_types = basestring, - integer_types = (int, long) - class_types = (type, types.ClassType) - text_type = unicode - binary_type = str - - if sys.platform.startswith("java"): - # Jython always uses 32 bits. - MAXSIZE = int((1 << 31) - 1) - else: - # It's possible to have sizeof(long) != sizeof(Py_ssize_t). - class X(object): - - def __len__(self): - return 1 << 31 - try: - len(X()) - except OverflowError: - # 32-bit - MAXSIZE = int((1 << 31) - 1) - else: - # 64-bit - MAXSIZE = int((1 << 63) - 1) - del X - - -def _add_doc(func, doc): - """Add documentation to a function.""" - func.__doc__ = doc - - -def _import_module(name): - """Import module, returning the module after the last dot.""" - __import__(name) - return sys.modules[name] - - -class _LazyDescr(object): - - def __init__(self, name): - self.name = name - - def __get__(self, obj, tp): - result = self._resolve() - setattr(obj, self.name, result) # Invokes __set__. - try: - # This is a bit ugly, but it avoids running this again by - # removing this descriptor. - delattr(obj.__class__, self.name) - except AttributeError: - pass - return result - - -class MovedModule(_LazyDescr): - - def __init__(self, name, old, new=None): - super(MovedModule, self).__init__(name) - if PY3: - if new is None: - new = name - self.mod = new - else: - self.mod = old - - def _resolve(self): - return _import_module(self.mod) - - def __getattr__(self, attr): - _module = self._resolve() - value = getattr(_module, attr) - setattr(self, attr, value) - return value - - -class _LazyModule(types.ModuleType): - - def __init__(self, name): - super(_LazyModule, self).__init__(name) - self.__doc__ = self.__class__.__doc__ - - def __dir__(self): - attrs = ["__doc__", "__name__"] - attrs += [attr.name for attr in self._moved_attributes] - return attrs - - # Subclasses should override this - _moved_attributes = [] - - -class MovedAttribute(_LazyDescr): - - def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): - super(MovedAttribute, self).__init__(name) - if PY3: - if new_mod is None: - new_mod = name - self.mod = new_mod - if new_attr is None: - if old_attr is None: - new_attr = name - else: - new_attr = old_attr - self.attr = new_attr - else: - self.mod = old_mod - if old_attr is None: - old_attr = name - self.attr = old_attr - - def _resolve(self): - module = _import_module(self.mod) - return getattr(module, self.attr) - - -class _SixMetaPathImporter(object): - - """ - A meta path importer to import six.moves and its submodules. - - This class implements a PEP302 finder and loader. It should be compatible - with Python 2.5 and all existing versions of Python3 - """ - - def __init__(self, six_module_name): - self.name = six_module_name - self.known_modules = {} - - def _add_module(self, mod, *fullnames): - for fullname in fullnames: - self.known_modules[self.name + "." + fullname] = mod - - def _get_module(self, fullname): - return self.known_modules[self.name + "." + fullname] - - def find_module(self, fullname, path=None): - if fullname in self.known_modules: - return self - return None - - def __get_module(self, fullname): - try: - return self.known_modules[fullname] - except KeyError: - raise ImportError("This loader does not know module " + fullname) - - def load_module(self, fullname): - try: - # in case of a reload - return sys.modules[fullname] - except KeyError: - pass - mod = self.__get_module(fullname) - if isinstance(mod, MovedModule): - mod = mod._resolve() - else: - mod.__loader__ = self - sys.modules[fullname] = mod - return mod - - def is_package(self, fullname): - """ - Return true, if the named module is a package. - - We need this method to get correct spec objects with - Python 3.4 (see PEP451) - """ - return hasattr(self.__get_module(fullname), "__path__") - - def get_code(self, fullname): - """Return None - - Required, if is_package is implemented""" - self.__get_module(fullname) # eventually raises ImportError - return None - get_source = get_code # same as get_code - -_importer = _SixMetaPathImporter(__name__) - - -class _MovedItems(_LazyModule): - - """Lazy loading of moved objects""" - __path__ = [] # mark as package - - -_moved_attributes = [ - MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), - MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), - MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"), - MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), - MovedAttribute("intern", "__builtin__", "sys"), - MovedAttribute("map", "itertools", "builtins", "imap", "map"), - MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"), - MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"), - MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), - MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"), - MovedAttribute("reduce", "__builtin__", "functools"), - MovedAttribute("shlex_quote", "pipes", "shlex", "quote"), - MovedAttribute("StringIO", "StringIO", "io"), - MovedAttribute("UserDict", "UserDict", "collections"), - MovedAttribute("UserList", "UserList", "collections"), - MovedAttribute("UserString", "UserString", "collections"), - MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), - MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), - MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"), - MovedModule("builtins", "__builtin__"), - MovedModule("configparser", "ConfigParser"), - MovedModule("copyreg", "copy_reg"), - MovedModule("dbm_gnu", "gdbm", "dbm.gnu"), - MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"), - MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), - MovedModule("http_cookies", "Cookie", "http.cookies"), - MovedModule("html_entities", "htmlentitydefs", "html.entities"), - MovedModule("html_parser", "HTMLParser", "html.parser"), - MovedModule("http_client", "httplib", "http.client"), - MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), - MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"), - MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), - MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), - MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), - MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), - MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), - MovedModule("cPickle", "cPickle", "pickle"), - MovedModule("queue", "Queue"), - MovedModule("reprlib", "repr"), - MovedModule("socketserver", "SocketServer"), - MovedModule("_thread", "thread", "_thread"), - MovedModule("tkinter", "Tkinter"), - MovedModule("tkinter_dialog", "Dialog", "tkinter.dialog"), - MovedModule("tkinter_filedialog", "FileDialog", "tkinter.filedialog"), - MovedModule("tkinter_scrolledtext", "ScrolledText", "tkinter.scrolledtext"), - MovedModule("tkinter_simpledialog", "SimpleDialog", "tkinter.simpledialog"), - MovedModule("tkinter_tix", "Tix", "tkinter.tix"), - MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"), - MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), - MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), - MovedModule("tkinter_colorchooser", "tkColorChooser", - "tkinter.colorchooser"), - MovedModule("tkinter_commondialog", "tkCommonDialog", - "tkinter.commondialog"), - MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), - MovedModule("tkinter_font", "tkFont", "tkinter.font"), - MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), - MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", - "tkinter.simpledialog"), - MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"), - MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"), - MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"), - MovedModule("urllib_robotparser", "robotparser", "urllib.robotparser"), - MovedModule("xmlrpc_client", "xmlrpclib", "xmlrpc.client"), - MovedModule("xmlrpc_server", "SimpleXMLRPCServer", "xmlrpc.server"), -] -# Add windows specific modules. -if sys.platform == "win32": - _moved_attributes += [ - MovedModule("winreg", "_winreg"), - ] - -for attr in _moved_attributes: - setattr(_MovedItems, attr.name, attr) - if isinstance(attr, MovedModule): - _importer._add_module(attr, "moves." + attr.name) -del attr - -_MovedItems._moved_attributes = _moved_attributes - -moves = _MovedItems(__name__ + ".moves") -_importer._add_module(moves, "moves") - - -class Module_six_moves_urllib_parse(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_parse""" - - -_urllib_parse_moved_attributes = [ - MovedAttribute("ParseResult", "urlparse", "urllib.parse"), - MovedAttribute("SplitResult", "urlparse", "urllib.parse"), - MovedAttribute("parse_qs", "urlparse", "urllib.parse"), - MovedAttribute("parse_qsl", "urlparse", "urllib.parse"), - MovedAttribute("urldefrag", "urlparse", "urllib.parse"), - MovedAttribute("urljoin", "urlparse", "urllib.parse"), - MovedAttribute("urlparse", "urlparse", "urllib.parse"), - MovedAttribute("urlsplit", "urlparse", "urllib.parse"), - MovedAttribute("urlunparse", "urlparse", "urllib.parse"), - MovedAttribute("urlunsplit", "urlparse", "urllib.parse"), - MovedAttribute("quote", "urllib", "urllib.parse"), - MovedAttribute("quote_plus", "urllib", "urllib.parse"), - MovedAttribute("unquote", "urllib", "urllib.parse"), - MovedAttribute("unquote_plus", "urllib", "urllib.parse"), - MovedAttribute("urlencode", "urllib", "urllib.parse"), - MovedAttribute("splitquery", "urllib", "urllib.parse"), - MovedAttribute("splittag", "urllib", "urllib.parse"), - MovedAttribute("splituser", "urllib", "urllib.parse"), - MovedAttribute("uses_fragment", "urlparse", "urllib.parse"), - MovedAttribute("uses_netloc", "urlparse", "urllib.parse"), - MovedAttribute("uses_params", "urlparse", "urllib.parse"), - MovedAttribute("uses_query", "urlparse", "urllib.parse"), - MovedAttribute("uses_relative", "urlparse", "urllib.parse"), -] -for attr in _urllib_parse_moved_attributes: - setattr(Module_six_moves_urllib_parse, attr.name, attr) -del attr - -Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes - -_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"), - "moves.urllib_parse", "moves.urllib.parse") - - -class Module_six_moves_urllib_error(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_error""" - - -_urllib_error_moved_attributes = [ - MovedAttribute("URLError", "urllib2", "urllib.error"), - MovedAttribute("HTTPError", "urllib2", "urllib.error"), - MovedAttribute("ContentTooShortError", "urllib", "urllib.error"), -] -for attr in _urllib_error_moved_attributes: - setattr(Module_six_moves_urllib_error, attr.name, attr) -del attr - -Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes - -_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"), - "moves.urllib_error", "moves.urllib.error") - - -class Module_six_moves_urllib_request(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_request""" - - -_urllib_request_moved_attributes = [ - MovedAttribute("urlopen", "urllib2", "urllib.request"), - MovedAttribute("install_opener", "urllib2", "urllib.request"), - MovedAttribute("build_opener", "urllib2", "urllib.request"), - MovedAttribute("pathname2url", "urllib", "urllib.request"), - MovedAttribute("url2pathname", "urllib", "urllib.request"), - MovedAttribute("getproxies", "urllib", "urllib.request"), - MovedAttribute("Request", "urllib2", "urllib.request"), - MovedAttribute("OpenerDirector", "urllib2", "urllib.request"), - MovedAttribute("HTTPDefaultErrorHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPRedirectHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPCookieProcessor", "urllib2", "urllib.request"), - MovedAttribute("ProxyHandler", "urllib2", "urllib.request"), - MovedAttribute("BaseHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPPasswordMgr", "urllib2", "urllib.request"), - MovedAttribute("HTTPPasswordMgrWithDefaultRealm", "urllib2", "urllib.request"), - MovedAttribute("AbstractBasicAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPBasicAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("ProxyBasicAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("AbstractDigestAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPDigestAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("ProxyDigestAuthHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPSHandler", "urllib2", "urllib.request"), - MovedAttribute("FileHandler", "urllib2", "urllib.request"), - MovedAttribute("FTPHandler", "urllib2", "urllib.request"), - MovedAttribute("CacheFTPHandler", "urllib2", "urllib.request"), - MovedAttribute("UnknownHandler", "urllib2", "urllib.request"), - MovedAttribute("HTTPErrorProcessor", "urllib2", "urllib.request"), - MovedAttribute("urlretrieve", "urllib", "urllib.request"), - MovedAttribute("urlcleanup", "urllib", "urllib.request"), - MovedAttribute("URLopener", "urllib", "urllib.request"), - MovedAttribute("FancyURLopener", "urllib", "urllib.request"), - MovedAttribute("proxy_bypass", "urllib", "urllib.request"), -] -for attr in _urllib_request_moved_attributes: - setattr(Module_six_moves_urllib_request, attr.name, attr) -del attr - -Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes - -_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"), - "moves.urllib_request", "moves.urllib.request") - - -class Module_six_moves_urllib_response(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_response""" - - -_urllib_response_moved_attributes = [ - MovedAttribute("addbase", "urllib", "urllib.response"), - MovedAttribute("addclosehook", "urllib", "urllib.response"), - MovedAttribute("addinfo", "urllib", "urllib.response"), - MovedAttribute("addinfourl", "urllib", "urllib.response"), -] -for attr in _urllib_response_moved_attributes: - setattr(Module_six_moves_urllib_response, attr.name, attr) -del attr - -Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes - -_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"), - "moves.urllib_response", "moves.urllib.response") - - -class Module_six_moves_urllib_robotparser(_LazyModule): - - """Lazy loading of moved objects in six.moves.urllib_robotparser""" - - -_urllib_robotparser_moved_attributes = [ - MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"), -] -for attr in _urllib_robotparser_moved_attributes: - setattr(Module_six_moves_urllib_robotparser, attr.name, attr) -del attr - -Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes - -_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"), - "moves.urllib_robotparser", "moves.urllib.robotparser") - - -class Module_six_moves_urllib(types.ModuleType): - - """Create a six.moves.urllib namespace that resembles the Python 3 namespace""" - __path__ = [] # mark as package - parse = _importer._get_module("moves.urllib_parse") - error = _importer._get_module("moves.urllib_error") - request = _importer._get_module("moves.urllib_request") - response = _importer._get_module("moves.urllib_response") - robotparser = _importer._get_module("moves.urllib_robotparser") - - def __dir__(self): - return ['parse', 'error', 'request', 'response', 'robotparser'] - -_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"), - "moves.urllib") - - -def add_move(move): - """Add an item to six.moves.""" - setattr(_MovedItems, move.name, move) - - -def remove_move(name): - """Remove item from six.moves.""" - try: - delattr(_MovedItems, name) - except AttributeError: - try: - del moves.__dict__[name] - except KeyError: - raise AttributeError("no such move, %r" % (name,)) - - -if PY3: - _meth_func = "__func__" - _meth_self = "__self__" - - _func_closure = "__closure__" - _func_code = "__code__" - _func_defaults = "__defaults__" - _func_globals = "__globals__" -else: - _meth_func = "im_func" - _meth_self = "im_self" - - _func_closure = "func_closure" - _func_code = "func_code" - _func_defaults = "func_defaults" - _func_globals = "func_globals" - - -try: - advance_iterator = next -except NameError: - def advance_iterator(it): - return it.next() -next = advance_iterator - - -try: - callable = callable -except NameError: - def callable(obj): - return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) - - -if PY3: - def get_unbound_function(unbound): - return unbound - - create_bound_method = types.MethodType - - def create_unbound_method(func, cls): - return func - - Iterator = object -else: - def get_unbound_function(unbound): - return unbound.im_func - - def create_bound_method(func, obj): - return types.MethodType(func, obj, obj.__class__) - - def create_unbound_method(func, cls): - return types.MethodType(func, None, cls) - - class Iterator(object): - - def next(self): - return type(self).__next__(self) - - callable = callable -_add_doc(get_unbound_function, - """Get the function out of a possibly unbound function""") - - -get_method_function = operator.attrgetter(_meth_func) -get_method_self = operator.attrgetter(_meth_self) -get_function_closure = operator.attrgetter(_func_closure) -get_function_code = operator.attrgetter(_func_code) -get_function_defaults = operator.attrgetter(_func_defaults) -get_function_globals = operator.attrgetter(_func_globals) - - -if PY3: - def iterkeys(d, **kw): - return iter(d.keys(**kw)) - - def itervalues(d, **kw): - return iter(d.values(**kw)) - - def iteritems(d, **kw): - return iter(d.items(**kw)) - - def iterlists(d, **kw): - return iter(d.lists(**kw)) - - viewkeys = operator.methodcaller("keys") - - viewvalues = operator.methodcaller("values") - - viewitems = operator.methodcaller("items") -else: - def iterkeys(d, **kw): - return d.iterkeys(**kw) - - def itervalues(d, **kw): - return d.itervalues(**kw) - - def iteritems(d, **kw): - return d.iteritems(**kw) - - def iterlists(d, **kw): - return d.iterlists(**kw) - - viewkeys = operator.methodcaller("viewkeys") - - viewvalues = operator.methodcaller("viewvalues") - - viewitems = operator.methodcaller("viewitems") - -_add_doc(iterkeys, "Return an iterator over the keys of a dictionary.") -_add_doc(itervalues, "Return an iterator over the values of a dictionary.") -_add_doc(iteritems, - "Return an iterator over the (key, value) pairs of a dictionary.") -_add_doc(iterlists, - "Return an iterator over the (key, [values]) pairs of a dictionary.") - - -if PY3: - def b(s): - return s.encode("latin-1") - - def u(s): - return s - unichr = chr - import struct - int2byte = struct.Struct(">B").pack - del struct - byte2int = operator.itemgetter(0) - indexbytes = operator.getitem - iterbytes = iter - import io - StringIO = io.StringIO - BytesIO = io.BytesIO - _assertCountEqual = "assertCountEqual" - if sys.version_info[1] <= 1: - _assertRaisesRegex = "assertRaisesRegexp" - _assertRegex = "assertRegexpMatches" - else: - _assertRaisesRegex = "assertRaisesRegex" - _assertRegex = "assertRegex" -else: - def b(s): - return s - # Workaround for standalone backslash - - def u(s): - return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape") - unichr = unichr - int2byte = chr - - def byte2int(bs): - return ord(bs[0]) - - def indexbytes(buf, i): - return ord(buf[i]) - iterbytes = functools.partial(itertools.imap, ord) - import StringIO - StringIO = BytesIO = StringIO.StringIO - _assertCountEqual = "assertItemsEqual" - _assertRaisesRegex = "assertRaisesRegexp" - _assertRegex = "assertRegexpMatches" -_add_doc(b, """Byte literal""") -_add_doc(u, """Text literal""") - - -def assertCountEqual(self, *args, **kwargs): - return getattr(self, _assertCountEqual)(*args, **kwargs) - - -def assertRaisesRegex(self, *args, **kwargs): - return getattr(self, _assertRaisesRegex)(*args, **kwargs) - - -def assertRegex(self, *args, **kwargs): - return getattr(self, _assertRegex)(*args, **kwargs) - - -if PY3: - exec_ = getattr(moves.builtins, "exec") - - def reraise(tp, value, tb=None): - if value is None: - value = tp() - if value.__traceback__ is not tb: - raise value.with_traceback(tb) - raise value - -else: - def exec_(_code_, _globs_=None, _locs_=None): - """Execute code in a namespace.""" - if _globs_ is None: - frame = sys._getframe(1) - _globs_ = frame.f_globals - if _locs_ is None: - _locs_ = frame.f_locals - del frame - elif _locs_ is None: - _locs_ = _globs_ - exec("""exec _code_ in _globs_, _locs_""") - - exec_("""def reraise(tp, value, tb=None): - raise tp, value, tb -""") - - -if sys.version_info[:2] == (3, 2): - exec_("""def raise_from(value, from_value): - if from_value is None: - raise value - raise value from from_value -""") -elif sys.version_info[:2] > (3, 2): - exec_("""def raise_from(value, from_value): - raise value from from_value -""") -else: - def raise_from(value, from_value): - raise value - - -print_ = getattr(moves.builtins, "print", None) -if print_ is None: - def print_(*args, **kwargs): - """The new-style print function for Python 2.4 and 2.5.""" - fp = kwargs.pop("file", sys.stdout) - if fp is None: - return - - def write(data): - if not isinstance(data, basestring): - data = str(data) - # If the file has an encoding, encode unicode with it. - if (isinstance(fp, file) and - isinstance(data, unicode) and - fp.encoding is not None): - errors = getattr(fp, "errors", None) - if errors is None: - errors = "strict" - data = data.encode(fp.encoding, errors) - fp.write(data) - want_unicode = False - sep = kwargs.pop("sep", None) - if sep is not None: - if isinstance(sep, unicode): - want_unicode = True - elif not isinstance(sep, str): - raise TypeError("sep must be None or a string") - end = kwargs.pop("end", None) - if end is not None: - if isinstance(end, unicode): - want_unicode = True - elif not isinstance(end, str): - raise TypeError("end must be None or a string") - if kwargs: - raise TypeError("invalid keyword arguments to print()") - if not want_unicode: - for arg in args: - if isinstance(arg, unicode): - want_unicode = True - break - if want_unicode: - newline = unicode("\n") - space = unicode(" ") - else: - newline = "\n" - space = " " - if sep is None: - sep = space - if end is None: - end = newline - for i, arg in enumerate(args): - if i: - write(sep) - write(arg) - write(end) -if sys.version_info[:2] < (3, 3): - _print = print_ - - def print_(*args, **kwargs): - fp = kwargs.get("file", sys.stdout) - flush = kwargs.pop("flush", False) - _print(*args, **kwargs) - if flush and fp is not None: - fp.flush() - -_add_doc(reraise, """Reraise an exception.""") - -if sys.version_info[0:2] < (3, 4): - def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, - updated=functools.WRAPPER_UPDATES): - def wrapper(f): - f = functools.wraps(wrapped, assigned, updated)(f) - f.__wrapped__ = wrapped - return f - return wrapper -else: - wraps = functools.wraps - - -def with_metaclass(meta, *bases): - """Create a base class with a metaclass.""" - # This requires a bit of explanation: the basic idea is to make a dummy - # metaclass for one level of class instantiation that replaces itself with - # the actual metaclass. - class metaclass(meta): - - def __new__(cls, name, this_bases, d): - return meta(name, bases, d) - return type.__new__(metaclass, 'temporary_class', (), {}) - - -def add_metaclass(metaclass): - """Class decorator for creating a class with a metaclass.""" - def wrapper(cls): - orig_vars = cls.__dict__.copy() - slots = orig_vars.get('__slots__') - if slots is not None: - if isinstance(slots, str): - slots = [slots] - for slots_var in slots: - orig_vars.pop(slots_var) - orig_vars.pop('__dict__', None) - orig_vars.pop('__weakref__', None) - return metaclass(cls.__name__, cls.__bases__, orig_vars) - return wrapper - - -def python_2_unicode_compatible(klass): - """ - A decorator that defines __unicode__ and __str__ methods under Python 2. - Under Python 3 it does nothing. - - To support Python 2 and 3 with a single code base, define a __str__ method - returning text and apply this decorator to the class. - """ - if PY2: - if '__str__' not in klass.__dict__: - raise ValueError("@python_2_unicode_compatible cannot be applied " - "to %s because it doesn't define __str__()." % - klass.__name__) - klass.__unicode__ = klass.__str__ - klass.__str__ = lambda self: self.__unicode__().encode('utf-8') - return klass - - -# Complete the moves implementation. -# This code is at the end of this module to speed up module loading. -# Turn this module into a package. -__path__ = [] # required for PEP 302 and PEP 451 -__package__ = __name__ # see PEP 366 @ReservedAssignment -if globals().get("__spec__") is not None: - __spec__.submodule_search_locations = [] # PEP 451 @UndefinedVariable -# Remove other six meta path importers, since they cause problems. This can -# happen if six is removed from sys.modules and then reloaded. (Setuptools does -# this for some reason.) -if sys.meta_path: - for i, importer in enumerate(sys.meta_path): - # Here's some real nastiness: Another "instance" of the six module might - # be floating around. Therefore, we can't use isinstance() to check for - # the six meta path importer, since the other six instance will have - # inserted an importer with different class. - if (type(importer).__name__ == "_SixMetaPathImporter" and - importer.name == __name__): - del sys.meta_path[i] - break - del i, importer -# Finally, add the importer to the meta path import hook. -sys.meta_path.append(_importer) diff --git a/libs/common/setuptools/archive_util.py b/libs/common/setuptools/archive_util.py index 81436044..6b8460bd 100644 --- a/libs/common/setuptools/archive_util.py +++ b/libs/common/setuptools/archive_util.py @@ -8,11 +8,16 @@ import posixpath import contextlib from distutils.errors import DistutilsError -from pkg_resources import ensure_directory +from ._path import ensure_directory __all__ = [ - "unpack_archive", "unpack_zipfile", "unpack_tarfile", "default_filter", - "UnrecognizedFormat", "extraction_drivers", "unpack_directory", + "unpack_archive", + "unpack_zipfile", + "unpack_tarfile", + "default_filter", + "UnrecognizedFormat", + "extraction_drivers", + "unpack_directory", ] @@ -25,8 +30,7 @@ def default_filter(src, dst): return dst -def unpack_archive(filename, extract_dir, progress_filter=default_filter, - drivers=None): +def unpack_archive(filename, extract_dir, progress_filter=default_filter, drivers=None): """Unpack `filename` to `extract_dir`, or raise ``UnrecognizedFormat`` `progress_filter` is a function taking two arguments: a source path @@ -55,13 +59,11 @@ def unpack_archive(filename, extract_dir, progress_filter=default_filter, else: return else: - raise UnrecognizedFormat( - "Not a recognized archive type: %s" % filename - ) + raise UnrecognizedFormat("Not a recognized archive type: %s" % filename) def unpack_directory(filename, extract_dir, progress_filter=default_filter): - """"Unpack" a directory, using the same interface as for archives + """ "Unpack" a directory, using the same interface as for archives Raises ``UnrecognizedFormat`` if `filename` is not a directory """ @@ -99,29 +101,87 @@ def unpack_zipfile(filename, extract_dir, progress_filter=default_filter): raise UnrecognizedFormat("%s is not a zip file" % (filename,)) with zipfile.ZipFile(filename) as z: - for info in z.infolist(): - name = info.filename + _unpack_zipfile_obj(z, extract_dir, progress_filter) + +def _unpack_zipfile_obj(zipfile_obj, extract_dir, progress_filter=default_filter): + """Internal/private API used by other parts of setuptools. + Similar to ``unpack_zipfile``, but receives an already opened :obj:`zipfile.ZipFile` + object instead of a filename. + """ + for info in zipfile_obj.infolist(): + name = info.filename + + # don't extract absolute paths or ones with .. in them + if name.startswith('/') or '..' in name.split('/'): + continue + + target = os.path.join(extract_dir, *name.split('/')) + target = progress_filter(name, target) + if not target: + continue + if name.endswith('/'): + # directory + ensure_directory(target) + else: + # file + ensure_directory(target) + data = zipfile_obj.read(info.filename) + with open(target, 'wb') as f: + f.write(data) + unix_attributes = info.external_attr >> 16 + if unix_attributes: + os.chmod(target, unix_attributes) + + +def _resolve_tar_file_or_dir(tar_obj, tar_member_obj): + """Resolve any links and extract link targets as normal files.""" + while tar_member_obj is not None and ( + tar_member_obj.islnk() or tar_member_obj.issym() + ): + linkpath = tar_member_obj.linkname + if tar_member_obj.issym(): + base = posixpath.dirname(tar_member_obj.name) + linkpath = posixpath.join(base, linkpath) + linkpath = posixpath.normpath(linkpath) + tar_member_obj = tar_obj._getmember(linkpath) + + is_file_or_dir = tar_member_obj is not None and ( + tar_member_obj.isfile() or tar_member_obj.isdir() + ) + if is_file_or_dir: + return tar_member_obj + + raise LookupError('Got unknown file type') + + +def _iter_open_tar(tar_obj, extract_dir, progress_filter): + """Emit member-destination pairs from a tar archive.""" + # don't do any chowning! + tar_obj.chown = lambda *args: None + + with contextlib.closing(tar_obj): + for member in tar_obj: + name = member.name # don't extract absolute paths or ones with .. in them if name.startswith('/') or '..' in name.split('/'): continue - target = os.path.join(extract_dir, *name.split('/')) - target = progress_filter(name, target) - if not target: + prelim_dst = os.path.join(extract_dir, *name.split('/')) + + try: + member = _resolve_tar_file_or_dir(tar_obj, member) + except LookupError: continue - if name.endswith('/'): - # directory - ensure_directory(target) - else: - # file - ensure_directory(target) - data = z.read(info.filename) - with open(target, 'wb') as f: - f.write(data) - unix_attributes = info.external_attr >> 16 - if unix_attributes: - os.chmod(target, unix_attributes) + + final_dst = progress_filter(name, prelim_dst) + if not final_dst: + continue + + if final_dst.endswith(os.sep): + final_dst = final_dst[:-1] + + yield member, final_dst def unpack_tarfile(filename, extract_dir, progress_filter=default_filter): @@ -133,41 +193,24 @@ def unpack_tarfile(filename, extract_dir, progress_filter=default_filter): """ try: tarobj = tarfile.open(filename) - except tarfile.TarError: + except tarfile.TarError as e: raise UnrecognizedFormat( "%s is not a compressed or uncompressed tar file" % (filename,) - ) - with contextlib.closing(tarobj): - # don't do any chowning! - tarobj.chown = lambda *args: None - for member in tarobj: - name = member.name - # don't extract absolute paths or ones with .. in them - if not name.startswith('/') and '..' not in name.split('/'): - prelim_dst = os.path.join(extract_dir, *name.split('/')) + ) from e - # resolve any links and to extract the link targets as normal - # files - while member is not None and (member.islnk() or member.issym()): - linkpath = member.linkname - if member.issym(): - base = posixpath.dirname(member.name) - linkpath = posixpath.join(base, linkpath) - linkpath = posixpath.normpath(linkpath) - member = tarobj._getmember(linkpath) + for member, final_dst in _iter_open_tar( + tarobj, + extract_dir, + progress_filter, + ): + try: + # XXX Ugh + tarobj._extract_member(member, final_dst) + except tarfile.ExtractError: + # chown/chmod/mkfifo/mknode/makedev failed + pass - if member is not None and (member.isfile() or member.isdir()): - final_dst = progress_filter(name, prelim_dst) - if final_dst: - if final_dst.endswith(os.sep): - final_dst = final_dst[:-1] - try: - # XXX Ugh - tarobj._extract_member(member, final_dst) - except tarfile.ExtractError: - # chown/chmod/mkfifo/mknode/makedev failed - pass - return True + return True extraction_drivers = unpack_directory, unpack_zipfile, unpack_tarfile diff --git a/libs/common/setuptools/build_meta.py b/libs/common/setuptools/build_meta.py index eb9e815e..2decd2d2 100644 --- a/libs/common/setuptools/build_meta.py +++ b/libs/common/setuptools/build_meta.py @@ -28,25 +28,41 @@ Again, this is not a formal definition! Just a "taste" of the module. import io import os +import shlex import sys import tokenize import shutil import contextlib +import tempfile +import warnings +from pathlib import Path +from typing import Dict, Iterator, List, Optional, Union import setuptools import distutils -from setuptools.py31compat import TemporaryDirectory +from . import errors +from ._path import same_path +from ._reqs import parse_strings +from .warnings import SetuptoolsDeprecationWarning +from distutils.util import strtobool -from pkg_resources import parse_requirements -from pkg_resources.py31compat import makedirs -__all__ = ['get_requires_for_build_sdist', - 'get_requires_for_build_wheel', - 'prepare_metadata_for_build_wheel', - 'build_wheel', - 'build_sdist', - '__legacy__', - 'SetupRequirementsError'] +__all__ = [ + 'get_requires_for_build_sdist', + 'get_requires_for_build_wheel', + 'prepare_metadata_for_build_wheel', + 'build_wheel', + 'build_sdist', + 'get_requires_for_build_editable', + 'prepare_metadata_for_build_editable', + 'build_editable', + '__legacy__', + 'SetupRequirementsError', +] + +SETUPTOOLS_ENABLE_FEATURES = os.getenv("SETUPTOOLS_ENABLE_FEATURES", "").lower() +LEGACY_EDITABLE = "legacy-editable" in SETUPTOOLS_ENABLE_FEATURES.replace("_", "-") + class SetupRequirementsError(BaseException): def __init__(self, specifiers): @@ -55,7 +71,7 @@ class SetupRequirementsError(BaseException): class Distribution(setuptools.dist.Distribution): def fetch_build_eggs(self, specifiers): - specifier_list = list(map(str, parse_requirements(specifiers))) + specifier_list = list(parse_strings(specifiers)) raise SetupRequirementsError(specifier_list) @@ -75,53 +91,205 @@ class Distribution(setuptools.dist.Distribution): distutils.core.Distribution = orig -def _to_str(s): +@contextlib.contextmanager +def no_install_setup_requires(): + """Temporarily disable installing setup_requires + + Under PEP 517, the backend reports build dependencies to the frontend, + and the frontend is responsible for ensuring they're installed. + So setuptools (acting as a backend) should not try to install them. """ - Convert a filename to a string (on Python 2, explicitly - a byte string, not Unicode) as distutils checks for the - exact type str. - """ - if sys.version_info[0] == 2 and not isinstance(s, str): - # Assume it's Unicode, as that's what the PEP says - # should be provided. - return s.encode(sys.getfilesystemencoding()) - return s + orig = setuptools._install_setup_requires + setuptools._install_setup_requires = lambda attrs: None + try: + yield + finally: + setuptools._install_setup_requires = orig def _get_immediate_subdirectories(a_dir): - return [name for name in os.listdir(a_dir) - if os.path.isdir(os.path.join(a_dir, name))] + return [ + name for name in os.listdir(a_dir) if os.path.isdir(os.path.join(a_dir, name)) + ] def _file_with_extension(directory, extension): - matching = ( - f for f in os.listdir(directory) - if f.endswith(extension) - ) - file, = matching + matching = (f for f in os.listdir(directory) if f.endswith(extension)) + try: + (file,) = matching + except ValueError: + raise ValueError( + 'No distribution was found. Ensure that `setup.py` ' + 'is not empty and that it calls `setup()`.' + ) from None return file def _open_setup_script(setup_script): if not os.path.exists(setup_script): # Supply a default setup.py - return io.StringIO(u"from setuptools import setup; setup()") + return io.StringIO("from setuptools import setup; setup()") - return getattr(tokenize, 'open', open)(setup_script) + return tokenize.open(setup_script) -class _BuildMetaBackend(object): +@contextlib.contextmanager +def suppress_known_deprecation(): + with warnings.catch_warnings(): + warnings.filterwarnings('ignore', 'setup.py install is deprecated') + yield - def _fix_config(self, config_settings): - config_settings = config_settings or {} - config_settings.setdefault('--global-option', []) - return config_settings +_ConfigSettings = Optional[Dict[str, Union[str, List[str], None]]] +""" +Currently the user can run:: + + pip install -e . --config-settings key=value + python -m build -C--key=value -C key=value + +- pip will pass both key and value as strings and overwriting repeated keys + (pypa/pip#11059). +- build will accumulate values associated with repeated keys in a list. + It will also accept keys with no associated value. + This means that an option passed by build can be ``str | list[str] | None``. +- PEP 517 specifies that ``config_settings`` is an optional dict. +""" + + +class _ConfigSettingsTranslator: + """Translate ``config_settings`` into distutils-style command arguments. + Only a limited number of options is currently supported. + """ + + # See pypa/setuptools#1928 pypa/setuptools#2491 + + def _get_config(self, key: str, config_settings: _ConfigSettings) -> List[str]: + """ + Get the value of a specific key in ``config_settings`` as a list of strings. + + >>> fn = _ConfigSettingsTranslator()._get_config + >>> fn("--global-option", None) + [] + >>> fn("--global-option", {}) + [] + >>> fn("--global-option", {'--global-option': 'foo'}) + ['foo'] + >>> fn("--global-option", {'--global-option': ['foo']}) + ['foo'] + >>> fn("--global-option", {'--global-option': 'foo'}) + ['foo'] + >>> fn("--global-option", {'--global-option': 'foo bar'}) + ['foo', 'bar'] + """ + cfg = config_settings or {} + opts = cfg.get(key) or [] + return shlex.split(opts) if isinstance(opts, str) else opts + + def _global_args(self, config_settings: _ConfigSettings) -> Iterator[str]: + """ + Let the user specify ``verbose`` or ``quiet`` + escape hatch via + ``--global-option``. + Note: ``-v``, ``-vv``, ``-vvv`` have similar effects in setuptools, + so we just have to cover the basic scenario ``-v``. + + >>> fn = _ConfigSettingsTranslator()._global_args + >>> list(fn(None)) + [] + >>> list(fn({"verbose": "False"})) + ['-q'] + >>> list(fn({"verbose": "1"})) + ['-v'] + >>> list(fn({"--verbose": None})) + ['-v'] + >>> list(fn({"verbose": "true", "--global-option": "-q --no-user-cfg"})) + ['-v', '-q', '--no-user-cfg'] + >>> list(fn({"--quiet": None})) + ['-q'] + """ + cfg = config_settings or {} + falsey = {"false", "no", "0", "off"} + if "verbose" in cfg or "--verbose" in cfg: + level = str(cfg.get("verbose") or cfg.get("--verbose") or "1") + yield ("-q" if level.lower() in falsey else "-v") + if "quiet" in cfg or "--quiet" in cfg: + level = str(cfg.get("quiet") or cfg.get("--quiet") or "1") + yield ("-v" if level.lower() in falsey else "-q") + + yield from self._get_config("--global-option", config_settings) + + def __dist_info_args(self, config_settings: _ConfigSettings) -> Iterator[str]: + """ + The ``dist_info`` command accepts ``tag-date`` and ``tag-build``. + + .. warning:: + We cannot use this yet as it requires the ``sdist`` and ``bdist_wheel`` + commands run in ``build_sdist`` and ``build_wheel`` to reuse the egg-info + directory created in ``prepare_metadata_for_build_wheel``. + + >>> fn = _ConfigSettingsTranslator()._ConfigSettingsTranslator__dist_info_args + >>> list(fn(None)) + [] + >>> list(fn({"tag-date": "False"})) + ['--no-date'] + >>> list(fn({"tag-date": None})) + ['--no-date'] + >>> list(fn({"tag-date": "true", "tag-build": ".a"})) + ['--tag-date', '--tag-build', '.a'] + """ + cfg = config_settings or {} + if "tag-date" in cfg: + val = strtobool(str(cfg["tag-date"] or "false")) + yield ("--tag-date" if val else "--no-date") + if "tag-build" in cfg: + yield from ["--tag-build", str(cfg["tag-build"])] + + def _editable_args(self, config_settings: _ConfigSettings) -> Iterator[str]: + """ + The ``editable_wheel`` command accepts ``editable-mode=strict``. + + >>> fn = _ConfigSettingsTranslator()._editable_args + >>> list(fn(None)) + [] + >>> list(fn({"editable-mode": "strict"})) + ['--mode', 'strict'] + """ + cfg = config_settings or {} + mode = cfg.get("editable-mode") or cfg.get("editable_mode") + if not mode: + return + yield from ["--mode", str(mode)] + + def _arbitrary_args(self, config_settings: _ConfigSettings) -> Iterator[str]: + """ + Users may expect to pass arbitrary lists of arguments to a command + via "--global-option" (example provided in PEP 517 of a "escape hatch"). + + >>> fn = _ConfigSettingsTranslator()._arbitrary_args + >>> list(fn(None)) + [] + >>> list(fn({})) + [] + >>> list(fn({'--build-option': 'foo'})) + ['foo'] + >>> list(fn({'--build-option': ['foo']})) + ['foo'] + >>> list(fn({'--build-option': 'foo'})) + ['foo'] + >>> list(fn({'--build-option': 'foo bar'})) + ['foo', 'bar'] + >>> list(fn({'--global-option': 'foo'})) + [] + """ + yield from self._get_config("--build-option", config_settings) + + +class _BuildMetaBackend(_ConfigSettingsTranslator): def _get_build_requires(self, config_settings, requirements): - config_settings = self._fix_config(config_settings) - - sys.argv = sys.argv[:1] + ['egg_info'] + \ - config_settings["--global-option"] + sys.argv = [ + *sys.argv[:1], + *self._global_args(config_settings), + "egg_info", + ] try: with Distribution.patch(): self.run_setup() @@ -133,65 +301,98 @@ class _BuildMetaBackend(object): def run_setup(self, setup_script='setup.py'): # Note that we can reuse our build directory between calls # Correctness comes first, then optimization later - __file__ = setup_script + __file__ = os.path.abspath(setup_script) __name__ = '__main__' with _open_setup_script(__file__) as f: code = f.read().replace(r'\r\n', r'\n') - exec(compile(code, __file__, 'exec'), locals()) + try: + exec(code, locals()) + except SystemExit as e: + if e.code: + raise + # We ignore exit code indicating success + SetuptoolsDeprecationWarning.emit( + "Running `setup.py` directly as CLI tool is deprecated.", + "Please avoid using `sys.exit(0)` or similar statements " + "that don't fit in the paradigm of a configuration file.", + see_url="https://blog.ganssle.io/articles/2021/10/" + "setup-py-deprecated.html", + ) def get_requires_for_build_wheel(self, config_settings=None): - config_settings = self._fix_config(config_settings) return self._get_build_requires(config_settings, requirements=['wheel']) def get_requires_for_build_sdist(self, config_settings=None): - config_settings = self._fix_config(config_settings) return self._get_build_requires(config_settings, requirements=[]) - def prepare_metadata_for_build_wheel(self, metadata_directory, - config_settings=None): - sys.argv = sys.argv[:1] + ['dist_info', '--egg-base', - _to_str(metadata_directory)] - self.run_setup() + def _bubble_up_info_directory(self, metadata_directory: str, suffix: str) -> str: + """ + PEP 517 requires that the .dist-info directory be placed in the + metadata_directory. To comply, we MUST copy the directory to the root. - dist_info_directory = metadata_directory - while True: - dist_infos = [f for f in os.listdir(dist_info_directory) - if f.endswith('.dist-info')] + Returns the basename of the info directory, e.g. `proj-0.0.0.dist-info`. + """ + info_dir = self._find_info_directory(metadata_directory, suffix) + if not same_path(info_dir.parent, metadata_directory): + shutil.move(str(info_dir), metadata_directory) + # PEP 517 allow other files and dirs to exist in metadata_directory + return info_dir.name - if (len(dist_infos) == 0 and - len(_get_immediate_subdirectories(dist_info_directory)) == 1): + def _find_info_directory(self, metadata_directory: str, suffix: str) -> Path: + for parent, dirs, _ in os.walk(metadata_directory): + candidates = [f for f in dirs if f.endswith(suffix)] - dist_info_directory = os.path.join( - dist_info_directory, os.listdir(dist_info_directory)[0]) - continue + if len(candidates) != 0 or len(dirs) != 1: + assert len(candidates) == 1, f"Multiple {suffix} directories found" + return Path(parent, candidates[0]) - assert len(dist_infos) == 1 - break + msg = f"No {suffix} directory found in {metadata_directory}" + raise errors.InternalError(msg) - # PEP 517 requires that the .dist-info directory be placed in the - # metadata_directory. To comply, we MUST copy the directory to the root - if dist_info_directory != metadata_directory: - shutil.move( - os.path.join(dist_info_directory, dist_infos[0]), - metadata_directory) - shutil.rmtree(dist_info_directory, ignore_errors=True) + def prepare_metadata_for_build_wheel( + self, metadata_directory, config_settings=None + ): + sys.argv = [ + *sys.argv[:1], + *self._global_args(config_settings), + "dist_info", + "--output-dir", + metadata_directory, + "--keep-egg-info", + ] + with no_install_setup_requires(): + self.run_setup() - return dist_infos[0] + self._bubble_up_info_directory(metadata_directory, ".egg-info") + return self._bubble_up_info_directory(metadata_directory, ".dist-info") - def _build_with_temp_dir(self, setup_command, result_extension, - result_directory, config_settings): - config_settings = self._fix_config(config_settings) + def _build_with_temp_dir( + self, + setup_command, + result_extension, + result_directory, + config_settings, + arbitrary_args=(), + ): result_directory = os.path.abspath(result_directory) # Build in a temporary directory, then copy to the target. - makedirs(result_directory, exist_ok=True) - with TemporaryDirectory(dir=result_directory) as tmp_dist_dir: - sys.argv = (sys.argv[:1] + setup_command + - ['--dist-dir', tmp_dist_dir] + - config_settings["--global-option"]) - self.run_setup() + os.makedirs(result_directory, exist_ok=True) + temp_opts = {"prefix": ".tmp-", "dir": result_directory} + + with tempfile.TemporaryDirectory(**temp_opts) as tmp_dist_dir: + sys.argv = [ + *sys.argv[:1], + *self._global_args(config_settings), + *setup_command, + "--dist-dir", + tmp_dist_dir, + *arbitrary_args, + ] + with no_install_setup_requires(): + self.run_setup() result_basename = _file_with_extension(tmp_dist_dir, result_extension) result_path = os.path.join(result_directory, result_basename) @@ -202,31 +403,75 @@ class _BuildMetaBackend(object): return result_basename - - def build_wheel(self, wheel_directory, config_settings=None, - metadata_directory=None): - return self._build_with_temp_dir(['bdist_wheel'], '.whl', - wheel_directory, config_settings) + def build_wheel( + self, wheel_directory, config_settings=None, metadata_directory=None + ): + with suppress_known_deprecation(): + return self._build_with_temp_dir( + ['bdist_wheel'], + '.whl', + wheel_directory, + config_settings, + self._arbitrary_args(config_settings), + ) def build_sdist(self, sdist_directory, config_settings=None): - return self._build_with_temp_dir(['sdist', '--formats', 'gztar'], - '.tar.gz', sdist_directory, - config_settings) + return self._build_with_temp_dir( + ['sdist', '--formats', 'gztar'], '.tar.gz', sdist_directory, config_settings + ) + + def _get_dist_info_dir(self, metadata_directory: Optional[str]) -> Optional[str]: + if not metadata_directory: + return None + dist_info_candidates = list(Path(metadata_directory).glob("*.dist-info")) + assert len(dist_info_candidates) <= 1 + return str(dist_info_candidates[0]) if dist_info_candidates else None + + if not LEGACY_EDITABLE: + # PEP660 hooks: + # build_editable + # get_requires_for_build_editable + # prepare_metadata_for_build_editable + def build_editable( + self, wheel_directory, config_settings=None, metadata_directory=None + ): + # XXX can or should we hide our editable_wheel command normally? + info_dir = self._get_dist_info_dir(metadata_directory) + opts = ["--dist-info-dir", info_dir] if info_dir else [] + cmd = ["editable_wheel", *opts, *self._editable_args(config_settings)] + with suppress_known_deprecation(): + return self._build_with_temp_dir( + cmd, ".whl", wheel_directory, config_settings + ) + + def get_requires_for_build_editable(self, config_settings=None): + return self.get_requires_for_build_wheel(config_settings) + + def prepare_metadata_for_build_editable( + self, metadata_directory, config_settings=None + ): + return self.prepare_metadata_for_build_wheel( + metadata_directory, config_settings + ) class _BuildMetaLegacyBackend(_BuildMetaBackend): """Compatibility backend for setuptools - This is a version of setuptools.build_meta that endeavors to maintain backwards - compatibility with pre-PEP 517 modes of invocation. It exists as a temporary - bridge between the old packaging mechanism and the new packaging mechanism, + This is a version of setuptools.build_meta that endeavors + to maintain backwards + compatibility with pre-PEP 517 modes of invocation. It + exists as a temporary + bridge between the old packaging mechanism and the new + packaging mechanism, and will eventually be removed. """ + def run_setup(self, setup_script='setup.py'): # In order to maintain compatibility with scripts assuming that # the setup.py script is in a directory on the PYTHONPATH, inject # '' into sys.path. (pypa/setuptools#1642) - sys_path = list(sys.path) # Save the original path + sys_path = list(sys.path) # Save the original path script_dir = os.path.dirname(os.path.abspath(setup_script)) if script_dir not in sys.path: @@ -239,8 +484,7 @@ class _BuildMetaLegacyBackend(_BuildMetaBackend): sys.argv[0] = setup_script try: - super(_BuildMetaLegacyBackend, - self).run_setup(setup_script=setup_script) + super().run_setup(setup_script=setup_script) finally: # While PEP 517 frontends should be calling each hook in a fresh # subprocess according to the standard (and thus it should not be @@ -250,6 +494,7 @@ class _BuildMetaLegacyBackend(_BuildMetaBackend): sys.path[:] = sys_path sys.argv[0] = sys_argv_0 + # The primary backend _BACKEND = _BuildMetaBackend() @@ -259,6 +504,11 @@ prepare_metadata_for_build_wheel = _BACKEND.prepare_metadata_for_build_wheel build_wheel = _BACKEND.build_wheel build_sdist = _BACKEND.build_sdist +if not LEGACY_EDITABLE: + get_requires_for_build_editable = _BACKEND.get_requires_for_build_editable + prepare_metadata_for_build_editable = _BACKEND.prepare_metadata_for_build_editable + build_editable = _BACKEND.build_editable + # The legacy backend __legacy__ = _BuildMetaLegacyBackend() diff --git a/libs/common/setuptools/cli-32.exe b/libs/common/setuptools/cli-32.exe index b1487b7819e7286577a043c7726fbe0ca1543083..65c3cd99cc7433f271a5b9387abdd1ddb949d1a6 100644 GIT binary patch literal 11776 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 I{a-)-138F7F#rGn literal 65536 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

@|$jDp4t7maBT@by!vG1&J_?=DS4W3Hu6w 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[c}iI#RErv3nvcIClH@!{vM)zJ_weD zu_-L8NU*GlC{d0L!!VW10^+~>qmNB~Y8H+F}!P8_d(PpvjzMJQmr z)FkX;2B~<|3JfJeWv@IXo~nTtp$}Gjie> zs8UDG*kid(%i5QCBp~MA;#I186PI-nZ&k7!k8BiLJSuR>h7ArSYHD~B0I z=T6L{zqglekt0JjG5z&|GWb4?+B5+{p^fgTufl_KesA{@I&g7rNq==^SGc5GcM%$N zDBG2)qExz*Z;jGN_-iD-y8i2BCq)p}2lKcspLg>w-;qwg(()HXrZa3jd!}spuwBVX zwmX!iwU?#7uoQnunw|OlU~+c z^L5Ak3zWhaA4B^FhMMboO0k*O2GL)lD9_<$5b>czbCvKcSt+u*gA*=%dH>Q-Bc11h zzO7jbXN)&5mBf=w2anK6P$YcJZQoWa2#E!v{hFKxxm7Fc)Fc9iC35{|Lp7bIDjrhC zgMiGf4r2yquH{U7WdMio;XS4Y%Ry{q7#kv#gZ07i`7eo#MMh_o68E*Fd_#nrri^4b zX+slbsv>+8pmck%oLDUL()8NRJ#Z z8DReF_eq2zsjEXGs)yS{k}ykS1B!ZrY0f6O65^lslJv3g&wfpDg-&EwF8wrc=hSwm zPlV&n%%yE_@onOwK?)`GNJ6MQ0drMuBYWCH5dkD)uErh@*k}#GcFl<-;;TN+5vb|b zctkCv;*zL7f)A;QuO%(81r0)&aUz4EQu;kA!k@7i8RZ)koMaWW`5cC6n@{w!!J$5d zx}l)4VP4xL=BKi&c^{n_Qi`q@G{vimblcVR53b#*X$FUOQFm!A8JKahNSiBdY+x3bJZfD8n{--FLUM4+Mx@{vM_ep zkk)U=K8R(rhU(X_faI*ZO}cn`5t*O}lx^j8|0rt-)o=Axn^DGcQTi!#7hxLTq?|HQ zB;T6(nrsCeYK0_o%)IO+CP{n#+|;w1ZmvD2c-J{i88bp63RjyKOE!B!D3U{RCs*Zh z&^%65VM(J34230U4bHS}M@SYS9TEK}c%)2<$h1|T;##zRtjRt@#1T%J=kAhOiw+Z% z7DpyWVK@6%9K^uVD9LDKj)dR^aZK6$@Lt)l;sj@`QSzBm{TlLG{JKM_^60Zr2w~nr zr>P-BaV8OjjWm?hQ3$ZCx+lyD%q`~4iNF9xWKi$t&pzBhwN9Dq-o^v9@=abLR#|

KZqkLal4YCRR9VNhIM|rBqmzzcImvcx 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 diff --git a/libs/common/setuptools/cli-64.exe b/libs/common/setuptools/cli-64.exe index 675e6bf3743f3d3011c238657e7128ee9960ef7f..3ea50eebfe3f0113b231a318cc1ad6e238afd60d 100644 GIT binary patch literal 14336 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 AO8@`> literal 74752 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 diff --git a/libs/common/setuptools/cli.exe b/libs/common/setuptools/cli.exe index b1487b7819e7286577a043c7726fbe0ca1543083..65c3cd99cc7433f271a5b9387abdd1ddb949d1a6 100644 GIT binary patch literal 11776 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 I{a-)-138F7F#rGn literal 65536 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

@|$jDp4t7maBT@by!vG1&J_?=DS4W3Hu6w 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[c}iI#RErv3nvcIClH@!{vM)zJ_weD zu_-L8NU*GlC{d0L!!VW10^+~>qmNB~Y8H+F}!P8_d(PpvjzMJQmr z)FkX;2B~<|3JfJeWv@IXo~nTtp$}Gjie> zs8UDG*kid(%i5QCBp~MA;#I186PI-nZ&k7!k8BiLJSuR>h7ArSYHD~B0I z=T6L{zqglekt0JjG5z&|GWb4?+B5+{p^fgTufl_KesA{@I&g7rNq==^SGc5GcM%$N zDBG2)qExz*Z;jGN_-iD-y8i2BCq)p}2lKcspLg>w-;qwg(()HXrZa3jd!}spuwBVX zwmX!iwU?#7uoQnunw|OlU~+c z^L5Ak3zWhaA4B^FhMMboO0k*O2GL)lD9_<$5b>czbCvKcSt+u*gA*=%dH>Q-Bc11h zzO7jbXN)&5mBf=w2anK6P$YcJZQoWa2#E!v{hFKxxm7Fc)Fc9iC35{|Lp7bIDjrhC zgMiGf4r2yquH{U7WdMio;XS4Y%Ry{q7#kv#gZ07i`7eo#MMh_o68E*Fd_#nrri^4b zX+slbsv>+8pmck%oLDUL()8NRJ#Z z8DReF_eq2zsjEXGs)yS{k}ykS1B!ZrY0f6O65^lslJv3g&wfpDg-&EwF8wrc=hSwm zPlV&n%%yE_@onOwK?)`GNJ6MQ0drMuBYWCH5dkD)uErh@*k}#GcFl<-;;TN+5vb|b zctkCv;*zL7f)A;QuO%(81r0)&aUz4EQu;kA!k@7i8RZ)koMaWW`5cC6n@{w!!J$5d zx}l)4VP4xL=BKi&c^{n_Qi`q@G{vimblcVR53b#*X$FUOQFm!A8JKahNSiBdY+x3bJZfD8n{--FLUM4+Mx@{vM_ep zkk)U=K8R(rhU(X_faI*ZO}cn`5t*O}lx^j8|0rt-)o=Axn^DGcQTi!#7hxLTq?|HQ zB;T6(nrsCeYK0_o%)IO+CP{n#+|;w1ZmvD2c-J{i88bp63RjyKOE!B!D3U{RCs*Zh z&^%65VM(J34230U4bHS}M@SYS9TEK}c%)2<$h1|T;##zRtjRt@#1T%J=kAhOiw+Z% z7DpyWVK@6%9K^uVD9LDKj)dR^aZK6$@Lt)l;sj@`QSzBm{TlLG{JKM_^60Zr2w~nr zr>P-BaV8OjjWm?hQ3$ZCx+lyD%q`~4iNF9xWKi$t&pzBhwN9Dq-o^v9@=abLR#|

KZqkLal4YCRR9VNhIM|rBqmzzcImvcx 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 diff --git a/libs/common/setuptools/command/__init__.py b/libs/common/setuptools/command/__init__.py index 743f5588..5acd7687 100644 --- a/libs/common/setuptools/command/__init__.py +++ b/libs/common/setuptools/command/__init__.py @@ -1,17 +1,12 @@ -__all__ = [ - 'alias', 'bdist_egg', 'bdist_rpm', 'build_ext', 'build_py', 'develop', - 'easy_install', 'egg_info', 'install', 'install_lib', 'rotate', 'saveopts', - 'sdist', 'setopt', 'test', 'install_egg_info', 'install_scripts', - 'bdist_wininst', 'upload_docs', 'build_clib', 'dist_info', -] - from distutils.command.bdist import bdist import sys -from setuptools.command import install_scripts - if 'egg' not in bdist.format_commands: - bdist.format_command['egg'] = ('bdist_egg', "Python .egg file") - bdist.format_commands.append('egg') + try: + bdist.format_commands['egg'] = ('bdist_egg', "Python .egg file") + except TypeError: + # For backward compatibility with older distutils (stdlib) + bdist.format_command['egg'] = ('bdist_egg', "Python .egg file") + bdist.format_commands.append('egg') del bdist, sys diff --git a/libs/common/setuptools/command/alias.py b/libs/common/setuptools/command/alias.py index 4532b1cc..e7b4d545 100644 --- a/libs/common/setuptools/command/alias.py +++ b/libs/common/setuptools/command/alias.py @@ -1,7 +1,5 @@ from distutils.errors import DistutilsOptionError -from setuptools.extern.six.moves import map - from setuptools.command.setopt import edit_config, option_base, config_file @@ -51,7 +49,7 @@ class alias(option_base): return elif len(self.args) == 1: - alias, = self.args + (alias,) = self.args if self.remove: command = None elif alias in aliases: diff --git a/libs/common/setuptools/command/bdist_egg.py b/libs/common/setuptools/command/bdist_egg.py index 98470f17..3687efdf 100644 --- a/libs/common/setuptools/command/bdist_egg.py +++ b/libs/common/setuptools/command/bdist_egg.py @@ -2,7 +2,6 @@ Build .egg distributions""" -from distutils.errors import DistutilsSetupError from distutils.dir_util import remove_tree, mkpath from distutils import log from types import CodeType @@ -12,24 +11,15 @@ import re import textwrap import marshal -from setuptools.extern import six - -from pkg_resources import get_build_platform, Distribution, ensure_directory -from pkg_resources import EntryPoint from setuptools.extension import Library from setuptools import Command +from .._path import ensure_directory -try: - # Python 2.7 or >=3.2 - from sysconfig import get_path, get_python_version +from sysconfig import get_path, get_python_version - def _get_purelib(): - return get_path("purelib") -except ImportError: - from distutils.sysconfig import get_python_lib, get_python_version - def _get_purelib(): - return get_python_lib(False) +def _get_purelib(): + return get_path("purelib") def strip_module(filename): @@ -51,15 +41,19 @@ def sorted_walk(dir): def write_stub(resource, pyfile): - _stub_template = textwrap.dedent(""" + _stub_template = textwrap.dedent( + """ def __bootstrap__(): global __bootstrap__, __loader__, __file__ - import sys, pkg_resources, imp + import sys, pkg_resources, importlib.util __file__ = pkg_resources.resource_filename(__name__, %r) __loader__ = None; del __bootstrap__, __loader__ - imp.load_dynamic(__name__,__file__) + spec = importlib.util.spec_from_file_location(__name__,__file__) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) __bootstrap__() - """).lstrip() + """ + ).lstrip() with open(pyfile, 'w') as f: f.write(_stub_template % resource) @@ -68,24 +62,25 @@ class bdist_egg(Command): description = "create an \"egg\" distribution" user_options = [ - ('bdist-dir=', 'b', - "temporary directory for creating the distribution"), - ('plat-name=', 'p', "platform name to embed in generated filenames " - "(default: %s)" % get_build_platform()), - ('exclude-source-files', None, - "remove all .py files from the generated egg"), - ('keep-temp', 'k', - "keep the pseudo-installation tree around after " + - "creating the distribution archive"), - ('dist-dir=', 'd', - "directory to put final built distributions in"), - ('skip-build', None, - "skip rebuilding everything (for testing/debugging)"), + ('bdist-dir=', 'b', "temporary directory for creating the distribution"), + ( + 'plat-name=', + 'p', + "platform name to embed in generated filenames " + "(by default uses `pkg_resources.get_build_platform()`)", + ), + ('exclude-source-files', None, "remove all .py files from the generated egg"), + ( + 'keep-temp', + 'k', + "keep the pseudo-installation tree around after " + + "creating the distribution archive", + ), + ('dist-dir=', 'd', "directory to put final built distributions in"), + ('skip-build', None, "skip rebuilding everything (for testing/debugging)"), ] - boolean_options = [ - 'keep-temp', 'skip-build', 'exclude-source-files' - ] + boolean_options = ['keep-temp', 'skip-build', 'exclude-source-files'] def initialize_options(self): self.bdist_dir = None @@ -105,18 +100,18 @@ class bdist_egg(Command): self.bdist_dir = os.path.join(bdist_base, 'egg') if self.plat_name is None: + from pkg_resources import get_build_platform + self.plat_name = get_build_platform() self.set_undefined_options('bdist', ('dist_dir', 'dist_dir')) if self.egg_output is None: - # Compute filename of the output egg - basename = Distribution( - None, None, ei_cmd.egg_name, ei_cmd.egg_version, - get_python_version(), - self.distribution.has_ext_modules() and self.plat_name - ).egg_name() + basename = ei_cmd._get_egg_basename( + py_version=get_python_version(), + platform=self.distribution.has_ext_modules() and self.plat_name, + ) self.egg_output = os.path.join(self.dist_dir, basename + '.egg') @@ -135,7 +130,7 @@ class bdist_egg(Command): if normalized == site_packages or normalized.startswith( site_packages + os.sep ): - item = realpath[len(site_packages) + 1:], item[1] + item = realpath[len(site_packages) + 1 :], item[1] # XXX else: raise ??? self.distribution.data_files.append(item) @@ -158,7 +153,7 @@ class bdist_egg(Command): self.run_command(cmdname) return cmd - def run(self): + def run(self): # noqa: C901 # is too complex (14) # FIXME # Generate metadata first self.run_command("egg_info") # We run install_lib before install_data, because some data hacks @@ -175,10 +170,9 @@ class bdist_egg(Command): all_outputs, ext_outputs = self.get_ext_outputs() self.stubs = [] to_compile = [] - for (p, ext_name) in enumerate(ext_outputs): + for p, ext_name in enumerate(ext_outputs): filename, ext = os.path.splitext(ext_name) - pyfile = os.path.join(self.bdist_dir, strip_module(filename) + - '.py') + pyfile = os.path.join(self.bdist_dir, strip_module(filename) + '.py') self.stubs.append(pyfile) log.info("creating stub loader for %s", ext_name) if not self.dry_run: @@ -198,8 +192,7 @@ class bdist_egg(Command): if self.distribution.scripts: script_dir = os.path.join(egg_info, 'scripts') log.info("installing scripts to %s", script_dir) - self.call_command('install_scripts', install_dir=script_dir, - no_ep=1) + self.call_command('install_scripts', install_dir=script_dir, no_ep=1) self.copy_metadata_to(egg_info) native_libs = os.path.join(egg_info, "native_libs.txt") @@ -216,9 +209,7 @@ class bdist_egg(Command): if not self.dry_run: os.unlink(native_libs) - write_safety_flag( - os.path.join(archive_root, 'EGG-INFO'), self.zip_safe() - ) + write_safety_flag(os.path.join(archive_root, 'EGG-INFO'), self.zip_safe()) if os.path.exists(os.path.join(self.egg_info, 'depends.txt')): log.warn( @@ -230,14 +221,22 @@ class bdist_egg(Command): self.zap_pyfiles() # Make the archive - make_zipfile(self.egg_output, archive_root, verbose=self.verbose, - dry_run=self.dry_run, mode=self.gen_header()) + make_zipfile( + self.egg_output, + archive_root, + verbose=self.verbose, + dry_run=self.dry_run, + mode=self.gen_header(), + ) if not self.keep_temp: remove_tree(self.bdist_dir, dry_run=self.dry_run) # Add to 'Distribution.dist_files' so that the "upload" command works - getattr(self.distribution, 'dist_files', []).append( - ('bdist_egg', get_python_version(), self.egg_output)) + getattr(self.distribution, 'dist_files', []).append(( + 'bdist_egg', + get_python_version(), + self.egg_output, + )) def zap_pyfiles(self): log.info("Removing .py files from temporary directory") @@ -254,11 +253,8 @@ class bdist_egg(Command): pattern = r'(?P.+)\.(?P[^.]+)\.pyc' m = re.match(pattern, name) - path_new = os.path.join( - base, os.pardir, m.group('name') + '.pyc') - log.info( - "Renaming file from [%s] to [%s]" - % (path_old, path_new)) + path_new = os.path.join(base, os.pardir, m.group('name') + '.pyc') + log.info("Renaming file from [%s] to [%s]" % (path_old, path_new)) try: os.remove(path_new) except OSError: @@ -273,43 +269,7 @@ class bdist_egg(Command): return analyze_egg(self.bdist_dir, self.stubs) def gen_header(self): - epm = EntryPoint.parse_map(self.distribution.entry_points or '') - ep = epm.get('setuptools.installation', {}).get('eggsecutable') - if ep is None: - return 'w' # not an eggsecutable, do it the usual way. - - if not ep.attrs or ep.extras: - raise DistutilsSetupError( - "eggsecutable entry point (%r) cannot have 'extras' " - "or refer to a module" % (ep,) - ) - - pyver = '{}.{}'.format(*sys.version_info) - pkg = ep.module_name - full = '.'.join(ep.attrs) - base = ep.attrs[0] - basename = os.path.basename(self.egg_output) - - header = ( - "#!/bin/sh\n" - 'if [ `basename $0` = "%(basename)s" ]\n' - 'then exec python%(pyver)s -c "' - "import sys, os; sys.path.insert(0, os.path.abspath('$0')); " - "from %(pkg)s import %(base)s; sys.exit(%(full)s())" - '" "$@"\n' - 'else\n' - ' echo $0 is not the correct name for this egg file.\n' - ' echo Please rename it back to %(basename)s and try again.\n' - ' exec false\n' - 'fi\n' - ) % locals() - - if not self.dry_run: - mkpath(os.path.dirname(self.egg_output), dry_run=self.dry_run) - f = open(self.egg_output, 'w') - f.write(header) - f.close() - return 'a' + return 'w' def copy_metadata_to(self, target_dir): "Copy metadata (egg info) to the target_dir" @@ -319,7 +279,7 @@ class bdist_egg(Command): prefix = os.path.join(norm_egg_info, '') for path in self.ei_cmd.filelist.files: if path.startswith(prefix): - target = os.path.join(target_dir, path[len(prefix):]) + target = os.path.join(target_dir, path[len(prefix) :]) ensure_directory(target) self.copy_file(path, target) @@ -335,8 +295,7 @@ class bdist_egg(Command): if os.path.splitext(filename)[1].lower() in NATIVE_EXTENSIONS: all_outputs.append(paths[base] + filename) for filename in dirs: - paths[os.path.join(base, filename)] = (paths[base] + - filename + '/') + paths[os.path.join(base, filename)] = paths[base] + filename + '/' if self.distribution.has_ext_modules(): build_cmd = self.get_finalized_command('build_ext') @@ -362,8 +321,7 @@ def walk_egg(egg_dir): if 'EGG-INFO' in dirs: dirs.remove('EGG-INFO') yield base, dirs, files - for bdf in walker: - yield bdf + yield from walker def analyze_egg(egg_dir, stubs): @@ -409,14 +367,9 @@ def scan_module(egg_dir, base, name, stubs): filename = os.path.join(base, name) if filename[:-1] in stubs: return True # Extension module - pkg = base[len(egg_dir) + 1:].replace(os.sep, '.') + pkg = base[len(egg_dir) + 1 :].replace(os.sep, '.') module = pkg + (pkg and '.' or '') + os.path.splitext(name)[0] - if six.PY2: - skip = 8 # skip magic & date - elif sys.version_info < (3, 7): - skip = 12 # skip magic & date & file size - else: - skip = 16 # skip magic & reserved? & date & file size + skip = 16 # skip magic & reserved? & date & file size f = open(filename, 'rb') f.read(skip) code = marshal.load(f) @@ -429,9 +382,17 @@ def scan_module(egg_dir, base, name, stubs): safe = False if 'inspect' in symbols: for bad in [ - 'getsource', 'getabsfile', 'getsourcefile', 'getfile' - 'getsourcelines', 'findsource', 'getcomments', 'getframeinfo', - 'getinnerframes', 'getouterframes', 'stack', 'trace' + 'getsource', + 'getabsfile', + 'getsourcefile', + 'getfile' 'getsourcelines', + 'findsource', + 'getcomments', + 'getframeinfo', + 'getinnerframes', + 'getouterframes', + 'stack', + 'trace', ]: if bad in symbols: log.warn("%s: module MAY be using inspect.%s", module, bad) @@ -441,14 +402,12 @@ def scan_module(egg_dir, base, name, stubs): def iter_symbols(code): """Yield names and strings used by `code` and its nested code objects""" - for name in code.co_names: - yield name + yield from code.co_names for const in code.co_consts: - if isinstance(const, six.string_types): + if isinstance(const, str): yield const elif isinstance(const, CodeType): - for name in iter_symbols(const): - yield name + yield from iter_symbols(const) def can_scan(): @@ -456,20 +415,20 @@ def can_scan(): # CPython, PyPy, etc. return True log.warn("Unable to analyze compiled code on this platform.") - log.warn("Please ask the author to include a 'zip_safe'" - " setting (either True or False) in the package's setup.py") + log.warn( + "Please ask the author to include a 'zip_safe'" + " setting (either True or False) in the package's setup.py" + ) + return False # Attribute names of options for commands that might need to be convinced to # install to the egg build directory -INSTALL_DIRECTORY_ATTRS = [ - 'install_lib', 'install_dir', 'install_data', 'install_base' -] +INSTALL_DIRECTORY_ATTRS = ['install_lib', 'install_dir', 'install_data', 'install_base'] -def make_zipfile(zip_filename, base_dir, verbose=0, dry_run=0, compress=True, - mode='w'): +def make_zipfile(zip_filename, base_dir, verbose=0, dry_run=0, compress=True, mode='w'): """Create a zip file from all the files under 'base_dir'. The output zip file will be named 'base_dir' + ".zip". Uses either the "zipfile" Python module (if available) or the InfoZIP "zip" utility (if installed @@ -485,7 +444,7 @@ def make_zipfile(zip_filename, base_dir, verbose=0, dry_run=0, compress=True, for name in names: path = os.path.normpath(os.path.join(dirname, name)) if os.path.isfile(path): - p = path[len(base_dir) + 1:] + p = path[len(base_dir) + 1 :] if not dry_run: z.write(path, p) log.debug("adding '%s'", p) diff --git a/libs/common/setuptools/command/bdist_rpm.py b/libs/common/setuptools/command/bdist_rpm.py index 70730927..70ed6b60 100644 --- a/libs/common/setuptools/command/bdist_rpm.py +++ b/libs/common/setuptools/command/bdist_rpm.py @@ -1,5 +1,7 @@ import distutils.command.bdist_rpm as orig +from ..warnings import SetuptoolsDeprecationWarning + class bdist_rpm(orig.bdist_rpm): """ @@ -8,36 +10,30 @@ class bdist_rpm(orig.bdist_rpm): 1. Run egg_info to ensure the name and version are properly calculated. 2. Always run 'install' using --single-version-externally-managed to disable eggs in RPM distributions. - 3. Replace dash with underscore in the version numbers for better RPM - compatibility. """ def run(self): + SetuptoolsDeprecationWarning.emit( + "Deprecated command", + """ + bdist_rpm is deprecated and will be removed in a future version. + Use bdist_wheel (wheel packages) instead. + """, + see_url="https://github.com/pypa/setuptools/issues/1988", + due_date=(2023, 10, 30), # Deprecation introduced in 22 Oct 2021. + ) + # ensure distro name is up-to-date self.run_command('egg_info') orig.bdist_rpm.run(self) def _make_spec_file(self): - version = self.distribution.get_version() - rpmversion = version.replace('-', '_') spec = orig.bdist_rpm._make_spec_file(self) - line23 = '%define version ' + version - line24 = '%define version ' + rpmversion - spec = [ + return [ line.replace( - "Source0: %{name}-%{version}.tar", - "Source0: %{name}-%{unmangled_version}.tar" - ).replace( "setup.py install ", - "setup.py install --single-version-externally-managed " - ).replace( - "%setup", - "%setup -n %{name}-%{unmangled_version}" - ).replace(line23, line24) + "setup.py install --single-version-externally-managed ", + ).replace("%setup", "%setup -n %{name}-%{unmangled_version}") for line in spec ] - insert_loc = spec.index(line24) + 1 - unmangled_version = "%define unmangled_version " + version - spec.insert(insert_loc, unmangled_version) - return spec diff --git a/libs/common/setuptools/command/bdist_wininst.py b/libs/common/setuptools/command/bdist_wininst.py deleted file mode 100644 index 073de97b..00000000 --- a/libs/common/setuptools/command/bdist_wininst.py +++ /dev/null @@ -1,21 +0,0 @@ -import distutils.command.bdist_wininst as orig - - -class bdist_wininst(orig.bdist_wininst): - def reinitialize_command(self, command, reinit_subcommands=0): - """ - Supplement reinitialize_command to work around - http://bugs.python.org/issue20819 - """ - cmd = self.distribution.reinitialize_command( - command, reinit_subcommands) - if command in ('install', 'install_lib'): - cmd.install_lib = None - return cmd - - def run(self): - self._is_running = True - try: - orig.bdist_wininst.run(self) - finally: - self._is_running = False diff --git a/libs/common/setuptools/command/build_clib.py b/libs/common/setuptools/command/build_clib.py index 09caff6f..acd4d1d3 100644 --- a/libs/common/setuptools/command/build_clib.py +++ b/libs/common/setuptools/command/build_clib.py @@ -1,7 +1,12 @@ import distutils.command.build_clib as orig from distutils.errors import DistutilsSetupError from distutils import log -from setuptools.dep_util import newer_pairwise_group + +try: + from distutils._modified import newer_pairwise_group +except ImportError: + # fallback for SETUPTOOLS_USE_DISTUTILS=stdlib + from .._distutils._modified import newer_pairwise_group class build_clib(orig.build_clib): @@ -21,14 +26,15 @@ class build_clib(orig.build_clib): """ def build_libraries(self, libraries): - for (lib_name, build_info) in libraries: + for lib_name, build_info in libraries: sources = build_info.get('sources') if sources is None or not isinstance(sources, (list, tuple)): raise DistutilsSetupError( - "in 'libraries' option (library '%s'), " - "'sources' must be present and must be " - "a list of source filenames" % lib_name) - sources = list(sources) + "in 'libraries' option (library '%s'), " + "'sources' must be present and must be " + "a list of source filenames" % lib_name + ) + sources = sorted(list(sources)) log.info("building '%s' library", lib_name) @@ -38,9 +44,10 @@ class build_clib(orig.build_clib): obj_deps = build_info.get('obj_deps', dict()) if not isinstance(obj_deps, dict): raise DistutilsSetupError( - "in 'libraries' option (library '%s'), " - "'obj_deps' must be a dictionary of " - "type 'source: list'" % lib_name) + "in 'libraries' option (library '%s'), " + "'obj_deps' must be a dictionary of " + "type 'source: list'" % lib_name + ) dependencies = [] # Get the global dependencies that are specified by the '' key. @@ -48,9 +55,10 @@ class build_clib(orig.build_clib): global_deps = obj_deps.get('', list()) if not isinstance(global_deps, (list, tuple)): raise DistutilsSetupError( - "in 'libraries' option (library '%s'), " - "'obj_deps' must be a dictionary of " - "type 'source: list'" % lib_name) + "in 'libraries' option (library '%s'), " + "'obj_deps' must be a dictionary of " + "type 'source: list'" % lib_name + ) # Build the list to be used by newer_pairwise_group # each source will be auto-added to its dependencies. @@ -60,16 +68,17 @@ class build_clib(orig.build_clib): extra_deps = obj_deps.get(source, list()) if not isinstance(extra_deps, (list, tuple)): raise DistutilsSetupError( - "in 'libraries' option (library '%s'), " - "'obj_deps' must be a dictionary of " - "type 'source: list'" % lib_name) + "in 'libraries' option (library '%s'), " + "'obj_deps' must be a dictionary of " + "type 'source: list'" % lib_name + ) src_deps.extend(extra_deps) dependencies.append(src_deps) expected_objects = self.compiler.object_filenames( - sources, - output_dir=self.build_temp - ) + sources, + output_dir=self.build_temp, + ) if newer_pairwise_group(dependencies, expected_objects) != ([], []): # First, compile the source code to object files in the library @@ -78,21 +87,18 @@ class build_clib(orig.build_clib): macros = build_info.get('macros') include_dirs = build_info.get('include_dirs') cflags = build_info.get('cflags') - objects = self.compiler.compile( - sources, - output_dir=self.build_temp, - macros=macros, - include_dirs=include_dirs, - extra_postargs=cflags, - debug=self.debug - ) + self.compiler.compile( + sources, + output_dir=self.build_temp, + macros=macros, + include_dirs=include_dirs, + extra_postargs=cflags, + debug=self.debug, + ) # Now "link" the object files together into a static library. # (On Unix at least, this isn't really linking -- it just # builds an archive. Whatever.) self.compiler.create_static_lib( - expected_objects, - lib_name, - output_dir=self.build_clib, - debug=self.debug - ) + expected_objects, lib_name, output_dir=self.build_clib, debug=self.debug + ) diff --git a/libs/common/setuptools/command/build_ext.py b/libs/common/setuptools/command/build_ext.py index 1b51e040..780afe3a 100644 --- a/libs/common/setuptools/command/build_ext.py +++ b/libs/common/setuptools/command/build_ext.py @@ -1,26 +1,23 @@ import os import sys import itertools +from importlib.machinery import EXTENSION_SUFFIXES +from importlib.util import cache_from_source as _compiled_file_name +from typing import Dict, Iterator, List, Tuple +from pathlib import Path + from distutils.command.build_ext import build_ext as _du_build_ext -from distutils.file_util import copy_file from distutils.ccompiler import new_compiler from distutils.sysconfig import customize_compiler, get_config_var -from distutils.errors import DistutilsError from distutils import log -from setuptools.extension import Library -from setuptools.extern import six - -if six.PY2: - import imp - - EXTENSION_SUFFIXES = [s for s, _, tp in imp.get_suffixes() if tp == imp.C_EXTENSION] -else: - from importlib.machinery import EXTENSION_SUFFIXES +from setuptools.errors import BaseError +from setuptools.extension import Extension, Library try: # Attempt to use Cython for building extensions, if available from Cython.Distutils.build_ext import build_ext as _build_ext + # Additionally, assert that the compiler module will load # also. Ref #1229. __import__('Cython.Compiler.Main') @@ -29,7 +26,7 @@ except ImportError: # make sure _config_vars is initialized get_config_var("LDSHARED") -from distutils.sysconfig import _config_vars as _CONFIG_VARS +from distutils.sysconfig import _config_vars as _CONFIG_VARS # noqa def _customize_compiler_for_shlib(compiler): @@ -41,7 +38,8 @@ def _customize_compiler_for_shlib(compiler): try: # XXX Help! I don't have any idea whether these are right... _CONFIG_VARS['LDSHARED'] = ( - "gcc -Wl,-x -dynamiclib -undefined dynamic_lookup") + "gcc -Wl,-x -dynamiclib -undefined dynamic_lookup" + ) _CONFIG_VARS['CCSHARED'] = " -dynamiclib" _CONFIG_VARS['SO'] = ".dylib" customize_compiler(compiler) @@ -61,11 +59,14 @@ if sys.platform == "darwin": elif os.name != 'nt': try: import dl + use_stubs = have_rtld = hasattr(dl, 'RTLD_NOW') except ImportError: pass -if_dl = lambda s: s if have_rtld else '' + +def if_dl(s): + return s if have_rtld else '' def get_abi3_suffix(): @@ -75,9 +76,13 @@ def get_abi3_suffix(): return suffix elif suffix == '.pyd': # Windows return suffix + return None class build_ext(_build_ext): + editable_mode: bool = False + inplace: bool = False + def run(self): """Build extensions in build directory, then copy if --inplace""" old_inplace, self.inplace = self.inplace, 0 @@ -86,41 +91,78 @@ class build_ext(_build_ext): if old_inplace: self.copy_extensions_to_source() + def _get_inplace_equivalent(self, build_py, ext: Extension) -> Tuple[str, str]: + fullname = self.get_ext_fullname(ext.name) + filename = self.get_ext_filename(fullname) + modpath = fullname.split('.') + package = '.'.join(modpath[:-1]) + package_dir = build_py.get_package_dir(package) + inplace_file = os.path.join(package_dir, os.path.basename(filename)) + regular_file = os.path.join(self.build_lib, filename) + return (inplace_file, regular_file) + def copy_extensions_to_source(self): build_py = self.get_finalized_command('build_py') for ext in self.extensions: - fullname = self.get_ext_fullname(ext.name) - filename = self.get_ext_filename(fullname) - modpath = fullname.split('.') - package = '.'.join(modpath[:-1]) - package_dir = build_py.get_package_dir(package) - dest_filename = os.path.join(package_dir, - os.path.basename(filename)) - src_filename = os.path.join(self.build_lib, filename) + inplace_file, regular_file = self._get_inplace_equivalent(build_py, ext) # Always copy, even if source is older than destination, to ensure # that the right extensions for the current Python/platform are # used. - copy_file( - src_filename, dest_filename, verbose=self.verbose, - dry_run=self.dry_run - ) + if os.path.exists(regular_file) or not ext.optional: + self.copy_file(regular_file, inplace_file, level=self.verbose) + if ext._needs_stub: - self.write_stub(package_dir or os.curdir, ext, True) + inplace_stub = self._get_equivalent_stub(ext, inplace_file) + self._write_stub_file(inplace_stub, ext, compile=True) + # Always compile stub and remove the original (leave the cache behind) + # (this behaviour was observed in previous iterations of the code) + + def _get_equivalent_stub(self, ext: Extension, output_file: str) -> str: + dir_ = os.path.dirname(output_file) + _, _, name = ext.name.rpartition(".") + return f"{os.path.join(dir_, name)}.py" + + def _get_output_mapping(self) -> Iterator[Tuple[str, str]]: + if not self.inplace: + return + + build_py = self.get_finalized_command('build_py') + opt = self.get_finalized_command('install_lib').optimize or "" + + for ext in self.extensions: + inplace_file, regular_file = self._get_inplace_equivalent(build_py, ext) + yield (regular_file, inplace_file) + + if ext._needs_stub: + # This version of `build_ext` always builds artifacts in another dir, + # when "inplace=True" is given it just copies them back. + # This is done in the `copy_extensions_to_source` function, which + # always compile stub files via `_compile_and_remove_stub`. + # At the end of the process, a `.pyc` stub file is created without the + # corresponding `.py`. + + inplace_stub = self._get_equivalent_stub(ext, inplace_file) + regular_stub = self._get_equivalent_stub(ext, regular_file) + inplace_cache = _compiled_file_name(inplace_stub, optimization=opt) + output_cache = _compiled_file_name(regular_stub, optimization=opt) + yield (output_cache, inplace_cache) def get_ext_filename(self, fullname): - filename = _build_ext.get_ext_filename(self, fullname) + so_ext = os.getenv('SETUPTOOLS_EXT_SUFFIX') + if so_ext: + filename = os.path.join(*fullname.split('.')) + so_ext + else: + filename = _build_ext.get_ext_filename(self, fullname) + so_ext = get_config_var('EXT_SUFFIX') + if fullname in self.ext_map: ext = self.ext_map[fullname] - use_abi3 = ( - not six.PY2 - and getattr(ext, 'py_limited_api') - and get_abi3_suffix() - ) + use_abi3 = ext.py_limited_api and get_abi3_suffix() if use_abi3: - so_ext = get_config_var('EXT_SUFFIX') - filename = filename[:-len(so_ext)] - filename = filename + get_abi3_suffix() + filename = filename[: -len(so_ext)] + so_ext = get_abi3_suffix() + filename = filename + so_ext if isinstance(ext, Library): fn, ext = os.path.splitext(filename) return self.shlib_compiler.library_filename(fn, libtype) @@ -134,13 +176,13 @@ class build_ext(_build_ext): self.shlib_compiler = None self.shlibs = [] self.ext_map = {} + self.editable_mode = False def finalize_options(self): _build_ext.finalize_options(self) self.extensions = self.extensions or [] self.check_extensions_list(self.extensions) - self.shlibs = [ext for ext in self.extensions - if isinstance(ext, Library)] + self.shlibs = [ext for ext in self.extensions if isinstance(ext, Library)] if self.shlibs: self.setup_shlib_compiler() for ext in self.extensions: @@ -164,6 +206,9 @@ class build_ext(_build_ext): if ltd and use_stubs and os.curdir not in ext.runtime_library_dirs: ext.runtime_library_dirs.append(os.curdir) + if self.editable_mode: + self.inplace = True + def setup_shlib_compiler(self): compiler = self.shlib_compiler = new_compiler( compiler=self.compiler, dry_run=self.dry_run, force=self.force @@ -174,7 +219,7 @@ class build_ext(_build_ext): compiler.set_include_dirs(self.include_dirs) if self.define is not None: # 'define' option is a list of (name,value) tuples - for (name, value) in self.define: + for name, value in self.define: compiler.define_macro(name, value) if self.undef is not None: for macro in self.undef: @@ -204,8 +249,8 @@ class build_ext(_build_ext): self.compiler = self.shlib_compiler _build_ext.build_extension(self, ext) if ext._needs_stub: - cmd = self.get_finalized_command('build_py').build_lib - self.write_stub(cmd, ext) + build_lib = self.get_finalized_command('build_py').build_lib + self.write_stub(build_lib, ext) finally: self.compiler = _compiler @@ -218,8 +263,56 @@ class build_ext(_build_ext): pkg = '.'.join(ext._full_name.split('.')[:-1] + ['']) return any(pkg + libname in libnames for libname in ext.libraries) - def get_outputs(self): - return _build_ext.get_outputs(self) + self.__get_stubs_outputs() + def get_source_files(self) -> List[str]: + return [*_build_ext.get_source_files(self), *self._get_internal_depends()] + + def _get_internal_depends(self) -> Iterator[str]: + """Yield ``ext.depends`` that are contained by the project directory""" + project_root = Path(self.distribution.src_root or os.curdir).resolve() + depends = (dep for ext in self.extensions for dep in ext.depends) + + def skip(orig_path: str, reason: str) -> None: + log.info( + "dependency %s won't be automatically " + "included in the manifest: the path %s", + orig_path, + reason, + ) + + for dep in depends: + path = Path(dep) + + if path.is_absolute(): + skip(dep, "must be relative") + continue + + if ".." in path.parts: + skip(dep, "can't have `..` segments") + continue + + try: + resolved = (project_root / path).resolve(strict=True) + except OSError: + skip(dep, "doesn't exist") + continue + + try: + resolved.relative_to(project_root) + except ValueError: + skip(dep, "must be inside the project root") + continue + + yield path.as_posix() + + def get_outputs(self) -> List[str]: + if self.inplace: + return list(self.get_output_mapping().keys()) + return sorted(_build_ext.get_outputs(self) + self.__get_stubs_outputs()) + + def get_output_mapping(self) -> Dict[str, str]: + """See :class:`setuptools.commands.build.SubCommand`""" + mapping = self._get_output_mapping() + return dict(sorted(mapping, key=lambda x: x[0])) def __get_stubs_outputs(self): # assemble the base name for each extension that needs a stub @@ -239,22 +332,22 @@ class build_ext(_build_ext): yield '.pyo' def write_stub(self, output_dir, ext, compile=False): - log.info("writing stub loader for %s to %s", ext._full_name, - output_dir) - stub_file = (os.path.join(output_dir, *ext._full_name.split('.')) + - '.py') + stub_file = os.path.join(output_dir, *ext._full_name.split('.')) + '.py' + self._write_stub_file(stub_file, ext, compile) + + def _write_stub_file(self, stub_file: str, ext: Extension, compile=False): + log.info("writing stub loader for %s to %s", ext._full_name, stub_file) if compile and os.path.exists(stub_file): - raise DistutilsError(stub_file + " already exists! Please delete.") + raise BaseError(stub_file + " already exists! Please delete.") if not self.dry_run: f = open(stub_file, 'w') f.write( '\n'.join([ "def __bootstrap__():", " global __bootstrap__, __file__, __loader__", - " import sys, os, pkg_resources, imp" + if_dl(", dl"), + " import sys, os, pkg_resources, importlib.util" + if_dl(", dl"), " __file__ = pkg_resources.resource_filename" - "(__name__,%r)" - % os.path.basename(ext._file_name), + "(__name__,%r)" % os.path.basename(ext._file_name), " del __bootstrap__", " if '__loader__' in globals():", " del __loader__", @@ -263,51 +356,87 @@ class build_ext(_build_ext): " try:", " os.chdir(os.path.dirname(__file__))", if_dl(" sys.setdlopenflags(dl.RTLD_NOW)"), - " imp.load_dynamic(__name__,__file__)", + " spec = importlib.util.spec_from_file_location(", + " __name__, __file__)", + " mod = importlib.util.module_from_spec(spec)", + " spec.loader.exec_module(mod)", " finally:", if_dl(" sys.setdlopenflags(old_flags)"), " os.chdir(old_dir)", "__bootstrap__()", - "" # terminal \n + "", # terminal \n ]) ) f.close() if compile: - from distutils.util import byte_compile + self._compile_and_remove_stub(stub_file) - byte_compile([stub_file], optimize=0, - force=True, dry_run=self.dry_run) - optimize = self.get_finalized_command('install_lib').optimize - if optimize > 0: - byte_compile([stub_file], optimize=optimize, - force=True, dry_run=self.dry_run) - if os.path.exists(stub_file) and not self.dry_run: - os.unlink(stub_file) + def _compile_and_remove_stub(self, stub_file: str): + from distutils.util import byte_compile + + byte_compile([stub_file], optimize=0, force=True, dry_run=self.dry_run) + optimize = self.get_finalized_command('install_lib').optimize + if optimize > 0: + byte_compile( + [stub_file], optimize=optimize, force=True, dry_run=self.dry_run + ) + if os.path.exists(stub_file) and not self.dry_run: + os.unlink(stub_file) if use_stubs or os.name == 'nt': # Build shared libraries # def link_shared_object( - self, objects, output_libname, output_dir=None, libraries=None, - library_dirs=None, runtime_library_dirs=None, export_symbols=None, - debug=0, extra_preargs=None, extra_postargs=None, build_temp=None, - target_lang=None): + self, + objects, + output_libname, + output_dir=None, + libraries=None, + library_dirs=None, + runtime_library_dirs=None, + export_symbols=None, + debug=0, + extra_preargs=None, + extra_postargs=None, + build_temp=None, + target_lang=None, + ): self.link( - self.SHARED_LIBRARY, objects, output_libname, - output_dir, libraries, library_dirs, runtime_library_dirs, - export_symbols, debug, extra_preargs, extra_postargs, - build_temp, target_lang + self.SHARED_LIBRARY, + objects, + output_libname, + output_dir, + libraries, + library_dirs, + runtime_library_dirs, + export_symbols, + debug, + extra_preargs, + extra_postargs, + build_temp, + target_lang, ) + else: # Build static libraries everywhere else libtype = 'static' def link_shared_object( - self, objects, output_libname, output_dir=None, libraries=None, - library_dirs=None, runtime_library_dirs=None, export_symbols=None, - debug=0, extra_preargs=None, extra_postargs=None, build_temp=None, - target_lang=None): + self, + objects, + output_libname, + output_dir=None, + libraries=None, + library_dirs=None, + runtime_library_dirs=None, + export_symbols=None, + debug=0, + extra_preargs=None, + extra_postargs=None, + build_temp=None, + target_lang=None, + ): # XXX we need to either disallow these attrs on Library instances, # or warn/abort here if set, or something... # libraries=None, library_dirs=None, runtime_library_dirs=None, @@ -322,6 +451,4 @@ else: # a different prefix basename = basename[3:] - self.create_static_lib( - objects, basename, output_dir, debug, target_lang - ) + self.create_static_lib(objects, basename, output_dir, debug, target_lang) diff --git a/libs/common/setuptools/command/build_py.py b/libs/common/setuptools/command/build_py.py index b0314fd4..3f40b060 100644 --- a/libs/common/setuptools/command/build_py.py +++ b/libs/common/setuptools/command/build_py.py @@ -1,26 +1,28 @@ +from functools import partial from glob import glob from distutils.util import convert_path import distutils.command.build_py as orig import os import fnmatch import textwrap -import io import distutils.errors import itertools +import stat +from pathlib import Path +from typing import Dict, Iterable, Iterator, List, Optional, Tuple -from setuptools.extern import six -from setuptools.extern.six.moves import map, filter, filterfalse - -try: - from setuptools.lib2to3_ex import Mixin2to3 -except ImportError: - - class Mixin2to3: - def run_2to3(self, files, doctests=True): - "do nothing" +from ..extern.more_itertools import unique_everseen +from ..warnings import SetuptoolsDeprecationWarning -class build_py(orig.build_py, Mixin2to3): +_IMPLICIT_DATA_FILES = ('*.pyi', 'py.typed') + + +def make_writable(target): + os.chmod(target, os.stat(target).st_mode | stat.S_IWRITE) + + +class build_py(orig.build_py): """Enhanced 'build_py' command that includes data files with packages The data files are specified via a 'package_data' argument to 'setup()'. @@ -30,19 +32,31 @@ class build_py(orig.build_py, Mixin2to3): 'py_modules' and 'packages' in the same setup operation. """ + editable_mode: bool = False + existing_egg_info_dir: Optional[str] = None #: Private API, internal use only. + def finalize_options(self): orig.build_py.finalize_options(self) self.package_data = self.distribution.package_data - self.exclude_package_data = (self.distribution.exclude_package_data or - {}) + self.exclude_package_data = self.distribution.exclude_package_data or {} if 'data_files' in self.__dict__: del self.__dict__['data_files'] self.__updated_files = [] - self.__doctests_2to3 = [] + + def copy_file( + self, infile, outfile, preserve_mode=1, preserve_times=1, link=None, level=1 + ): + # Overwrite base class to allow using links + if link: + infile = str(Path(infile).resolve()) + outfile = str(Path(outfile).resolve()) + return super().copy_file( + infile, outfile, preserve_mode, preserve_times, link, level + ) def run(self): """Build modules, packages, and copy data files to build directory""" - if not self.py_modules and not self.packages: + if not (self.py_modules or self.packages) or self.editable_mode: return if self.py_modules: @@ -52,10 +66,6 @@ class build_py(orig.build_py, Mixin2to3): self.build_packages() self.build_package_data() - self.run_2to3(self.__updated_files, False) - self.run_2to3(self.__updated_files, True) - self.run_2to3(self.__doctests_2to3, True) - # Only compile actual .py files, using our base class' idea of what our # output files are. self.byte_compile(orig.build_py.get_outputs(self, include_bytecode=0)) @@ -68,11 +78,7 @@ class build_py(orig.build_py, Mixin2to3): return orig.build_py.__getattr__(self, attr) def build_module(self, module, module_file, package): - if six.PY2 and isinstance(package, six.string_types): - # avoid errors on Python 2 when unicode is passed (#190) - package = package.split('.') - outfile, copied = orig.build_py.build_module(self, module, module_file, - package) + outfile, copied = orig.build_py.build_module(self, module, module_file, package) if copied: self.__updated_files.append(outfile) return outfile, copied @@ -82,6 +88,16 @@ class build_py(orig.build_py, Mixin2to3): self.analyze_manifest() return list(map(self._get_pkg_data_files, self.packages or ())) + def get_data_files_without_manifest(self): + """ + Generate list of ``(package,src_dir,build_dir,filenames)`` tuples, + but without triggering any attempt to analyze or build the manifest. + """ + # Prevent eventual errors from unset `manifest_files` + # (that would otherwise be set by `analyze_manifest`) + self.__dict__.setdefault('manifest_files', {}) + return list(map(self._get_pkg_data_files, self.packages or ())) + def _get_pkg_data_files(self, package): # Locate package source directory src_dir = self.get_package_dir(package) @@ -102,8 +118,9 @@ class build_py(orig.build_py, Mixin2to3): self.package_data, package, src_dir, + extra_patterns=_IMPLICIT_DATA_FILES, ) - globs_expanded = map(glob, patterns) + globs_expanded = map(partial(glob, recursive=True), patterns) # flatten the expanded globs into an iterable of matches globs_matches = itertools.chain.from_iterable(globs_expanded) glob_files = filter(os.path.isfile, globs_matches) @@ -113,18 +130,41 @@ class build_py(orig.build_py, Mixin2to3): ) return self.exclude_data_files(package, src_dir, files) - def build_package_data(self): - """Copy data files into build directory""" + def get_outputs(self, include_bytecode=1) -> List[str]: + """See :class:`setuptools.commands.build.SubCommand`""" + if self.editable_mode: + return list(self.get_output_mapping().keys()) + return super().get_outputs(include_bytecode) + + def get_output_mapping(self) -> Dict[str, str]: + """See :class:`setuptools.commands.build.SubCommand`""" + mapping = itertools.chain( + self._get_package_data_output_mapping(), + self._get_module_mapping(), + ) + return dict(sorted(mapping, key=lambda x: x[0])) + + def _get_module_mapping(self) -> Iterator[Tuple[str, str]]: + """Iterate over all modules producing (dest, src) pairs.""" + for package, module, module_file in self.find_all_modules(): + package = package.split('.') + filename = self.get_module_outfile(self.build_lib, package, module) + yield (filename, module_file) + + def _get_package_data_output_mapping(self) -> Iterator[Tuple[str, str]]: + """Iterate over package data producing (dest, src) pairs.""" for package, src_dir, build_dir, filenames in self.data_files: for filename in filenames: target = os.path.join(build_dir, filename) - self.mkpath(os.path.dirname(target)) srcfile = os.path.join(src_dir, filename) - outf, copied = self.copy_file(srcfile, target) - srcfile = os.path.abspath(srcfile) - if (copied and - srcfile in self.distribution.convert_2to3_doctests): - self.__doctests_2to3.append(outf) + yield (target, srcfile) + + def build_package_data(self): + """Copy data files into build directory""" + for target, srcfile in self._get_package_data_output_mapping(): + self.mkpath(os.path.dirname(target)) + _outf, _copied = self.copy_file(srcfile, target) + make_writable(target) def analyze_manifest(self): self.manifest_files = mf = {} @@ -135,9 +175,21 @@ class build_py(orig.build_py, Mixin2to3): # Locate package source directory src_dirs[assert_relative(self.get_package_dir(package))] = package - self.run_command('egg_info') - ei_cmd = self.get_finalized_command('egg_info') - for path in ei_cmd.filelist.files: + if ( + getattr(self, 'existing_egg_info_dir', None) + and Path(self.existing_egg_info_dir, "SOURCES.txt").exists() + ): + egg_info_dir = self.existing_egg_info_dir + manifest = Path(egg_info_dir, "SOURCES.txt") + files = manifest.read_text(encoding="utf-8").splitlines() + else: + self.run_command('egg_info') + ei_cmd = self.get_finalized_command('egg_info') + egg_info_dir = ei_cmd.egg_info + files = ei_cmd.filelist.files + + check = _IncludePackageDataAbuse() + for path in self._filter_build_files(files, egg_info_dir): d, f = os.path.split(assert_relative(path)) prev = None oldf = f @@ -146,10 +198,34 @@ class build_py(orig.build_py, Mixin2to3): d, df = os.path.split(d) f = os.path.join(df, f) if d in src_dirs: - if path.endswith('.py') and f == oldf: - continue # it's a module, not data + if f == oldf: + if check.is_module(f): + continue # it's a module, not data + else: + importable = check.importable_subpackage(src_dirs[d], f) + if importable: + check.warn(importable) mf.setdefault(src_dirs[d], []).append(path) + def _filter_build_files(self, files: Iterable[str], egg_info: str) -> Iterator[str]: + """ + ``build_meta`` may try to create egg_info outside of the project directory, + and this can be problematic for certain plugins (reported in issue #3500). + + Extensions might also include between their sources files created on the + ``build_lib`` and ``build_temp`` directories. + + This function should filter this case of invalid files out. + """ + build = self.get_finalized_command("build") + build_dirs = (egg_info, self.build_lib, build.build_temp, build.build_base) + norm_dirs = [os.path.normpath(p) for p in build_dirs if p] + + for file in files: + norm_path = os.path.normpath(file) + if not os.path.isabs(file) or all(d not in norm_path for d in norm_dirs): + yield file + def get_data_files(self): pass # Lazily compute data files in _get_data_files() function. @@ -172,7 +248,7 @@ class build_py(orig.build_py, Mixin2to3): else: return init_py - with io.open(init_py, 'rb') as f: + with open(init_py, 'rb') as f: contents = f.read() if b'declare_namespace' not in contents: raise distutils.errors.DistutilsError( @@ -186,6 +262,8 @@ class build_py(orig.build_py, Mixin2to3): def initialize_options(self): self.packages_checked = {} orig.build_py.initialize_options(self) + self.editable_mode = False + self.existing_egg_info_dir = None def get_package_dir(self, package): res = orig.build_py.get_package_dir(self, package) @@ -201,23 +279,16 @@ class build_py(orig.build_py, Mixin2to3): package, src_dir, ) - match_groups = ( - fnmatch.filter(files, pattern) - for pattern in patterns - ) + match_groups = (fnmatch.filter(files, pattern) for pattern in patterns) # flatten the groups of matches into an iterable of matches matches = itertools.chain.from_iterable(match_groups) bad = set(matches) - keepers = ( - fn - for fn in files - if fn not in bad - ) + keepers = (fn for fn in files if fn not in bad) # ditch dupes - return list(_unique_everseen(keepers)) + return list(unique_everseen(keepers)) @staticmethod - def _get_platform_patterns(spec, package, src_dir): + def _get_platform_patterns(spec, package, src_dir, extra_patterns=()): """ yield platform-specific path patterns (suitable for glob or fn_match) from a glob-based spec (such as @@ -225,6 +296,7 @@ class build_py(orig.build_py, Mixin2to3): matching package in src_dir. """ raw_patterns = itertools.chain( + extra_patterns, spec.get('', []), spec.get(package, []), ) @@ -235,36 +307,87 @@ class build_py(orig.build_py, Mixin2to3): ) -# from Python docs -def _unique_everseen(iterable, key=None): - "List unique elements, preserving order. Remember all elements ever seen." - # unique_everseen('AAAABBBCCDAABBB') --> A B C D - # unique_everseen('ABBCcAD', str.lower) --> A B C D - seen = set() - seen_add = seen.add - if key is None: - for element in filterfalse(seen.__contains__, iterable): - seen_add(element) - yield element - else: - for element in iterable: - k = key(element) - if k not in seen: - seen_add(k) - yield element - - def assert_relative(path): if not os.path.isabs(path): return path from distutils.errors import DistutilsSetupError - msg = textwrap.dedent(""" + msg = ( + textwrap.dedent( + """ Error: setup script specifies an absolute path: %s setup() arguments must *always* be /-separated paths relative to the setup.py directory, *never* absolute paths. - """).lstrip() % path + """ + ).lstrip() + % path + ) raise DistutilsSetupError(msg) + + +class _IncludePackageDataAbuse: + """Inform users that package or module is included as 'data file'""" + + class _Warning(SetuptoolsDeprecationWarning): + _SUMMARY = """ + Package {importable!r} is absent from the `packages` configuration. + """ + + _DETAILS = """ + ############################ + # Package would be ignored # + ############################ + Python recognizes {importable!r} as an importable package[^1], + but it is absent from setuptools' `packages` configuration. + + This leads to an ambiguous overall configuration. If you want to distribute this + package, please make sure that {importable!r} is explicitly added + to the `packages` configuration field. + + Alternatively, you can also rely on setuptools' discovery methods + (for example by using `find_namespace_packages(...)`/`find_namespace:` + instead of `find_packages(...)`/`find:`). + + You can read more about "package discovery" on setuptools documentation page: + + - https://setuptools.pypa.io/en/latest/userguide/package_discovery.html + + If you don't want {importable!r} to be distributed and are + already explicitly excluding {importable!r} via + `find_namespace_packages(...)/find_namespace` or `find_packages(...)/find`, + you can try to use `exclude_package_data`, or `include-package-data=False` in + combination with a more fine grained `package-data` configuration. + + You can read more about "package data files" on setuptools documentation page: + + - https://setuptools.pypa.io/en/latest/userguide/datafiles.html + + + [^1]: For Python, any directory (with suitable naming) can be imported, + even if it does not contain any `.py` files. + On the other hand, currently there is no concept of package data + directory, all directories are treated like packages. + """ + # _DUE_DATE: still not defined as this is particularly controversial. + # Warning initially introduced in May 2022. See issue #3340 for discussion. + + def __init__(self): + self._already_warned = set() + + def is_module(self, file): + return file.endswith(".py") and file[: -len(".py")].isidentifier() + + def importable_subpackage(self, parent, file): + pkg = Path(file).parent + parts = list(itertools.takewhile(str.isidentifier, pkg.parts)) + if parts: + return ".".join([parent, *parts]) + return None + + def warn(self, importable): + if importable not in self._already_warned: + self._Warning.emit(importable=importable) + self._already_warned.add(importable) diff --git a/libs/common/setuptools/command/develop.py b/libs/common/setuptools/command/develop.py index b5619246..d8c1b49b 100644 --- a/libs/common/setuptools/command/develop.py +++ b/libs/common/setuptools/command/develop.py @@ -1,19 +1,15 @@ from distutils.util import convert_path from distutils import log -from distutils.errors import DistutilsError, DistutilsOptionError +from distutils.errors import DistutilsOptionError import os import glob -import io -from setuptools.extern import six - -import pkg_resources from setuptools.command.easy_install import easy_install +from setuptools import _normalization +from setuptools import _path from setuptools import namespaces import setuptools -__metaclass__ = type - class develop(namespaces.DevelopInstaller, easy_install): """Set up package for development""" @@ -46,11 +42,9 @@ class develop(namespaces.DevelopInstaller, easy_install): self.always_copy_from = '.' # always copy eggs installed in curdir def finalize_options(self): + import pkg_resources + ei = self.get_finalized_command("egg_info") - if ei.broken_egg_info: - template = "Please rename %r to %r before using 'develop'" - args = ei.egg_info, ei.broken_egg_info - raise DistutilsError(template % args) self.args = [ei.egg_name] easy_install.finalize_options(self) @@ -59,15 +53,16 @@ class develop(namespaces.DevelopInstaller, easy_install): # pick up setup-dir .egg files only: no .egg-info self.package_index.scan(glob.glob('*.egg')) - egg_link_fn = ei.egg_name + '.egg-link' + egg_link_fn = ( + _normalization.filename_component_broken(ei.egg_name) + '.egg-link' + ) self.egg_link = os.path.join(self.install_dir, egg_link_fn) self.egg_base = ei.egg_base if self.egg_path is None: self.egg_path = os.path.abspath(ei.egg_base) - target = pkg_resources.normalize_path(self.egg_base) - egg_path = pkg_resources.normalize_path( - os.path.join(self.install_dir, self.egg_path)) + target = _path.normpath(self.egg_base) + egg_path = _path.normpath(os.path.join(self.install_dir, self.egg_path)) if egg_path != target: raise DistutilsOptionError( "--egg-path must be a relative path from the install" @@ -78,7 +73,7 @@ class develop(namespaces.DevelopInstaller, easy_install): self.dist = pkg_resources.Distribution( target, pkg_resources.PathMetadata(target, os.path.abspath(ei.egg_info)), - project_name=ei.egg_name + project_name=ei.egg_name, ) self.setup_path = self._resolve_setup_path( @@ -97,49 +92,24 @@ class develop(namespaces.DevelopInstaller, easy_install): path_to_setup = egg_base.replace(os.sep, '/').rstrip('/') if path_to_setup != os.curdir: path_to_setup = '../' * (path_to_setup.count('/') + 1) - resolved = pkg_resources.normalize_path( - os.path.join(install_dir, egg_path, path_to_setup) - ) - if resolved != pkg_resources.normalize_path(os.curdir): + resolved = _path.normpath(os.path.join(install_dir, egg_path, path_to_setup)) + curdir = _path.normpath(os.curdir) + if resolved != curdir: raise DistutilsOptionError( "Can't get a consistent path to setup script from" - " installation directory", resolved, - pkg_resources.normalize_path(os.curdir)) + " installation directory", + resolved, + curdir, + ) return path_to_setup def install_for_development(self): - if not six.PY2 and getattr(self.distribution, 'use_2to3', False): - # If we run 2to3 we can not do this inplace: + self.run_command('egg_info') - # Ensure metadata is up-to-date - self.reinitialize_command('build_py', inplace=0) - self.run_command('build_py') - bpy_cmd = self.get_finalized_command("build_py") - build_path = pkg_resources.normalize_path(bpy_cmd.build_lib) + # Build extensions in-place + self.reinitialize_command('build_ext', inplace=1) + self.run_command('build_ext') - # Build extensions - self.reinitialize_command('egg_info', egg_base=build_path) - self.run_command('egg_info') - - self.reinitialize_command('build_ext', inplace=0) - self.run_command('build_ext') - - # Fixup egg-link and easy-install.pth - ei_cmd = self.get_finalized_command("egg_info") - self.egg_path = build_path - self.dist.location = build_path - # XXX - self.dist._provider = pkg_resources.PathMetadata( - build_path, ei_cmd.egg_info) - else: - # Without 2to3 inplace works fine: - self.run_command('egg_info') - - # Build extensions in-place - self.reinitialize_command('build_ext', inplace=1) - self.run_command('build_ext') - - self.install_site_py() # ensure that target dir is site-safe if setuptools.bootstrap_install_from: self.easy_install(setuptools.bootstrap_install_from) setuptools.bootstrap_install_from = None @@ -161,8 +131,7 @@ class develop(namespaces.DevelopInstaller, easy_install): egg_link_file = open(self.egg_link) contents = [line.rstrip() for line in egg_link_file] egg_link_file.close() - if contents not in ([self.egg_path], - [self.egg_path, self.setup_path]): + if contents not in ([self.egg_path], [self.egg_path, self.setup_path]): log.warn("Link points to %s: uninstall aborted", contents) return if not self.dry_run: @@ -187,10 +156,12 @@ class develop(namespaces.DevelopInstaller, easy_install): for script_name in self.distribution.scripts or []: script_path = os.path.abspath(convert_path(script_name)) script_name = os.path.basename(script_path) - with io.open(script_path) as strm: + with open(script_path) as strm: script_text = strm.read() self.install_script(dist, script_name, script_text, script_path) + return None + def install_wrapper_scripts(self, dist): dist = VersionlessRequirement(dist) return easy_install.install_wrapper_scripts(self, dist) diff --git a/libs/common/setuptools/command/dist_info.py b/libs/common/setuptools/command/dist_info.py index c45258fa..f5061afa 100644 --- a/libs/common/setuptools/command/dist_info.py +++ b/libs/common/setuptools/command/dist_info.py @@ -4,33 +4,101 @@ As defined in the wheel specification """ import os - -from distutils.core import Command +import shutil +from contextlib import contextmanager from distutils import log +from distutils.core import Command +from pathlib import Path + +from .. import _normalization class dist_info(Command): + """ + This command is private and reserved for internal use of setuptools, + users should rely on ``setuptools.build_meta`` APIs. + """ - description = 'create a .dist-info directory' + description = "DO NOT CALL DIRECTLY, INTERNAL ONLY: create .dist-info directory" user_options = [ - ('egg-base=', 'e', "directory containing .egg-info directories" - " (default: top of the source tree)"), + ( + 'output-dir=', + 'o', + "directory inside of which the .dist-info will be" + "created (default: top of the source tree)", + ), + ('tag-date', 'd', "Add date stamp (e.g. 20050528) to version number"), + ('tag-build=', 'b', "Specify explicit tag to add to version number"), + ('no-date', 'D', "Don't include date stamp [default]"), + ('keep-egg-info', None, "*TRANSITIONAL* will be removed in the future"), ] + boolean_options = ['tag-date', 'keep-egg-info'] + negative_opt = {'no-date': 'tag-date'} + def initialize_options(self): - self.egg_base = None + self.output_dir = None + self.name = None + self.dist_info_dir = None + self.tag_date = None + self.tag_build = None + self.keep_egg_info = False def finalize_options(self): - pass + dist = self.distribution + project_dir = dist.src_root or os.curdir + self.output_dir = Path(self.output_dir or project_dir) + + egg_info = self.reinitialize_command("egg_info") + egg_info.egg_base = str(self.output_dir) + + if self.tag_date: + egg_info.tag_date = self.tag_date + else: + self.tag_date = egg_info.tag_date + + if self.tag_build: + egg_info.tag_build = self.tag_build + else: + self.tag_build = egg_info.tag_build + + egg_info.finalize_options() + self.egg_info = egg_info + + name = _normalization.safer_name(dist.get_name()) + version = _normalization.safer_best_effort_version(dist.get_version()) + self.name = f"{name}-{version}" + self.dist_info_dir = os.path.join(self.output_dir, f"{self.name}.dist-info") + + @contextmanager + def _maybe_bkp_dir(self, dir_path: str, requires_bkp: bool): + if requires_bkp: + bkp_name = f"{dir_path}.__bkp__" + _rm(bkp_name, ignore_errors=True) + shutil.copytree(dir_path, bkp_name, dirs_exist_ok=True, symlinks=True) + try: + yield + finally: + _rm(dir_path, ignore_errors=True) + shutil.move(bkp_name, dir_path) + else: + yield def run(self): - egg_info = self.get_finalized_command('egg_info') - egg_info.egg_base = self.egg_base - egg_info.finalize_options() - egg_info.run() - dist_info_dir = egg_info.egg_info[:-len('.egg-info')] + '.dist-info' - log.info("creating '{}'".format(os.path.abspath(dist_info_dir))) + self.output_dir.mkdir(parents=True, exist_ok=True) + self.egg_info.run() + egg_info_dir = self.egg_info.egg_info + assert os.path.isdir(egg_info_dir), ".egg-info dir should have been created" + log.info("creating '{}'".format(os.path.abspath(self.dist_info_dir))) bdist_wheel = self.get_finalized_command('bdist_wheel') - bdist_wheel.egg2dist(egg_info.egg_info, dist_info_dir) + + # TODO: if bdist_wheel if merged into setuptools, just add "keep_egg_info" there + with self._maybe_bkp_dir(egg_info_dir, self.keep_egg_info): + bdist_wheel.egg2dist(egg_info_dir, self.dist_info_dir) + + +def _rm(dir_name, **opts): + if os.path.isdir(dir_name): + shutil.rmtree(dir_name, **opts) diff --git a/libs/common/setuptools/command/easy_install.py b/libs/common/setuptools/command/easy_install.py index 426301d6..cc0c4091 100644 --- a/libs/common/setuptools/command/easy_install.py +++ b/libs/common/setuptools/command/easy_install.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """ Easy Install ------------ @@ -7,7 +6,7 @@ A tool for doing automatic download/extract/build of distutils-based Python packages. For detailed documentation, see the accompanying EasyInstall.txt file, or visit the `EasyInstall home page`__. -__ https://setuptools.readthedocs.io/en/latest/easy_install.html +__ https://setuptools.pypa.io/en/latest/deprecated/easy_install.html """ @@ -15,13 +14,15 @@ from glob import glob from distutils.util import get_platform from distutils.util import convert_path, subst_vars from distutils.errors import ( - DistutilsArgError, DistutilsOptionError, - DistutilsError, DistutilsPlatformError, + DistutilsArgError, + DistutilsOptionError, + DistutilsError, + DistutilsPlatformError, ) -from distutils.command.install import INSTALL_SCHEMES, SCHEME_KEYS from distutils import log, dir_util from distutils.command.build_scripts import first_line_re from distutils.spawn import find_executable +from distutils.command import install import sys import os import zipimport @@ -39,41 +40,53 @@ import contextlib import subprocess import shlex import io +import configparser +import sysconfig -from sysconfig import get_config_vars, get_path - -from setuptools import SetuptoolsDeprecationWarning - -from setuptools.extern import six -from setuptools.extern.six.moves import configparser, map +from sysconfig import get_path from setuptools import Command from setuptools.sandbox import run_setup -from setuptools.py27compat import rmtree_safe from setuptools.command import setopt from setuptools.archive_util import unpack_archive from setuptools.package_index import ( - PackageIndex, parse_requirement_arg, URL_SCHEME, + PackageIndex, + parse_requirement_arg, + URL_SCHEME, ) from setuptools.command import bdist_egg, egg_info +from setuptools.warnings import SetuptoolsDeprecationWarning, SetuptoolsWarning from setuptools.wheel import Wheel from pkg_resources import ( - yield_lines, normalize_path, resource_string, ensure_directory, - get_distribution, find_distributions, Environment, Requirement, - Distribution, PathMetadata, EggMetadata, WorkingSet, DistributionNotFound, - VersionConflict, DEVELOP_DIST, + normalize_path, + resource_string, + get_distribution, + find_distributions, + Environment, + Requirement, + Distribution, + PathMetadata, + EggMetadata, + WorkingSet, + DistributionNotFound, + VersionConflict, + DEVELOP_DIST, ) -import pkg_resources.py31compat +import pkg_resources +from ..compat import py311 +from .._path import ensure_directory +from ..extern.jaraco.text import yield_lines -__metaclass__ = type # Turn on PEP440Warnings warnings.filterwarnings("default", category=pkg_resources.PEP440Warning) __all__ = [ - 'samefile', 'easy_install', 'PthDistributions', 'extract_wininst_cfg', - 'main', 'get_exe_prefixes', + 'easy_install', + 'PthDistributions', + 'extract_wininst_cfg', + 'get_exe_prefixes', ] @@ -81,51 +94,25 @@ def is_64bit(): return struct.calcsize("P") == 8 -def samefile(p1, p2): - """ - Determine if two paths reference the same file. - - Augments os.path.samefile to work on Windows and - suppresses errors if the path doesn't exist. - """ - both_exist = os.path.exists(p1) and os.path.exists(p2) - use_samefile = hasattr(os.path, 'samefile') and both_exist - if use_samefile: - return os.path.samefile(p1, p2) - norm_p1 = os.path.normpath(os.path.normcase(p1)) - norm_p2 = os.path.normpath(os.path.normcase(p2)) - return norm_p1 == norm_p2 +def _to_bytes(s): + return s.encode('utf8') -if six.PY2: - - def _to_bytes(s): - return s - - def isascii(s): - try: - six.text_type(s, 'ascii') - return True - except UnicodeError: - return False -else: - - def _to_bytes(s): - return s.encode('utf8') - - def isascii(s): - try: - s.encode('ascii') - return True - except UnicodeError: - return False +def isascii(s): + try: + s.encode('ascii') + return True + except UnicodeError: + return False -_one_liner = lambda text: textwrap.dedent(text).strip().replace('\n', '; ') +def _one_liner(text): + return textwrap.dedent(text).strip().replace('\n', '; ') class easy_install(Command): """Manage a download/build/install process""" + description = "Find/get/install Python packages" command_consumes_arguments = True @@ -140,39 +127,47 @@ class easy_install(Command): ("always-copy", "a", "Copy all needed packages to install dir"), ("index-url=", "i", "base URL of Python Package Index"), ("find-links=", "f", "additional URL(s) to search for packages"), - ("build-directory=", "b", - "download/extract/build in DIR; keep the results"), - ('optimize=', 'O', - "also compile with optimization: -O1 for \"python -O\", " - "-O2 for \"python -OO\", and -O0 to disable [default: -O0]"), - ('record=', None, - "filename in which to record list of installed files"), + ("build-directory=", "b", "download/extract/build in DIR; keep the results"), + ( + 'optimize=', + 'O', + "also compile with optimization: -O1 for \"python -O\", " + "-O2 for \"python -OO\", and -O0 to disable [default: -O0]", + ), + ('record=', None, "filename in which to record list of installed files"), ('always-unzip', 'Z', "don't install as a zipfile, no matter what"), ('site-dirs=', 'S', "list of directories where .pth files work"), ('editable', 'e', "Install specified packages in editable form"), ('no-deps', 'N', "don't install dependencies"), ('allow-hosts=', 'H', "pattern(s) that hostnames must match"), - ('local-snapshots-ok', 'l', - "allow building eggs from local checkouts"), + ('local-snapshots-ok', 'l', "allow building eggs from local checkouts"), ('version', None, "print version information and exit"), - ('no-find-links', None, - "Don't load find-links defined in packages being installed") + ( + 'no-find-links', + None, + "Don't load find-links defined in packages being installed", + ), + ('user', None, "install in user site-package '%s'" % site.USER_SITE), ] boolean_options = [ - 'zip-ok', 'multi-version', 'exclude-scripts', 'upgrade', 'always-copy', + 'zip-ok', + 'multi-version', + 'exclude-scripts', + 'upgrade', + 'always-copy', 'editable', - 'no-deps', 'local-snapshots-ok', 'version' + 'no-deps', + 'local-snapshots-ok', + 'version', + 'user', ] - if site.ENABLE_USER_SITE: - help_msg = "install in user site-package '%s'" % site.USER_SITE - user_options.append(('user', None, help_msg)) - boolean_options.append('user') - negative_opt = {'always-unzip': 'zip-ok'} create_index = PackageIndex def initialize_options(self): + EasyInstallDeprecationWarning.emit() + # the --user option seems to be an opt-in one, # so the default should be False. self.user = 0 @@ -195,12 +190,8 @@ class easy_install(Command): self.install_data = None self.install_base = None self.install_platbase = None - if site.ENABLE_USER_SITE: - self.install_userbase = site.USER_BASE - self.install_usersite = site.USER_SITE - else: - self.install_userbase = None - self.install_usersite = None + self.install_userbase = site.USER_BASE + self.install_usersite = site.USER_SITE self.no_find_links = None # Options not specifiable via command line @@ -208,7 +199,6 @@ class easy_install(Command): self.pth_file = self.always_copy_from = None self.site_dirs = None self.installed_projects = {} - self.sitepy_installed = False # Always read easy_install options, even if we are subclassed, or have # an independent instance created. This ensures that defaults will # always come from the standard configuration file(s)' "easy_install" @@ -222,7 +212,8 @@ class easy_install(Command): def delete_blockers(self, blockers): extant_blockers = ( - filename for filename in blockers + filename + for filename in blockers if os.path.exists(filename) or os.path.islink(filename) ) list(map(self._delete_path, extant_blockers)) @@ -233,7 +224,7 @@ class easy_install(Command): return is_tree = os.path.isdir(path) and not os.path.islink(path) - remover = rmtree if is_tree else os.unlink + remover = _rmtree if is_tree else os.unlink remover(path) @staticmethod @@ -247,30 +238,44 @@ class easy_install(Command): print(tmpl.format(**locals())) raise SystemExit() - def finalize_options(self): + def finalize_options(self): # noqa: C901 # is too complex (25) # FIXME self.version and self._render_version() py_version = sys.version.split()[0] - prefix, exec_prefix = get_config_vars('prefix', 'exec_prefix') - self.config_vars = { + self.config_vars = dict(sysconfig.get_config_vars()) + + self.config_vars.update({ 'dist_name': self.distribution.get_name(), 'dist_version': self.distribution.get_version(), 'dist_fullname': self.distribution.get_fullname(), 'py_version': py_version, - 'py_version_short': py_version[0:3], - 'py_version_nodot': py_version[0] + py_version[2], - 'sys_prefix': prefix, - 'prefix': prefix, - 'sys_exec_prefix': exec_prefix, - 'exec_prefix': exec_prefix, - # Only python 3.2+ has abiflags + 'py_version_short': f'{sys.version_info.major}.{sys.version_info.minor}', + 'py_version_nodot': f'{sys.version_info.major}{sys.version_info.minor}', + 'sys_prefix': self.config_vars['prefix'], + 'sys_exec_prefix': self.config_vars['exec_prefix'], + # Only POSIX systems have abiflags 'abiflags': getattr(sys, 'abiflags', ''), - } + # Only python 3.9+ has platlibdir + 'platlibdir': getattr(sys, 'platlibdir', 'lib'), + }) + with contextlib.suppress(AttributeError): + # only for distutils outside stdlib + self.config_vars.update({ + 'implementation_lower': install._get_implementation().lower(), + 'implementation': install._get_implementation(), + }) - if site.ENABLE_USER_SITE: - self.config_vars['userbase'] = self.install_userbase - self.config_vars['usersite'] = self.install_usersite + # pypa/distutils#113 Python 3.9 compat + self.config_vars.setdefault( + 'py_version_nodot_plat', + getattr(sys, 'windir', '').replace('.', ''), + ) + + self.config_vars['userbase'] = self.install_userbase + self.config_vars['usersite'] = self.install_usersite + if self.user and not site.ENABLE_USER_SITE: + log.warn("WARNING: The user site-packages directory is disabled.") self._fix_install_dir_for_user_site() @@ -278,7 +283,9 @@ class easy_install(Command): self.expand_dirs() self._expand( - 'install_dir', 'script_dir', 'build_directory', + 'install_dir', + 'script_dir', + 'build_directory', 'site_dirs', ) # If a non-default installation directory was specified, default the @@ -292,40 +299,23 @@ class easy_install(Command): # Let install_dir get set by install_lib command, which in turn # gets its info from the install command, and takes into account # --prefix and --home and all that other crud. - self.set_undefined_options( - 'install_lib', ('install_dir', 'install_dir') - ) + self.set_undefined_options('install_lib', ('install_dir', 'install_dir')) # Likewise, set default script_dir from 'install_scripts.install_dir' - self.set_undefined_options( - 'install_scripts', ('install_dir', 'script_dir') - ) + self.set_undefined_options('install_scripts', ('install_dir', 'script_dir')) if self.user and self.install_purelib: self.install_dir = self.install_purelib self.script_dir = self.install_scripts # default --record from the install command self.set_undefined_options('install', ('record', 'record')) - # Should this be moved to the if statement below? It's not used - # elsewhere - normpath = map(normalize_path, sys.path) self.all_site_dirs = get_site_dirs() - if self.site_dirs is not None: - site_dirs = [ - os.path.expanduser(s.strip()) for s in - self.site_dirs.split(',') - ] - for d in site_dirs: - if not os.path.isdir(d): - log.warn("%s (in --site-dirs) does not exist", d) - elif normalize_path(d) not in normpath: - raise DistutilsOptionError( - d + " (in --site-dirs) is not on sys.path" - ) - else: - self.all_site_dirs.append(normalize_path(d)) + self.all_site_dirs.extend(self._process_site_dirs(self.site_dirs)) + if not self.editable: self.check_site_dir() - self.index_url = self.index_url or "https://pypi.org/simple/" + default_index = os.getenv("__EASYINSTALL_INDEX", "https://pypi.org/simple/") + # ^ Private API for testing purposes only + self.index_url = self.index_url or default_index self.shadow_path = self.all_site_dirs[:] for path_item in self.install_dir, normalize_path(self.script_dir): if path_item not in self.shadow_path: @@ -337,12 +327,14 @@ class easy_install(Command): hosts = ['*'] if self.package_index is None: self.package_index = self.create_index( - self.index_url, search_path=self.shadow_path, hosts=hosts, + self.index_url, + search_path=self.shadow_path, + hosts=hosts, ) self.local_index = Environment(self.shadow_path + sys.path) if self.find_links is not None: - if isinstance(self.find_links, six.string_types): + if isinstance(self.find_links, str): self.find_links = self.find_links.split() else: self.find_links = [] @@ -351,13 +343,7 @@ class easy_install(Command): if not self.no_find_links: self.package_index.add_find_links(self.find_links) self.set_undefined_options('install_lib', ('optimize', 'optimize')) - if not isinstance(self.optimize, int): - try: - self.optimize = int(self.optimize) - if not (0 <= self.optimize <= 2): - raise ValueError - except ValueError: - raise DistutilsOptionError("--optimize must be 0, 1, or 2") + self.optimize = self._validate_optimize(self.optimize) if self.editable and not self.build_directory: raise DistutilsArgError( @@ -365,15 +351,42 @@ class easy_install(Command): ) if not self.args: raise DistutilsArgError( - "No urls, filenames, or requirements specified (see --help)") + "No urls, filenames, or requirements specified (see --help)" + ) self.outputs = [] + @staticmethod + def _process_site_dirs(site_dirs): + if site_dirs is None: + return + + normpath = map(normalize_path, sys.path) + site_dirs = [os.path.expanduser(s.strip()) for s in site_dirs.split(',')] + for d in site_dirs: + if not os.path.isdir(d): + log.warn("%s (in --site-dirs) does not exist", d) + elif normalize_path(d) not in normpath: + raise DistutilsOptionError(d + " (in --site-dirs) is not on sys.path") + else: + yield normalize_path(d) + + @staticmethod + def _validate_optimize(value): + try: + value = int(value) + if value not in range(3): + raise ValueError + except ValueError as e: + raise DistutilsOptionError("--optimize must be 0, 1, or 2") from e + + return value + def _fix_install_dir_for_user_site(self): """ Fix the install_dir if "--user" was used. """ - if not self.user or not site.ENABLE_USER_SITE: + if not self.user: return self.create_home_path() @@ -381,7 +394,7 @@ class easy_install(Command): msg = "User base directory is not specified" raise DistutilsPlatformError(msg) self.install_base = self.install_platbase = self.install_userbase - scheme_name = os.name.replace('posix', 'unix') + '_user' + scheme_name = f'{os.name}_user' self.select_scheme(scheme_name) def _expand_attrs(self, attrs): @@ -414,8 +427,8 @@ class easy_install(Command): if show_deprecation: self.announce( "WARNING: The easy_install command is deprecated " - "and will be removed in a future version." - , log.WARN, + "and will be removed in a future version.", + log.WARN, ) if self.verbose != self.distribution.verbose: log.set_verbosity(self.verbose) @@ -431,9 +444,9 @@ class easy_install(Command): from distutils import file_util self.execute( - file_util.write_file, (self.record, outputs), - "writing list of installed files to '%s'" % - self.record + file_util.write_file, + (self.record, outputs), + "writing list of installed files to '%s'" % self.record, ) self.warn_deprecated_options() finally: @@ -453,12 +466,18 @@ class easy_install(Command): def warn_deprecated_options(self): pass - def check_site_dir(self): + def check_site_dir(self): # noqa: C901 # is too complex (12) # FIXME """Verify that self.install_dir is .pth-capable dir, if needed""" instdir = normalize_path(self.install_dir) pth_file = os.path.join(instdir, 'easy-install.pth') + if not os.path.exists(instdir): + try: + os.makedirs(instdir) + except OSError: + self.cant_write_to_target() + # Is it a configured, PYTHONPATH, implicit, or explicit site dir? is_site_dir = instdir in self.all_site_dirs @@ -474,12 +493,13 @@ class easy_install(Command): os.unlink(testfile) open(testfile, 'w').close() os.unlink(testfile) - except (OSError, IOError): + except OSError: self.cant_write_to_target() if not is_site_dir and not self.multi_version: - # Can't install non-multi to non-site dir - raise DistutilsError(self.no_default_version_msg()) + # Can't install non-multi to non-site dir with easy_install + pythonpath = os.environ.get('PYTHONPATH', '') + log.warn(self.__no_default_msg, self.install_dir, pythonpath) if is_site_dir: if self.pth_file is None: @@ -487,15 +507,12 @@ class easy_install(Command): else: self.pth_file = None - if instdir not in map(normalize_path, _pythonpath()): - # only PYTHONPATH dirs need a site.py, so pretend it's there - self.sitepy_installed = True - elif self.multi_version and not os.path.exists(pth_file): - self.sitepy_installed = True # don't need site.py in this case - self.pth_file = None # and don't create a .pth file + if self.multi_version and not os.path.exists(pth_file): + self.pth_file = None # don't create a .pth file self.install_dir = instdir - __cant_write_msg = textwrap.dedent(""" + __cant_write_msg = textwrap.dedent( + """ can't create or remove files in install directory The following error occurred while trying to add or remove files in the @@ -507,15 +524,19 @@ class easy_install(Command): the distutils default setting) was: %s - """).lstrip() + """ + ).lstrip() # noqa - __not_exists_id = textwrap.dedent(""" + __not_exists_id = textwrap.dedent( + """ This directory does not currently exist. Please create it and try again, or choose a different installation directory (using the -d or --install-dir option). - """).lstrip() + """ + ).lstrip() # noqa - __access_msg = textwrap.dedent(""" + __access_msg = textwrap.dedent( + """ Perhaps your account does not have write access to this directory? If the installation directory is a system-owned directory, you may need to sign in as the administrator or "root" account. If you do not have administrative @@ -526,13 +547,17 @@ class easy_install(Command): For information on other options, you may wish to consult the documentation at: - https://setuptools.readthedocs.io/en/latest/easy_install.html + https://setuptools.pypa.io/en/latest/deprecated/easy_install.html Please make the appropriate changes for your system and try again. - """).lstrip() + """ + ).lstrip() # noqa def cant_write_to_target(self): - msg = self.__cant_write_msg % (sys.exc_info()[1], self.install_dir,) + msg = self.__cant_write_msg % ( + sys.exc_info()[1], + self.install_dir, + ) if not os.path.exists(self.install_dir): msg += '\n' + self.__not_exists_id @@ -547,19 +572,24 @@ class easy_install(Command): pth_file = self.pseudo_tempname() + ".pth" ok_file = pth_file + '.ok' ok_exists = os.path.exists(ok_file) - tmpl = _one_liner(""" + tmpl = ( + _one_liner( + """ import os f = open({ok_file!r}, 'w') f.write('OK') f.close() - """) + '\n' + """ + ) + + '\n' + ) try: if ok_exists: os.unlink(ok_file) dirname = os.path.dirname(ok_file) - pkg_resources.py31compat.makedirs(dirname, exist_ok=True) + os.makedirs(dirname, exist_ok=True) f = open(pth_file, 'w') - except (OSError, IOError): + except OSError: self.cant_write_to_target() else: try: @@ -570,10 +600,7 @@ class easy_install(Command): if os.name == 'nt': dirname, basename = os.path.split(executable) alt = os.path.join(dirname, 'pythonw.exe') - use_alt = ( - basename.lower() == 'python.exe' and - os.path.exists(alt) - ) + use_alt = basename.lower() == 'python.exe' and os.path.exists(alt) if use_alt: # use pythonw.exe to avoid opening a console window executable = alt @@ -583,10 +610,7 @@ class easy_install(Command): spawn([executable, '-E', '-c', 'pass'], 0) if os.path.exists(ok_file): - log.info( - "TEST PASSED: %s appears to support .pth files", - instdir - ) + log.info("TEST PASSED: %s appears to support .pth files", instdir) return True finally: if f: @@ -608,8 +632,7 @@ class easy_install(Command): # __pycache__ directory, so skip it. continue self.install_script( - dist, script_name, - dist.get_metadata('scripts/' + script_name) + dist, script_name, dist.get_metadata('scripts/' + script_name) ) self.install_wrapper_scripts(dist) @@ -625,8 +648,7 @@ class easy_install(Command): if self.editable: raise DistutilsArgError( "Invalid argument %r: you can't use filenames or URLs " - "with --editable (except via the --find-links option)." - % (spec,) + "with --editable (except via the --find-links option)." % (spec,) ) def check_editable(self, spec): @@ -635,23 +657,20 @@ class easy_install(Command): if os.path.exists(os.path.join(self.build_directory, spec.key)): raise DistutilsArgError( - "%r already exists in %s; can't do a checkout there" % - (spec.key, self.build_directory) + "%r already exists in %s; can't do a checkout there" + % (spec.key, self.build_directory) ) @contextlib.contextmanager def _tmpdir(self): - tmpdir = tempfile.mkdtemp(prefix=u"easy_install-") + tmpdir = tempfile.mkdtemp(prefix="easy_install-") try: # cast to str as workaround for #709 and #710 and #712 yield str(tmpdir) finally: - os.path.exists(tmpdir) and rmtree(rmtree_safe(tmpdir)) + os.path.exists(tmpdir) and _rmtree(tmpdir) def easy_install(self, spec, deps=False): - if not self.editable: - self.install_site_py() - with self._tmpdir() as tmpdir: if not isinstance(spec, Requirement): if URL_SCHEME(spec): @@ -669,8 +688,12 @@ class easy_install(Command): self.check_editable(spec) dist = self.package_index.fetch_distribution( - spec, tmpdir, self.upgrade, self.editable, - not self.always_copy, self.local_index + spec, + tmpdir, + self.upgrade, + self.editable, + not self.always_copy, + self.local_index, ) if dist is None: msg = "Could not find suitable distribution for %r" % spec @@ -685,15 +708,14 @@ class easy_install(Command): return self.install_item(spec, dist.location, tmpdir, deps) def install_item(self, spec, download, tmpdir, deps, install_needed=False): - # Installation is also needed if file in tmpdir or is not an egg install_needed = install_needed or self.always_copy install_needed = install_needed or os.path.dirname(download) == tmpdir install_needed = install_needed or not download.endswith('.egg') install_needed = install_needed or ( - self.always_copy_from is not None and - os.path.dirname(normalize_path(download)) == - normalize_path(self.always_copy_from) + self.always_copy_from is not None + and os.path.dirname(normalize_path(download)) + == normalize_path(self.always_copy_from) ) if spec and not install_needed: @@ -719,17 +741,23 @@ class easy_install(Command): for dist in dists: if dist in spec: return dist + return None def select_scheme(self, name): - """Sets the install directories by applying the install schemes.""" - # it's the caller's problem if they supply a bad name! - scheme = INSTALL_SCHEMES[name] - for key in SCHEME_KEYS: - attrname = 'install_' + key - if getattr(self, attrname) is None: - setattr(self, attrname, scheme[key]) + try: + install._select_scheme(self, name) + except AttributeError: + # stdlib distutils + install.install.select_scheme(self, name.replace('posix', 'unix')) - def process_distribution(self, requirement, dist, deps=True, *info): + # FIXME: 'easy_install.process_distribution' is too complex (12) + def process_distribution( # noqa: C901 + self, + requirement, + dist, + deps=True, + *info, + ): self.update_pth(dist) self.package_index.add(dist) if dist in self.local_index[dist.key]: @@ -738,8 +766,7 @@ class easy_install(Command): self.install_egg_scripts(dist) self.installed_projects[dist.key] = dist log.info(self.installation_report(requirement, dist, *info)) - if (dist.has_metadata('dependency_links.txt') and - not self.no_find_links): + if dist.has_metadata('dependency_links.txt') and not self.no_find_links: self.package_index.add_find_links( dist.get_metadata_lines('dependency_links.txt') ) @@ -758,9 +785,9 @@ class easy_install(Command): [requirement], self.local_index, self.easy_install ) except DistributionNotFound as e: - raise DistutilsError(str(e)) + raise DistutilsError(str(e)) from e except VersionConflict as e: - raise DistutilsError(e.report()) + raise DistutilsError(e.report()) from e if self.always_copy or self.always_copy_from: # Force all the relevant distros to be copied or activated for dist in distros: @@ -780,9 +807,7 @@ class easy_install(Command): def maybe_move(self, spec, dist_filename, setup_base): dst = os.path.join(self.build_directory, spec.key) if os.path.exists(dst): - msg = ( - "%r already exists in %s; build directory %s will not be kept" - ) + msg = "%r already exists in %s; build directory %s will not be kept" log.warn(msg, spec.key, self.build_directory, setup_base) return setup_base if os.path.isdir(dist_filename): @@ -853,12 +878,17 @@ class easy_install(Command): def install_eggs(self, spec, dist_filename, tmpdir): # .egg dirs or files are already built, so just return them - if dist_filename.lower().endswith('.egg'): - return [self.install_egg(dist_filename, tmpdir)] - elif dist_filename.lower().endswith('.exe'): - return [self.install_exe(dist_filename, tmpdir)] - elif dist_filename.lower().endswith('.whl'): - return [self.install_wheel(dist_filename, tmpdir)] + installer_map = { + '.egg': self.install_egg, + '.exe': self.install_exe, + '.whl': self.install_wheel, + } + try: + install_dist = installer_map[dist_filename.lower()[-4:]] + except KeyError: + pass + else: + return [install_dist(dist_filename, tmpdir)] # Anything else, try to extract and build setup_base = tmpdir @@ -867,8 +897,11 @@ class easy_install(Command): elif os.path.isdir(dist_filename): setup_base = os.path.abspath(dist_filename) - if (setup_base.startswith(tmpdir) # something we downloaded - and self.build_directory and spec is not None): + if ( + setup_base.startswith(tmpdir) # something we downloaded + and self.build_directory + and spec is not None + ): setup_base = self.maybe_move(spec, dist_filename, setup_base) # Find the setup.py file @@ -878,13 +911,12 @@ class easy_install(Command): setups = glob(os.path.join(setup_base, '*', 'setup.py')) if not setups: raise DistutilsError( - "Couldn't find a setup script in %s" % - os.path.abspath(dist_filename) + "Couldn't find a setup script in %s" + % os.path.abspath(dist_filename) ) if len(setups) > 1: raise DistutilsError( - "Multiple setup scripts in %s" % - os.path.abspath(dist_filename) + "Multiple setup scripts in %s" % os.path.abspath(dist_filename) ) setup_script = setups[0] @@ -897,13 +929,13 @@ class easy_install(Command): def egg_distribution(self, egg_path): if os.path.isdir(egg_path): - metadata = PathMetadata(egg_path, os.path.join(egg_path, - 'EGG-INFO')) + metadata = PathMetadata(egg_path, os.path.join(egg_path, 'EGG-INFO')) else: metadata = EggMetadata(zipimport.zipimporter(egg_path)) return Distribution.from_filename(egg_path, metadata=metadata) - def install_egg(self, egg_path, tmpdir): + # FIXME: 'easy_install.install_egg' is too complex (11) + def install_egg(self, egg_path, tmpdir): # noqa: C901 destination = os.path.join( self.install_dir, os.path.basename(egg_path), @@ -913,7 +945,9 @@ class easy_install(Command): ensure_directory(destination) dist = self.egg_distribution(egg_path) - if not samefile(egg_path, destination): + if not ( + os.path.exists(destination) and os.path.samefile(egg_path, destination) + ): if os.path.isdir(destination) and not os.path.islink(destination): dir_util.remove_tree(destination, dry_run=self.dry_run) elif os.path.exists(destination): @@ -941,10 +975,8 @@ class easy_install(Command): self.execute( f, (egg_path, destination), - (m + " %s to %s") % ( - os.path.basename(egg_path), - os.path.dirname(destination) - ), + (m + " %s to %s") + % (os.path.basename(egg_path), os.path.dirname(destination)), ) update_dist_caches( destination, @@ -968,7 +1000,8 @@ class easy_install(Command): dist = Distribution( None, project_name=cfg.get('metadata', 'name'), - version=cfg.get('metadata', 'version'), platform=get_platform(), + version=cfg.get('metadata', 'version'), + platform=get_platform(), ) # Convert the .exe to an unpacked egg @@ -992,17 +1025,20 @@ class easy_install(Command): script_dir = os.path.join(_egg_info, 'scripts') # delete entry-point scripts to avoid duping self.delete_blockers([ - os.path.join(script_dir, args[0]) - for args in ScriptWriter.get_args(dist) + os.path.join(script_dir, args[0]) for args in ScriptWriter.get_args(dist) ]) # Build .egg file from tmpdir bdist_egg.make_zipfile( - egg_path, egg_tmp, verbose=self.verbose, dry_run=self.dry_run, + egg_path, + egg_tmp, + verbose=self.verbose, + dry_run=self.dry_run, ) # install the .egg return self.install_egg(egg_path, tmpdir) - def exe_to_egg(self, dist_filename, egg_tmp): + # FIXME: 'easy_install.exe_to_egg' is too complex (12) + def exe_to_egg(self, dist_filename, egg_tmp): # noqa: C901 """Extract a bdist_wininst to the directories an egg would use""" # Check for .pth file and set up prefix translations prefixes = get_exe_prefixes(dist_filename) @@ -1014,7 +1050,7 @@ class easy_install(Command): s = src.lower() for old, new in prefixes: if s.startswith(old): - src = new + src[len(old):] + src = new + src[len(old) :] parts = src.split('/') dst = os.path.join(egg_tmp, *parts) dl = dst.lower() @@ -1044,8 +1080,8 @@ class easy_install(Command): bdist_egg.write_stub(resource, pyfile) self.byte_compile(to_compile) # compile .py's bdist_egg.write_safety_flag( - os.path.join(egg_tmp, 'EGG-INFO'), - bdist_egg.analyze_egg(egg_tmp, stubs)) # write zip-safety flag + os.path.join(egg_tmp, 'EGG-INFO'), bdist_egg.analyze_egg(egg_tmp, stubs) + ) # write zip-safety flag for name in 'top_level', 'native_libs': if locals()[name]: @@ -1074,17 +1110,16 @@ class easy_install(Command): self.execute( wheel.install_as_egg, (destination,), - ("Installing %s to %s") % ( - os.path.basename(wheel_path), - os.path.dirname(destination) - ), + ("Installing %s to %s") + % (os.path.basename(wheel_path), os.path.dirname(destination)), ) finally: update_dist_caches(destination, fix_zipimporter_caches=False) self.add_output(destination) return self.egg_distribution(destination) - __mv_warning = textwrap.dedent(""" + __mv_warning = textwrap.dedent( + """ Because this distribution was installed --multi-version, before you can import modules from this package in an application, you will need to 'import pkg_resources' and then use a 'require()' call similar to one of @@ -1093,13 +1128,16 @@ class easy_install(Command): pkg_resources.require("%(name)s") # latest installed version pkg_resources.require("%(name)s==%(version)s") # this exact version pkg_resources.require("%(name)s>=%(version)s") # this version or higher - """).lstrip() + """ + ).lstrip() # noqa - __id_warning = textwrap.dedent(""" + __id_warning = textwrap.dedent( + """ Note also that the installation directory must be on sys.path at runtime for this to work. (e.g. by being the application's script directory, by being on PYTHONPATH, or by being added to sys.path by your code.) - """) + """ + ) # noqa def installation_report(self, req, dist, what="Installed"): """Helpful installation message for display to package users""" @@ -1115,7 +1153,8 @@ class easy_install(Command): extras = '' # TODO: self.report_extras(req, dist) return msg % locals() - __editable_msg = textwrap.dedent(""" + __editable_msg = textwrap.dedent( + """ Extracted editable version of %(spec)s to %(dirname)s If it uses setuptools in its setup script, you can activate it in @@ -1124,7 +1163,8 @@ class easy_install(Command): %(python)s setup.py develop See the setuptools documentation for the "develop" command for more info. - """).lstrip() + """ + ).lstrip() # noqa def report_editable(self, spec, setup_script): dirname = os.path.dirname(setup_script) @@ -1143,13 +1183,11 @@ class easy_install(Command): args.insert(0, '-q') if self.dry_run: args.insert(0, '-n') - log.info( - "Running %s %s", setup_script[len(setup_base) + 1:], ' '.join(args) - ) + log.info("Running %s %s", setup_script[len(setup_base) + 1 :], ' '.join(args)) try: run_setup(setup_script, args) except SystemExit as v: - raise DistutilsError("Setup script exited with %s" % (v.args[0],)) + raise DistutilsError("Setup script exited with %s" % (v.args[0],)) from v def build_and_install(self, setup_script, setup_base): args = ['bdist_egg', '--dist-dir'] @@ -1168,11 +1206,10 @@ class easy_install(Command): for dist in all_eggs[key]: eggs.append(self.install_egg(dist.location, setup_base)) if not eggs and not self.dry_run: - log.warn("No eggs found in %s (setup script problem?)", - dist_dir) + log.warn("No eggs found in %s (setup script problem?)", dist_dir) return eggs finally: - rmtree(dist_dir) + _rmtree(dist_dir) log.set_verbosity(self.verbose) # restore our log verbosity def _set_fetcher_options(self, base): @@ -1186,28 +1223,34 @@ class easy_install(Command): # to the setup.cfg file. ei_opts = self.distribution.get_option_dict('easy_install').copy() fetch_directives = ( - 'find_links', 'site_dirs', 'index_url', 'optimize', 'allow_hosts', + 'find_links', + 'site_dirs', + 'index_url', + 'optimize', + 'allow_hosts', ) fetch_options = {} for key, val in ei_opts.items(): if key not in fetch_directives: continue - fetch_options[key.replace('_', '-')] = val[1] + fetch_options[key] = val[1] # create a settings dictionary suitable for `edit_config` settings = dict(easy_install=fetch_options) cfg_filename = os.path.join(base, 'setup.cfg') setopt.edit_config(cfg_filename, settings) - def update_pth(self, dist): + def update_pth(self, dist): # noqa: C901 # is too complex (11) # FIXME if self.pth_file is None: return for d in self.pth_file[dist.key]: # drop old entries - if self.multi_version or d.location != dist.location: - log.info("Removing %s from easy-install.pth file", d) - self.pth_file.remove(d) - if d.location in self.shadow_path: - self.shadow_path.remove(d.location) + if not self.multi_version and d.location == dist.location: + continue + + log.info("Removing %s from easy-install.pth file", d) + self.pth_file.remove(d) + if d.location in self.shadow_path: + self.shadow_path.remove(d.location) if not self.multi_version: if dist.location in self.pth_file.paths: @@ -1221,19 +1264,21 @@ class easy_install(Command): if dist.location not in self.shadow_path: self.shadow_path.append(dist.location) - if not self.dry_run: + if self.dry_run: + return - self.pth_file.save() + self.pth_file.save() - if dist.key == 'setuptools': - # Ensure that setuptools itself never becomes unavailable! - # XXX should this check for latest version? - filename = os.path.join(self.install_dir, 'setuptools.pth') - if os.path.islink(filename): - os.unlink(filename) - f = open(filename, 'wt') - f.write(self.pth_file.make_relative(dist.location) + '\n') - f.close() + if dist.key != 'setuptools': + return + + # Ensure that setuptools itself never becomes unavailable! + # XXX should this check for latest version? + filename = os.path.join(self.install_dir, 'setuptools.pth') + if os.path.islink(filename): + os.unlink(filename) + with open(filename, 'wt') as f: + f.write(self.pth_file.make_relative(dist.location) + '\n') def unpack_progress(self, src, dst): # Progress filter for unpacking @@ -1272,13 +1317,16 @@ class easy_install(Command): byte_compile(to_compile, optimize=0, force=1, dry_run=self.dry_run) if self.optimize: byte_compile( - to_compile, optimize=self.optimize, force=1, + to_compile, + optimize=self.optimize, + force=1, dry_run=self.dry_run, ) finally: log.set_verbosity(self.verbose) # restore original verbosity - __no_default_msg = textwrap.dedent(""" + __no_default_msg = textwrap.dedent( + """ bad install directory or PYTHONPATH You are attempting to install a package to a directory that is not @@ -1304,53 +1352,19 @@ class easy_install(Command): * You can set up the installation directory to support ".pth" files by using one of the approaches described here: - https://setuptools.readthedocs.io/en/latest/easy_install.html#custom-installation-locations + https://setuptools.pypa.io/en/latest/deprecated/easy_install.html#custom-installation-locations - Please make the appropriate changes for your system and try again.""").lstrip() - - def no_default_version_msg(self): - template = self.__no_default_msg - return template % (self.install_dir, os.environ.get('PYTHONPATH', '')) - - def install_site_py(self): - """Make sure there's a site.py in the target dir, if needed""" - - if self.sitepy_installed: - return # already did it, or don't need to - - sitepy = os.path.join(self.install_dir, "site.py") - source = resource_string("setuptools", "site-patch.py") - source = source.decode('utf-8') - current = "" - - if os.path.exists(sitepy): - log.debug("Checking existing site.py in %s", self.install_dir) - with io.open(sitepy) as strm: - current = strm.read() - - if not current.startswith('def __boot():'): - raise DistutilsError( - "%s is not a setuptools-generated site.py; please" - " remove it." % sitepy - ) - - if current != source: - log.info("Creating %s", sitepy) - if not self.dry_run: - ensure_directory(sitepy) - with io.open(sitepy, 'w', encoding='utf-8') as strm: - strm.write(source) - self.byte_compile([sitepy]) - - self.sitepy_installed = True + Please make the appropriate changes for your system and try again. + """ + ).strip() def create_home_path(self): """Create directories under ~.""" if not self.user: return home = convert_path(os.path.expanduser("~")) - for name, path in six.iteritems(self.config_vars): + for path in only_strs(self.config_vars.values()): if path.startswith(home) and not os.path.isdir(path): self.debug_print("os.makedirs('%s', 0o700)" % path) os.makedirs(path, 0o700) @@ -1372,7 +1386,7 @@ class easy_install(Command): if self.prefix: # Set default install_dir/scripts from --prefix - config_vars = config_vars.copy() + config_vars = dict(config_vars) config_vars['base'] = self.prefix scheme = self.INSTALL_SCHEMES.get(os.name, self.DEFAULT_SCHEME) for attr, val in scheme.items(): @@ -1409,58 +1423,61 @@ def get_site_dirs(): if sys.exec_prefix != sys.prefix: prefixes.append(sys.exec_prefix) for prefix in prefixes: - if prefix: - if sys.platform in ('os2emx', 'riscos'): - sitedirs.append(os.path.join(prefix, "Lib", "site-packages")) - elif os.sep == '/': - sitedirs.extend([ - os.path.join( - prefix, - "lib", - "python{}.{}".format(*sys.version_info), - "site-packages", - ), - os.path.join(prefix, "lib", "site-python"), - ]) - else: - sitedirs.extend([ + if not prefix: + continue + + if sys.platform in ('os2emx', 'riscos'): + sitedirs.append(os.path.join(prefix, "Lib", "site-packages")) + elif os.sep == '/': + sitedirs.extend([ + os.path.join( prefix, - os.path.join(prefix, "lib", "site-packages"), - ]) - if sys.platform == 'darwin': - # for framework builds *only* we add the standard Apple - # locations. Currently only per-user, but /Library and - # /Network/Library could be added too - if 'Python.framework' in prefix: - home = os.environ.get('HOME') - if home: - home_sp = os.path.join( - home, - 'Library', - 'Python', - '{}.{}'.format(*sys.version_info), - 'site-packages', - ) - sitedirs.append(home_sp) + "lib", + "python{}.{}".format(*sys.version_info), + "site-packages", + ), + os.path.join(prefix, "lib", "site-python"), + ]) + else: + sitedirs.extend([ + prefix, + os.path.join(prefix, "lib", "site-packages"), + ]) + if sys.platform != 'darwin': + continue + + # for framework builds *only* we add the standard Apple + # locations. Currently only per-user, but /Library and + # /Network/Library could be added too + if 'Python.framework' not in prefix: + continue + + home = os.environ.get('HOME') + if not home: + continue + + home_sp = os.path.join( + home, + 'Library', + 'Python', + '{}.{}'.format(*sys.version_info), + 'site-packages', + ) + sitedirs.append(home_sp) lib_paths = get_path('purelib'), get_path('platlib') - for site_lib in lib_paths: - if site_lib not in sitedirs: - sitedirs.append(site_lib) + + sitedirs.extend(s for s in lib_paths if s not in sitedirs) if site.ENABLE_USER_SITE: sitedirs.append(site.USER_SITE) - try: + with contextlib.suppress(AttributeError): sitedirs.extend(site.getsitepackages()) - except AttributeError: - pass - sitedirs = list(map(normalize_path, sitedirs)) - - return sitedirs + return list(map(normalize_path, sitedirs)) -def expand_paths(inputs): +def expand_paths(inputs): # noqa: C901 # is too complex (11) # FIXME """Yield sys.path directories that might contain "old-style" packages""" seen = {} @@ -1492,13 +1509,18 @@ def expand_paths(inputs): # Yield existing non-dupe, non-import directory lines from it for line in lines: - if not line.startswith("import"): - line = normalize_path(line.rstrip()) - if line not in seen: - seen[line] = 1 - if not os.path.isdir(line): - continue - yield line, os.listdir(line) + if line.startswith("import"): + continue + + line = normalize_path(line.rstrip()) + if line in seen: + continue + + seen[line] = 1 + if not os.path.isdir(line): + continue + + yield line, os.listdir(line) def extract_wininst_cfg(dist_filename): @@ -1531,7 +1553,7 @@ def extract_wininst_cfg(dist_filename): # Now the config is in bytes, but for RawConfigParser, it should # be text, so decode it. config = config.decode(sys.getfilesystemencoding()) - cfg.readfp(six.StringIO(config)) + cfg.read_file(io.StringIO(config)) except configparser.Error: return None if not cfg.has_section('metadata') or not cfg.has_section('Setup'): @@ -1566,9 +1588,7 @@ def get_exe_prefixes(exe_filename): if name.endswith('-nspkg.pth'): continue if parts[0].upper() in ('PURELIB', 'PLATLIB'): - contents = z.read(name) - if not six.PY2: - contents = contents.decode() + contents = z.read(name).decode() for pth in yield_lines(contents): pth = pth.strip().replace('\\', '/') if not pth.startswith('import'): @@ -1584,50 +1604,82 @@ def get_exe_prefixes(exe_filename): class PthDistributions(Environment): """A .pth file with Distribution paths in it""" - dirty = False - def __init__(self, filename, sitedirs=()): self.filename = filename self.sitedirs = list(map(normalize_path, sitedirs)) self.basedir = normalize_path(os.path.dirname(self.filename)) - self._load() - Environment.__init__(self, [], None, None) + self.paths, self.dirty = self._load() + # keep a copy if someone manually updates the paths attribute on the instance + self._init_paths = self.paths[:] + super().__init__([], None, None) for path in yield_lines(self.paths): list(map(self.add, find_distributions(path, True))) - def _load(self): - self.paths = [] - saw_import = False + def _load_raw(self): + paths = [] + dirty = saw_import = False seen = dict.fromkeys(self.sitedirs) - if os.path.isfile(self.filename): - f = open(self.filename, 'rt') - for line in f: - if line.startswith('import'): - saw_import = True - continue - path = line.rstrip() - self.paths.append(path) - if not path.strip() or path.strip().startswith('#'): - continue - # skip non-existent paths, in case somebody deleted a package - # manually, and duplicate paths as well - path = self.paths[-1] = normalize_path( - os.path.join(self.basedir, path) - ) - if not os.path.exists(path) or path in seen: - self.paths.pop() # skip it - self.dirty = True # we cleaned up, so we're dirty now :) - continue - seen[path] = 1 - f.close() + f = open(self.filename, 'rt') + for line in f: + path = line.rstrip() + # still keep imports and empty/commented lines for formatting + paths.append(path) + if line.startswith(('import ', 'from ')): + saw_import = True + continue + stripped_path = path.strip() + if not stripped_path or stripped_path.startswith('#'): + continue + # skip non-existent paths, in case somebody deleted a package + # manually, and duplicate paths as well + normalized_path = normalize_path(os.path.join(self.basedir, path)) + if normalized_path in seen or not os.path.exists(normalized_path): + log.debug("cleaned up dirty or duplicated %r", path) + dirty = True + paths.pop() + continue + seen[normalized_path] = 1 + f.close() + # remove any trailing empty/blank line + while paths and not paths[-1].strip(): + paths.pop() + dirty = True + return paths, dirty or (paths and saw_import) - if self.paths and not saw_import: - self.dirty = True # ensure anything we touch has import wrappers - while self.paths and not self.paths[-1].strip(): - self.paths.pop() + def _load(self): + if os.path.isfile(self.filename): + return self._load_raw() + return [], False def save(self): """Write changed .pth file back to disk""" + # first reload the file + last_paths, last_dirty = self._load() + # and check that there are no difference with what we have. + # there can be difference if someone else has written to the file + # since we first loaded it. + # we don't want to lose the eventual new paths added since then. + for path in last_paths[:]: + if path not in self.paths: + self.paths.append(path) + log.info("detected new path %r", path) + last_dirty = True + else: + last_paths.remove(path) + # also, re-check that all paths are still valid before saving them + for path in self.paths[:]: + if path not in last_paths and not path.startswith(( + 'import ', + 'from ', + '#', + )): + absolute_path = os.path.join(self.basedir, path) + if not os.path.exists(absolute_path): + self.paths.remove(path) + log.info("removing now non-existent path %r", path) + last_dirty = True + + self.dirty |= last_dirty or self.paths != self._init_paths if not self.dirty: return @@ -1636,17 +1688,16 @@ class PthDistributions(Environment): log.debug("Saving %s", self.filename) lines = self._wrap_lines(rel_paths) data = '\n'.join(lines) + '\n' - if os.path.islink(self.filename): os.unlink(self.filename) with open(self.filename, 'wt') as f: f.write(data) - elif os.path.exists(self.filename): log.debug("Deleting empty %s", self.filename) os.unlink(self.filename) self.dirty = False + self._init_paths[:] = self.paths[:] @staticmethod def _wrap_lines(lines): @@ -1654,24 +1705,23 @@ class PthDistributions(Environment): def add(self, dist): """Add `dist` to the distribution map""" - new_path = ( - dist.location not in self.paths and ( - dist.location not in self.sitedirs or - # account for '.' being in PYTHONPATH - dist.location == os.getcwd() - ) + new_path = dist.location not in self.paths and ( + dist.location not in self.sitedirs + or + # account for '.' being in PYTHONPATH + dist.location == os.getcwd() ) if new_path: self.paths.append(dist.location) self.dirty = True - Environment.add(self, dist) + super().add(dist) def remove(self, dist): """Remove `dist` from the distribution map""" while dist.location in self.paths: self.paths.remove(dist.location) self.dirty = True - Environment.remove(self, dist) + super().remove(dist) def make_relative(self, path): npath, last = os.path.split(normalize_path(path)) @@ -1693,22 +1743,25 @@ class RewritePthDistributions(PthDistributions): @classmethod def _wrap_lines(cls, lines): yield cls.prelude - for line in lines: - yield line + yield from lines yield cls.postlude - prelude = _one_liner(""" + prelude = _one_liner( + """ import sys sys.__plen = len(sys.path) - """) - postlude = _one_liner(""" + """ + ) + postlude = _one_liner( + """ import sys new = sys.path[sys.__plen:] del sys.path[sys.__plen:] p = getattr(sys, '__egginsert', 0) sys.path[p:p] = new sys.__egginsert = p + len(new) - """) + """ + ) if os.environ.get('SETUPTOOLS_SYS_PATH_TECHNIQUE', 'raw') == 'rewrite': @@ -1732,7 +1785,8 @@ def auto_chmod(func, arg, exc): chmod(arg, stat.S_IWRITE) return func(arg) et, ev, _ = sys.exc_info() - six.reraise(et, (ev[0], ev[1] + (" %s %s" % (func, arg)))) + # TODO: This code doesn't make sense. What is it trying to do? + raise (ev[0], ev[1] + (" %s %s" % (func, arg))) def update_dist_caches(dist_path, fix_zipimporter_caches): @@ -1831,8 +1885,10 @@ def _collect_zipimporter_cache_entries(normalized_path, cache): prefix_len = len(normalized_path) for p in cache: np = normalize_path(p) - if (np.startswith(normalized_path) and - np[prefix_len:prefix_len + 1] in (os.sep, '')): + if np.startswith(normalized_path) and np[prefix_len : prefix_len + 1] in ( + os.sep, + '', + ): result.append(p) return result @@ -1861,7 +1917,7 @@ def _update_zipimporter_cache(normalized_path, cache, updater=None): # get/del patterns instead. For more detailed information see the # following links: # https://github.com/pypa/setuptools/issues/202#issuecomment-202913420 - # http://bit.ly/2h9itJX + # https://foss.heptapod.net/pypy/pypy/-/blob/144c4e65cb6accb8e592f3a7584ea38265d1873c/pypy/module/zipimport/interp_zipimport.py old_entry = cache[p] del cache[p] new_entry = updater and updater(p, old_entry) @@ -1878,8 +1934,10 @@ def _remove_and_clear_zip_directory_cache_data(normalized_path): old_entry.clear() _update_zipimporter_cache( - normalized_path, zipimport._zip_directory_cache, - updater=clear_and_remove_cached_zip_archive_directory_data) + normalized_path, + zipimport._zip_directory_cache, + updater=clear_and_remove_cached_zip_archive_directory_data, + ) # PyPy Python implementation does not allow directly writing to the @@ -1891,8 +1949,7 @@ def _remove_and_clear_zip_directory_cache_data(normalized_path): # instead of being automatically corrected to use the new correct zip archive # directory information. if '__pypy__' in sys.builtin_module_names: - _replace_zip_directory_cache_data = \ - _remove_and_clear_zip_directory_cache_data + _replace_zip_directory_cache_data = _remove_and_clear_zip_directory_cache_data else: def _replace_zip_directory_cache_data(normalized_path): @@ -1910,8 +1967,10 @@ else: return old_entry _update_zipimporter_cache( - normalized_path, zipimport._zip_directory_cache, - updater=replace_cached_zip_archive_directory_data) + normalized_path, + zipimport._zip_directory_cache, + updater=replace_cached_zip_archive_directory_data, + ) def is_python(text, filename=''): @@ -1927,9 +1986,9 @@ def is_python(text, filename=''): def is_sh(executable): """Determine if the specified executable is a .sh (contains a #! line)""" try: - with io.open(executable, encoding='latin-1') as fp: + with open(executable, encoding='latin-1') as fp: magic = fp.read(2) - except (OSError, IOError): + except OSError: return executable return magic == '#!' @@ -1940,8 +1999,7 @@ def nt_quote_arg(arg): def is_python_script(script_text, filename): - """Is this text, as a whole, a Python script? (as opposed to shell/bat/etc. - """ + """Is this text, as a whole, a Python script? (as opposed to shell/bat/etc.""" if filename.endswith('.py') or filename.endswith('.pyw'): return True # extension says it's Python if is_python(script_text, filename): @@ -1965,7 +2023,7 @@ def chmod(path, mode): log.debug("changing mode of %s to %o", path, mode) try: _chmod(path, mode) - except os.error as e: + except OSError as e: log.debug("chmod failed: %s", e) @@ -2048,7 +2106,8 @@ class CommandSpec(list): @staticmethod def _render(items): cmdline = subprocess.list2cmdline( - CommandSpec._strip_quotes(item.strip()) for item in items) + CommandSpec._strip_quotes(item.strip()) for item in items + ) return '#!' + cmdline + '\n' @@ -2066,38 +2125,45 @@ class ScriptWriter: gui apps. """ - template = textwrap.dedent(r""" + template = textwrap.dedent( + r""" # EASY-INSTALL-ENTRY-SCRIPT: %(spec)r,%(group)r,%(name)r - __requires__ = %(spec)r import re import sys - from pkg_resources import load_entry_point + + # for compatibility with easy_install; see #2198 + __requires__ = %(spec)r + + try: + from importlib.metadata import distribution + except ImportError: + try: + from importlib_metadata import distribution + except ImportError: + from pkg_resources import load_entry_point + + + def importlib_load_entry_point(spec, group, name): + dist_name, _, _ = spec.partition('==') + matches = ( + entry_point + for entry_point in distribution(dist_name).entry_points + if entry_point.group == group and entry_point.name == name + ) + return next(matches).load() + + + globals().setdefault('load_entry_point', importlib_load_entry_point) + if __name__ == '__main__': sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) - sys.exit( - load_entry_point(%(spec)r, %(group)r, %(name)r)() - ) - """).lstrip() + sys.exit(load_entry_point(%(spec)r, %(group)r, %(name)r)()) + """ + ).lstrip() command_spec_class = CommandSpec - @classmethod - def get_script_args(cls, dist, executable=None, wininst=False): - # for backward compatibility - warnings.warn("Use get_args", EasyInstallDeprecationWarning) - writer = (WindowsScriptWriter if wininst else ScriptWriter).best() - header = cls.get_script_header("", executable, wininst) - return writer.get_args(dist, header) - - @classmethod - def get_script_header(cls, script_text, executable=None, wininst=False): - # for backward compatibility - warnings.warn("Use get_header", EasyInstallDeprecationWarning, stacklevel=2) - if wininst: - executable = "python.exe" - return cls.get_header(script_text, executable) - @classmethod def get_args(cls, dist, header=None): """ @@ -2113,8 +2179,7 @@ class ScriptWriter: cls._ensure_safe_name(name) script_text = cls.template % locals() args = cls._get_script_args(type_, name, header, script_text) - for res in args: - yield res + yield from args @staticmethod def _ensure_safe_name(name): @@ -2125,12 +2190,6 @@ class ScriptWriter: if has_path_sep: raise ValueError("Path separators not allowed in script names") - @classmethod - def get_writer(cls, force_windows): - # for backward compatibility - warnings.warn("Use best", EasyInstallDeprecationWarning) - return WindowsScriptWriter.best() if force_windows else cls.best() - @classmethod def best(cls): """ @@ -2157,12 +2216,6 @@ class ScriptWriter: class WindowsScriptWriter(ScriptWriter): command_spec_class = WindowsCommandSpec - @classmethod - def get_writer(cls): - # for backward compatibility - warnings.warn("Use best", EasyInstallDeprecationWarning) - return cls.best() - @classmethod def best(cls): """ @@ -2185,7 +2238,7 @@ class WindowsScriptWriter(ScriptWriter): "{ext} not listed in PATHEXT; scripts will not be " "recognized as executables." ).format(**locals()) - warnings.warn(msg, UserWarning) + SetuptoolsWarning.emit(msg) old = ['.pya', '.py', '-script.py', '.pyc', '.pyo', '.pyw', '.exe'] old.remove(ext) header = cls._adjust_header(type_, header) @@ -2195,7 +2248,7 @@ class WindowsScriptWriter(ScriptWriter): @classmethod def _adjust_header(cls, type_, orig_header): """ - Make sure 'pythonw' is used for gui and and 'python' is used for + Make sure 'pythonw' is used for gui and 'python' is used for console (regardless of what sys.executable is). """ pattern = 'pythonw.exe' @@ -2237,8 +2290,9 @@ class WindowsExecutableLauncherWriter(WindowsScriptWriter): blockers = [name + x for x in old] yield (name + ext, hdr + script_text, 't', blockers) yield ( - name + '.exe', get_win_launcher(launcher_type), - 'b' # write in binary mode + name + '.exe', + get_win_launcher(launcher_type), + 'b', # write in binary mode ) if not is_64bit(): # install a manifest for the launcher to prevent Windows @@ -2250,11 +2304,6 @@ class WindowsExecutableLauncherWriter(WindowsScriptWriter): yield (m_name, load_launcher_manifest(name), 't') -# for backward-compatibility -get_script_args = ScriptWriter.get_script_args -get_script_header = ScriptWriter.get_script_header - - def get_win_launcher(type): """ Load the Windows launcher (executable) suitable for launching a script. @@ -2265,7 +2314,10 @@ def get_win_launcher(type): """ launcher_fn = '%s.exe' % type if is_64bit(): - launcher_fn = launcher_fn.replace(".", "-64.") + if get_platform() == "win-arm64": + launcher_fn = launcher_fn.replace(".", "-arm64.") + else: + launcher_fn = launcher_fn.replace(".", "-64.") else: launcher_fn = launcher_fn.replace(".", "-32.") return resource_string('setuptools', launcher_fn) @@ -2273,14 +2325,11 @@ def get_win_launcher(type): def load_launcher_manifest(name): manifest = pkg_resources.resource_string(__name__, 'launcher manifest.xml') - if six.PY2: - return manifest % vars() - else: - return manifest.decode('utf-8') % vars() + return manifest.decode('utf-8') % vars() -def rmtree(path, ignore_errors=False, onerror=auto_chmod): - return shutil.rmtree(path, ignore_errors, onerror) +def _rmtree(path, ignore_errors=False, onexc=auto_chmod): + return py311.shutil_rmtree(path, ignore_errors, onexc) def current_umask(): @@ -2289,59 +2338,19 @@ def current_umask(): return tmp -def bootstrap(): - # This function is called when setuptools*.egg is run using /bin/sh - import setuptools +def only_strs(values): + """ + Exclude non-str values. Ref #3063. + """ + return filter(lambda val: isinstance(val, str), values) - argv0 = os.path.dirname(setuptools.__path__[0]) - sys.argv[0] = argv0 - sys.argv.append(argv0) - main() - - -def main(argv=None, **kw): - from setuptools import setup - from setuptools.dist import Distribution - - class DistributionWithoutHelpCommands(Distribution): - common_usage = "" - - def _show_help(self, *args, **kw): - with _patch_usage(): - Distribution._show_help(self, *args, **kw) - - if argv is None: - argv = sys.argv[1:] - - with _patch_usage(): - setup( - script_args=['-q', 'easy_install', '-v'] + argv, - script_name=sys.argv[0] or 'easy_install', - distclass=DistributionWithoutHelpCommands, - **kw - ) - - -@contextlib.contextmanager -def _patch_usage(): - import distutils.core - USAGE = textwrap.dedent(""" - usage: %(script)s [options] requirement_or_url ... - or: %(script)s --help - """).lstrip() - - def gen_usage(script_name): - return USAGE % dict( - script=os.path.basename(script_name), - ) - - saved = distutils.core.gen_usage - distutils.core.gen_usage = gen_usage - try: - yield - finally: - distutils.core.gen_usage = saved class EasyInstallDeprecationWarning(SetuptoolsDeprecationWarning): - """Class for warning about deprecations in EasyInstall in SetupTools. Not ignored by default, unlike DeprecationWarning.""" - + _SUMMARY = "easy_install command is deprecated." + _DETAILS = """ + Please avoid running ``setup.py`` and ``easy_install``. + Instead, use pypa/build, pypa/installer or other + standards-based tools. + """ + _SEE_URL = "https://github.com/pypa/setuptools/issues/917" + # _DUE_DATE not defined yet diff --git a/libs/common/setuptools/command/egg_info.py b/libs/common/setuptools/command/egg_info.py index a5c5a2fc..62d2feea 100644 --- a/libs/common/setuptools/command/egg_info.py +++ b/libs/common/setuptools/command/egg_info.py @@ -8,32 +8,33 @@ from distutils.util import convert_path from distutils import log import distutils.errors import distutils.filelist +import functools import os import re import sys -import io -import warnings import time import collections -from setuptools.extern import six -from setuptools.extern.six.moves import map +from .._importlib import metadata +from .. import _entry_points, _normalization +from . import _requirestxt from setuptools import Command from setuptools.command.sdist import sdist from setuptools.command.sdist import walk_revctrl from setuptools.command.setopt import edit_config from setuptools.command import bdist_egg -from pkg_resources import ( - parse_requirements, safe_name, parse_version, - safe_version, yield_lines, EntryPoint, iter_entry_points, to_filename) import setuptools.unicode_utils as unicode_utils from setuptools.glob import glob from setuptools.extern import packaging -from setuptools import SetuptoolsDeprecationWarning +from ..warnings import SetuptoolsDeprecationWarning -def translate_pattern(glob): + +PY_MAJOR = '{}.{}'.format(*sys.version_info) + + +def translate_pattern(glob): # noqa: C901 # is too complex (14) # FIXME """ Translate a file path glob like '*.txt' in to a regular expression. This differs from fnmatch.translate which allows wildcards to match @@ -91,7 +92,7 @@ def translate_pattern(glob): pat += re.escape(char) else: # Grab the insides of the [brackets] - inner = chunk[i + 1:inner_i] + inner = chunk[i + 1 : inner_i] char_class = '' # Class negation @@ -113,7 +114,7 @@ def translate_pattern(glob): pat += sep pat += r'\Z' - return re.compile(pat, flags=re.MULTILINE|re.DOTALL) + return re.compile(pat, flags=re.MULTILINE | re.DOTALL) class InfoCommon: @@ -122,23 +123,44 @@ class InfoCommon: @property def name(self): - return safe_name(self.distribution.get_name()) + return _normalization.safe_name(self.distribution.get_name()) def tagged_version(self): - version = self.distribution.get_version() - # egg_info may be called more than once for a distribution, - # in which case the version string already contains all tags. - if self.vtags and version.endswith(self.vtags): - return safe_version(version) - return safe_version(version + self.vtags) + tagged = self._maybe_tag(self.distribution.get_version()) + return _normalization.safe_version(tagged) - def tags(self): + def _maybe_tag(self, version): + """ + egg_info may be called more than once for a distribution, + in which case the version string already contains all tags. + """ + return ( + version + if self.vtags and self._already_tagged(version) + else version + self.vtags + ) + + def _already_tagged(self, version: str) -> bool: + # Depending on their format, tags may change with version normalization. + # So in addition the regular tags, we have to search for the normalized ones. + return version.endswith(self.vtags) or version.endswith(self._safe_tags()) + + def _safe_tags(self) -> str: + # To implement this we can rely on `safe_version` pretending to be version 0 + # followed by tags. Then we simply discard the starting 0 (fake version number) + try: + return _normalization.safe_version(f"0{self.vtags}")[1:] + except packaging.version.InvalidVersion: + return _normalization.safe_name(self.vtags.replace(' ', '.')) + + def tags(self) -> str: version = '' if self.tag_build: version += self.tag_build if self.tag_date: - version += time.strftime("-%Y%m%d") + version += time.strftime("%Y%m%d") return version + vtags = property(tags) @@ -146,8 +168,12 @@ class egg_info(InfoCommon, Command): description = "create a distribution's .egg-info directory" user_options = [ - ('egg-base=', 'e', "directory containing .egg-info directories" - " (default: top of the source tree)"), + ( + 'egg-base=', + 'e', + "directory containing .egg-info directories" + " (default: top of the source tree)", + ), ('tag-date', 'd', "Add date stamp (e.g. 20050528) to version number"), ('tag-build=', 'b', "Specify explicit tag to add to version number"), ('no-date', 'D', "Don't include date stamp [default]"), @@ -163,7 +189,7 @@ class egg_info(InfoCommon, Command): self.egg_name = None self.egg_info = None self.egg_version = None - self.broken_egg_info = False + self.ignore_egg_info_in_manifest = False #################################### # allow the 'tag_svn_revision' to be detected and @@ -175,6 +201,7 @@ class egg_info(InfoCommon, Command): @tag_svn_revision.setter def tag_svn_revision(self, value): pass + #################################### def save_version_info(self, filename): @@ -197,32 +224,26 @@ class egg_info(InfoCommon, Command): # repercussions. self.egg_name = self.name self.egg_version = self.tagged_version() - parsed_version = parse_version(self.egg_version) + parsed_version = packaging.version.Version(self.egg_version) try: is_version = isinstance(parsed_version, packaging.version.Version) - spec = ( - "%s==%s" if is_version else "%s===%s" - ) - list( - parse_requirements(spec % (self.egg_name, self.egg_version)) - ) - except ValueError: + spec = "%s==%s" if is_version else "%s===%s" + packaging.requirements.Requirement(spec % (self.egg_name, self.egg_version)) + except ValueError as e: raise distutils.errors.DistutilsOptionError( - "Invalid distribution name or version syntax: %s-%s" % - (self.egg_name, self.egg_version) - ) + "Invalid distribution name or version syntax: %s-%s" + % (self.egg_name, self.egg_version) + ) from e if self.egg_base is None: dirs = self.distribution.package_dir self.egg_base = (dirs or {}).get('', os.curdir) self.ensure_dirname('egg_base') - self.egg_info = to_filename(self.egg_name) + '.egg-info' + self.egg_info = _normalization.filename_component(self.egg_name) + '.egg-info' if self.egg_base != os.curdir: self.egg_info = os.path.join(self.egg_base, self.egg_info) - if '-' in self.egg_name: - self.check_broken_egg_info() # Set package version for the benefit of dumber commands # (e.g. sdist, bdist_wininst, etc.) @@ -234,11 +255,16 @@ class egg_info(InfoCommon, Command): # to the version info # pd = self.distribution._patched_dist - if pd is not None and pd.key == self.egg_name.lower(): + key = getattr(pd, "key", None) or getattr(pd, "name", None) + if pd is not None and key == self.egg_name.lower(): pd._version = self.egg_version - pd._parsed_version = parse_version(self.egg_version) + pd._parsed_version = packaging.version.Version(self.egg_version) self.distribution._patched_dist = None + def _get_egg_basename(self, py_version=PY_MAJOR, platform=None): + """Compute filename of the output egg. Private API.""" + return _egg_basename(self.egg_name, self.egg_version, py_version, platform) + def write_or_delete_file(self, what, filename, data, force=False): """Write `data` to `filename` or delete if empty @@ -252,9 +278,7 @@ class egg_info(InfoCommon, Command): self.write_file(what, filename, data) elif os.path.exists(filename): if data is None and not force: - log.warn( - "%s not set in setup(), but %s exists", what, filename - ) + log.warn("%s not set in setup(), but %s exists", what, filename) return else: self.delete_file(filename) @@ -266,8 +290,7 @@ class egg_info(InfoCommon, Command): to the file. """ log.info("writing %s to %s", what, filename) - if not six.PY2: - data = data.encode("utf-8") + data = data.encode("utf-8") if not self.dry_run: f = open(filename, 'wb') f.write(data) @@ -281,11 +304,13 @@ class egg_info(InfoCommon, Command): def run(self): self.mkpath(self.egg_info) - os.utime(self.egg_info, None) - installer = self.distribution.fetch_build_egg - for ep in iter_entry_points('egg_info.writers'): - ep.require(installer=installer) - writer = ep.resolve() + try: + os.utime(self.egg_info, None) + except OSError as e: + msg = f"Cannot update time stamp of directory '{self.egg_info}'" + raise distutils.errors.DistutilsFileError(msg) from e + for ep in metadata.entry_points(group='egg_info.writers'): + writer = ep.load() writer(self, ep.name, os.path.join(self.egg_info, ep.name)) # Get rid of native_libs.txt if it was put there by older bdist_egg @@ -299,29 +324,19 @@ class egg_info(InfoCommon, Command): """Generate SOURCES.txt manifest file""" manifest_filename = os.path.join(self.egg_info, "SOURCES.txt") mm = manifest_maker(self.distribution) + mm.ignore_egg_info_dir = self.ignore_egg_info_in_manifest mm.manifest = manifest_filename mm.run() self.filelist = mm.filelist - def check_broken_egg_info(self): - bei = self.egg_name + '.egg-info' - if self.egg_base != os.curdir: - bei = os.path.join(self.egg_base, bei) - if os.path.exists(bei): - log.warn( - "-" * 78 + '\n' - "Note: Your current .egg-info directory has a '-' in its name;" - '\nthis will not work correctly with "setup.py develop".\n\n' - 'Please rename %s to %s to correct this problem.\n' + '-' * 78, - bei, self.egg_info - ) - self.broken_egg_info = self.egg_info - self.egg_info = bei # make it work for now - class FileList(_FileList): # Implementations of the various MANIFEST.in commands + def __init__(self, warn=None, debug_print=None, ignore_egg_info_dir=False): + super().__init__(warn, debug_print) + self.ignore_egg_info_dir = ignore_egg_info_dir + def process_template_line(self, line): # Parse the line: split it up, make sure the right number of words # is there, and return the relevant words. 'action' is always @@ -330,70 +345,67 @@ class FileList(_FileList): # patterns, (dir and patterns), or (dir_pattern). (action, patterns, dir, dir_pattern) = self._parse_template_line(line) + action_map = { + 'include': self.include, + 'exclude': self.exclude, + 'global-include': self.global_include, + 'global-exclude': self.global_exclude, + 'recursive-include': functools.partial( + self.recursive_include, + dir, + ), + 'recursive-exclude': functools.partial( + self.recursive_exclude, + dir, + ), + 'graft': self.graft, + 'prune': self.prune, + } + log_map = { + 'include': "warning: no files found matching '%s'", + 'exclude': ("warning: no previously-included files found " "matching '%s'"), + 'global-include': ( + "warning: no files found matching '%s' " "anywhere in distribution" + ), + 'global-exclude': ( + "warning: no previously-included files matching " + "'%s' found anywhere in distribution" + ), + 'recursive-include': ( + "warning: no files found matching '%s' " "under directory '%s'" + ), + 'recursive-exclude': ( + "warning: no previously-included files matching " + "'%s' found under directory '%s'" + ), + 'graft': "warning: no directories found matching '%s'", + 'prune': "no previously-included directories found matching '%s'", + } + + try: + process_action = action_map[action] + except KeyError: + msg = f"Invalid MANIFEST.in: unknown action {action!r} in {line!r}" + raise DistutilsInternalError(msg) from None + # OK, now we know that the action is valid and we have the # right number of words on the line for that action -- so we # can proceed with minimal error-checking. - if action == 'include': - self.debug_print("include " + ' '.join(patterns)) - for pattern in patterns: - if not self.include(pattern): - log.warn("warning: no files found matching '%s'", pattern) - elif action == 'exclude': - self.debug_print("exclude " + ' '.join(patterns)) - for pattern in patterns: - if not self.exclude(pattern): - log.warn(("warning: no previously-included files " - "found matching '%s'"), pattern) + action_is_recursive = action.startswith('recursive-') + if action in {'graft', 'prune'}: + patterns = [dir_pattern] + extra_log_args = (dir,) if action_is_recursive else () + log_tmpl = log_map[action] - elif action == 'global-include': - self.debug_print("global-include " + ' '.join(patterns)) - for pattern in patterns: - if not self.global_include(pattern): - log.warn(("warning: no files found matching '%s' " - "anywhere in distribution"), pattern) - - elif action == 'global-exclude': - self.debug_print("global-exclude " + ' '.join(patterns)) - for pattern in patterns: - if not self.global_exclude(pattern): - log.warn(("warning: no previously-included files matching " - "'%s' found anywhere in distribution"), - pattern) - - elif action == 'recursive-include': - self.debug_print("recursive-include %s %s" % - (dir, ' '.join(patterns))) - for pattern in patterns: - if not self.recursive_include(dir, pattern): - log.warn(("warning: no files found matching '%s' " - "under directory '%s'"), - pattern, dir) - - elif action == 'recursive-exclude': - self.debug_print("recursive-exclude %s %s" % - (dir, ' '.join(patterns))) - for pattern in patterns: - if not self.recursive_exclude(dir, pattern): - log.warn(("warning: no previously-included files matching " - "'%s' found under directory '%s'"), - pattern, dir) - - elif action == 'graft': - self.debug_print("graft " + dir_pattern) - if not self.graft(dir_pattern): - log.warn("warning: no directories found matching '%s'", - dir_pattern) - - elif action == 'prune': - self.debug_print("prune " + dir_pattern) - if not self.prune(dir_pattern): - log.warn(("no previously-included directories found " - "matching '%s'"), dir_pattern) - - else: - raise DistutilsInternalError( - "this cannot happen: invalid action '%s'" % action) + self.debug_print( + ' '.join( + [action] + ([dir] if action_is_recursive else []) + patterns, + ) + ) + for pattern in patterns: + if not process_action(pattern): + log.warn(log_tmpl, pattern, *extra_log_args) def _remove_files(self, predicate): """ @@ -424,8 +436,7 @@ class FileList(_FileList): Include all files anywhere in 'dir/' that match the pattern. """ full_pattern = os.path.join(dir, '**', pattern) - found = [f for f in glob(full_pattern, recursive=True) - if not os.path.isdir(f)] + found = [f for f in glob(full_pattern, recursive=True) if not os.path.isdir(f)] self.extend(found) return bool(found) @@ -507,6 +518,10 @@ class FileList(_FileList): return False try: + # ignore egg-info paths + is_egg_info = ".egg-info" in u_path or b".egg-info" in utf8_path + if self.ignore_egg_info_dir and is_egg_info: + return False # accept is either way checks out if os.path.exists(u_path) or os.path.exists(utf8_path): return True @@ -523,17 +538,20 @@ class manifest_maker(sdist): self.prune = 1 self.manifest_only = 1 self.force_manifest = 1 + self.ignore_egg_info_dir = False def finalize_options(self): pass def run(self): - self.filelist = FileList() + self.filelist = FileList(ignore_egg_info_dir=self.ignore_egg_info_dir) if not os.path.exists(self.manifest): self.write_manifest() # it must exist so it'll get in the list self.add_defaults() if os.path.exists(self.template): self.read_template() + self.add_license_files() + self._add_referenced_files() self.prune_file_list() self.filelist.sort() self.filelist.remove_duplicates() @@ -568,7 +586,6 @@ class manifest_maker(sdist): def add_defaults(self): sdist.add_defaults(self) - self.check_license() self.filelist.append(self.template) self.filelist.append(self.manifest) rcfiles = list(walk_revctrl()) @@ -585,14 +602,53 @@ class manifest_maker(sdist): ei_cmd = self.get_finalized_command('egg_info') self.filelist.graft(ei_cmd.egg_info) + def add_license_files(self): + license_files = self.distribution.metadata.license_files or [] + for lf in license_files: + log.info("adding license file '%s'", lf) + self.filelist.extend(license_files) + + def _add_referenced_files(self): + """Add files referenced by the config (e.g. `file:` directive) to filelist""" + referenced = getattr(self.distribution, '_referenced_files', []) + # ^-- fallback if dist comes from distutils or is a custom class + for rf in referenced: + log.debug("adding file referenced by config '%s'", rf) + self.filelist.extend(referenced) + def prune_file_list(self): build = self.get_finalized_command('build') base_dir = self.distribution.get_fullname() self.filelist.prune(build.build_base) self.filelist.prune(base_dir) sep = re.escape(os.sep) - self.filelist.exclude_pattern(r'(^|' + sep + r')(RCS|CVS|\.svn)' + sep, - is_regex=1) + self.filelist.exclude_pattern( + r'(^|' + sep + r')(RCS|CVS|\.svn)' + sep, is_regex=1 + ) + + def _safe_data_files(self, build_py): + """ + The parent class implementation of this method + (``sdist``) will try to include data files, which + might cause recursion problems when + ``include_package_data=True``. + + Therefore, avoid triggering any attempt of + analyzing/building the manifest again. + """ + if hasattr(build_py, 'get_data_files_without_manifest'): + return build_py.get_data_files_without_manifest() + + SetuptoolsDeprecationWarning.emit( + "`build_py` command does not inherit from setuptools' `build_py`.", + """ + Custom 'build_py' does not implement 'get_data_files_without_manifest'. + Please extend command classes from setuptools instead of distutils. + """, + see_url="https://peps.python.org/pep-0632/", + # due_date not defined yet, old projects might still do it? + ) + return build_py.get_data_files() def write_file(filename, contents): @@ -628,44 +684,24 @@ def write_pkg_info(cmd, basename, filename): def warn_depends_obsolete(cmd, basename, filename): - if os.path.exists(filename): - log.warn( - "WARNING: 'depends.txt' is not used by setuptools 0.6!\n" - "Use the install_requires/extras_require setup() args instead." - ) + """ + Unused: left to avoid errors when updating (from source) from <= 67.8. + Old installations have a .dist-info directory with the entry-point + ``depends.txt = setuptools.command.egg_info:warn_depends_obsolete``. + This may trigger errors when running the first egg_info in build_meta. + TODO: Remove this function in a version sufficiently > 68. + """ -def _write_requirements(stream, reqs): - lines = yield_lines(reqs or ()) - append_cr = lambda line: line + '\n' - lines = map(append_cr, lines) - stream.writelines(lines) - - -def write_requirements(cmd, basename, filename): - dist = cmd.distribution - data = six.StringIO() - _write_requirements(data, dist.install_requires) - extras_require = dist.extras_require or {} - for extra in sorted(extras_require): - data.write('\n[{extra}]\n'.format(**vars())) - _write_requirements(data, extras_require[extra]) - cmd.write_or_delete_file("requirements", filename, data.getvalue()) - - -def write_setup_requirements(cmd, basename, filename): - data = io.StringIO() - _write_requirements(data, cmd.distribution.setup_requires) - cmd.write_or_delete_file("setup-requirements", filename, data.getvalue()) +# Export API used in entry_points +write_requirements = _requirestxt.write_requirements +write_setup_requirements = _requirestxt.write_setup_requirements def write_toplevel_names(cmd, basename, filename): - pkgs = dict.fromkeys( - [ - k.split('.', 1)[0] - for k in cmd.distribution.iter_distribution_names() - ] - ) + pkgs = dict.fromkeys([ + k.split('.', 1)[0] for k in cmd.distribution.iter_distribution_names() + ]) cmd.write_file("top-level names", filename, '\n'.join(sorted(pkgs)) + '\n') @@ -682,36 +718,20 @@ def write_arg(cmd, basename, filename, force=False): def write_entries(cmd, basename, filename): - ep = cmd.distribution.entry_points - - if isinstance(ep, six.string_types) or ep is None: - data = ep - elif ep is not None: - data = [] - for section, contents in sorted(ep.items()): - if not isinstance(contents, six.string_types): - contents = EntryPoint.parse_group(section, contents) - contents = '\n'.join(sorted(map(str, contents.values()))) - data.append('[%s]\n%s\n\n' % (section, contents)) - data = ''.join(data) - - cmd.write_or_delete_file('entry points', filename, data, True) + eps = _entry_points.load(cmd.distribution.entry_points) + defn = _entry_points.render(eps) + cmd.write_or_delete_file('entry points', filename, defn, True) -def get_pkg_info_revision(): - """ - Get a -r### off of PKG-INFO Version in case this is an sdist of - a subversion revision. - """ - warnings.warn("get_pkg_info_revision is deprecated.", EggInfoDeprecationWarning) - if os.path.exists('PKG-INFO'): - with io.open('PKG-INFO') as f: - for line in f: - match = re.match(r"Version:.*-r(\d+)\s*$", line) - if match: - return int(match.group(1)) - return 0 +def _egg_basename(egg_name, egg_version, py_version=None, platform=None): + """Compute filename of the output egg. Private API.""" + name = _normalization.filename_component(egg_name) + version = _normalization.filename_component(egg_version) + egg = f"{name}-{version}-py{py_version or PY_MAJOR}" + if platform: + egg += f"-{platform}" + return egg class EggInfoDeprecationWarning(SetuptoolsDeprecationWarning): - """Class for warning about deprecations in eggInfo in setupTools. Not ignored by default, unlike DeprecationWarning.""" + """Deprecated behavior warning for EggInfo, bypassing suppression.""" diff --git a/libs/common/setuptools/command/install.py b/libs/common/setuptools/command/install.py index 72b9a3e4..b97a9b47 100644 --- a/libs/common/setuptools/command/install.py +++ b/libs/common/setuptools/command/install.py @@ -1,11 +1,11 @@ from distutils.errors import DistutilsArgError import inspect import glob -import warnings import platform import distutils.command.install as orig import setuptools +from ..warnings import SetuptoolsDeprecationWarning, SetuptoolsWarning # Prior to numpy 1.9, NumPy relies on the '_install' name, so provide it for # now. See https://github.com/pypa/setuptools/issues/199/ @@ -17,11 +17,15 @@ class install(orig.install): user_options = orig.install.user_options + [ ('old-and-unmanageable', None, "Try not to use this!"), - ('single-version-externally-managed', None, - "used by system package builders to create 'flat' eggs"), + ( + 'single-version-externally-managed', + None, + "used by system package builders to create 'flat' eggs", + ), ] boolean_options = orig.install.boolean_options + [ - 'old-and-unmanageable', 'single-version-externally-managed', + 'old-and-unmanageable', + 'single-version-externally-managed', ] new_commands = [ ('install_egg_info', lambda self: True), @@ -30,6 +34,19 @@ class install(orig.install): _nc = dict(new_commands) def initialize_options(self): + SetuptoolsDeprecationWarning.emit( + "setup.py install is deprecated.", + """ + Please avoid running ``setup.py`` directly. + Instead, use pypa/build, pypa/installer or other + standards-based tools. + """, + see_url="https://blog.ganssle.io/articles/2021/10/setup-py-deprecated.html", + # TODO: Document how to bootstrap setuptools without install + # (e.g. by unziping the wheel file) + # and then add a due_date to this warning. + ) + orig.install.initialize_options(self) self.old_and_unmanageable = None self.single_version_externally_managed = None @@ -54,6 +71,7 @@ class install(orig.install): # command without --root or --single-version-externally-managed self.path_file = None self.extra_dirs = '' + return None def run(self): # Explicit request for old-style install? Just do it @@ -66,6 +84,8 @@ class install(orig.install): else: self.do_egg_install() + return None + @staticmethod def _called_from_setup(run_frame): """ @@ -79,26 +99,34 @@ class install(orig.install): """ if run_frame is None: msg = "Call stack not available. bdist_* commands may fail." - warnings.warn(msg) + SetuptoolsWarning.emit(msg) if platform.python_implementation() == 'IronPython': msg = "For best results, pass -X:Frames to enable call stack." - warnings.warn(msg) + SetuptoolsWarning.emit(msg) return True - res = inspect.getouterframes(run_frame)[2] - caller, = res[:1] - info = inspect.getframeinfo(caller) - caller_module = caller.f_globals.get('__name__', '') - return ( - caller_module == 'distutils.dist' - and info.function == 'run_commands' - ) + + frames = inspect.getouterframes(run_frame) + for frame in frames[2:4]: + (caller,) = frame[:1] + info = inspect.getframeinfo(caller) + caller_module = caller.f_globals.get('__name__', '') + + if caller_module == "setuptools.dist" and info.function == "run_command": + # Starting from v61.0.0 setuptools overwrites dist.run_command + continue + + return caller_module == 'distutils.dist' and info.function == 'run_commands' + + return False def do_egg_install(self): - easy_install = self.distribution.get_command_class('easy_install') cmd = easy_install( - self.distribution, args="x", root=self.root, record=self.record, + self.distribution, + args="x", + root=self.root, + record=self.record, ) cmd.ensure_finalized() # finalize before bdist_egg munges install cmd cmd.always_copy_from = '.' # make sure local-dir eggs get installed @@ -119,7 +147,6 @@ class install(orig.install): # XXX Python 3.1 doesn't see _nc if this is inside the class -install.sub_commands = ( - [cmd for cmd in orig.install.sub_commands if cmd[0] not in install._nc] + - install.new_commands -) +install.sub_commands = [ + cmd for cmd in orig.install.sub_commands if cmd[0] not in install._nc +] + install.new_commands diff --git a/libs/common/setuptools/command/install_egg_info.py b/libs/common/setuptools/command/install_egg_info.py index edc4718b..a1d2e818 100644 --- a/libs/common/setuptools/command/install_egg_info.py +++ b/libs/common/setuptools/command/install_egg_info.py @@ -4,7 +4,7 @@ import os from setuptools import Command from setuptools import namespaces from setuptools.archive_util import unpack_archive -import pkg_resources +from .._path import ensure_directory class install_egg_info(namespaces.Installer, Command): @@ -20,12 +20,9 @@ class install_egg_info(namespaces.Installer, Command): self.install_dir = None def finalize_options(self): - self.set_undefined_options('install_lib', - ('install_dir', 'install_dir')) + self.set_undefined_options('install_lib', ('install_dir', 'install_dir')) ei_cmd = self.get_finalized_command("egg_info") - basename = pkg_resources.Distribution( - None, None, ei_cmd.egg_name, ei_cmd.egg_version - ).egg_name() + '.egg-info' + basename = f"{ei_cmd._get_egg_basename()}.egg-info" self.source = ei_cmd.egg_info self.target = os.path.join(self.install_dir, basename) self.outputs = [] @@ -37,10 +34,8 @@ class install_egg_info(namespaces.Installer, Command): elif os.path.exists(self.target): self.execute(os.unlink, (self.target,), "Removing " + self.target) if not self.dry_run: - pkg_resources.ensure_directory(self.target) - self.execute( - self.copytree, (), "Copying %s to %s" % (self.source, self.target) - ) + ensure_directory(self.target) + self.execute(self.copytree, (), "Copying %s to %s" % (self.source, self.target)) self.install_namespaces() def get_outputs(self): diff --git a/libs/common/setuptools/command/install_lib.py b/libs/common/setuptools/command/install_lib.py index 07d65933..32ff65e7 100644 --- a/libs/common/setuptools/command/install_lib.py +++ b/libs/common/setuptools/command/install_lib.py @@ -84,8 +84,13 @@ class install_lib(orig.install_lib): yield base + '.opt-2.pyc' def copy_tree( - self, infile, outfile, - preserve_mode=1, preserve_times=1, preserve_symlinks=0, level=1 + self, + infile, + outfile, + preserve_mode=1, + preserve_times=1, + preserve_symlinks=0, + level=1, ): assert preserve_mode and preserve_times and not preserve_symlinks exclude = self.get_exclusions() @@ -102,8 +107,7 @@ class install_lib(orig.install_lib): def pf(src, dst): if dst in exclude: - log.warn("Skipping installation of %s (namespace package)", - dst) + log.warn("Skipping installation of %s (namespace package)", dst) return False log.info("copying %s -> %s", src, os.path.dirname(dst)) diff --git a/libs/common/setuptools/command/install_scripts.py b/libs/common/setuptools/command/install_scripts.py index 16234273..72b2e45c 100644 --- a/libs/common/setuptools/command/install_scripts.py +++ b/libs/common/setuptools/command/install_scripts.py @@ -3,7 +3,7 @@ import distutils.command.install_scripts as orig import os import sys -from pkg_resources import Distribution, PathMetadata, ensure_directory +from .._path import ensure_directory class install_scripts(orig.install_scripts): @@ -14,8 +14,6 @@ class install_scripts(orig.install_scripts): self.no_ep = False def run(self): - import setuptools.command.easy_install as ei - self.run_command("egg_info") if self.distribution.scripts: orig.install_scripts.run(self) # run first to set up self.outfiles @@ -24,20 +22,23 @@ class install_scripts(orig.install_scripts): if self.no_ep: # don't install entry point scripts into .egg file! return + self._install_ep_scripts() + + def _install_ep_scripts(self): + # Delay import side-effects + from pkg_resources import Distribution, PathMetadata + from . import easy_install as ei ei_cmd = self.get_finalized_command("egg_info") dist = Distribution( - ei_cmd.egg_base, PathMetadata(ei_cmd.egg_base, ei_cmd.egg_info), - ei_cmd.egg_name, ei_cmd.egg_version, + ei_cmd.egg_base, + PathMetadata(ei_cmd.egg_base, ei_cmd.egg_info), + ei_cmd.egg_name, + ei_cmd.egg_version, ) bs_cmd = self.get_finalized_command('build_scripts') exec_param = getattr(bs_cmd, 'executable', None) - bw_cmd = self.get_finalized_command("bdist_wininst") - is_wininst = getattr(bw_cmd, '_is_running', False) writer = ei.ScriptWriter - if is_wininst: - exec_param = "python.exe" - writer = ei.WindowsScriptWriter if exec_param == sys.executable: # In case the path to the Python executable contains a space, wrap # it so it's not split up. diff --git a/libs/common/setuptools/command/py36compat.py b/libs/common/setuptools/command/py36compat.py deleted file mode 100644 index 61063e75..00000000 --- a/libs/common/setuptools/command/py36compat.py +++ /dev/null @@ -1,136 +0,0 @@ -import os -from glob import glob -from distutils.util import convert_path -from distutils.command import sdist - -from setuptools.extern.six.moves import filter - - -class sdist_add_defaults: - """ - Mix-in providing forward-compatibility for functionality as found in - distutils on Python 3.7. - - Do not edit the code in this class except to update functionality - as implemented in distutils. Instead, override in the subclass. - """ - - def add_defaults(self): - """Add all the default files to self.filelist: - - README or README.txt - - setup.py - - test/test*.py - - all pure Python modules mentioned in setup script - - all files pointed by package_data (build_py) - - all files defined in data_files. - - all files defined as scripts. - - all C sources listed as part of extensions or C libraries - in the setup script (doesn't catch C headers!) - Warns if (README or README.txt) or setup.py are missing; everything - else is optional. - """ - self._add_defaults_standards() - self._add_defaults_optional() - self._add_defaults_python() - self._add_defaults_data_files() - self._add_defaults_ext() - self._add_defaults_c_libs() - self._add_defaults_scripts() - - @staticmethod - def _cs_path_exists(fspath): - """ - Case-sensitive path existence check - - >>> sdist_add_defaults._cs_path_exists(__file__) - True - >>> sdist_add_defaults._cs_path_exists(__file__.upper()) - False - """ - if not os.path.exists(fspath): - return False - # make absolute so we always have a directory - abspath = os.path.abspath(fspath) - directory, filename = os.path.split(abspath) - return filename in os.listdir(directory) - - def _add_defaults_standards(self): - standards = [self.READMES, self.distribution.script_name] - for fn in standards: - if isinstance(fn, tuple): - alts = fn - got_it = False - for fn in alts: - if self._cs_path_exists(fn): - got_it = True - self.filelist.append(fn) - break - - if not got_it: - self.warn("standard file not found: should have one of " + - ', '.join(alts)) - else: - if self._cs_path_exists(fn): - self.filelist.append(fn) - else: - self.warn("standard file '%s' not found" % fn) - - def _add_defaults_optional(self): - optional = ['test/test*.py', 'setup.cfg'] - for pattern in optional: - files = filter(os.path.isfile, glob(pattern)) - self.filelist.extend(files) - - def _add_defaults_python(self): - # build_py is used to get: - # - python modules - # - files defined in package_data - build_py = self.get_finalized_command('build_py') - - # getting python files - if self.distribution.has_pure_modules(): - self.filelist.extend(build_py.get_source_files()) - - # getting package_data files - # (computed in build_py.data_files by build_py.finalize_options) - for pkg, src_dir, build_dir, filenames in build_py.data_files: - for filename in filenames: - self.filelist.append(os.path.join(src_dir, filename)) - - def _add_defaults_data_files(self): - # getting distribution.data_files - if self.distribution.has_data_files(): - for item in self.distribution.data_files: - if isinstance(item, str): - # plain file - item = convert_path(item) - if os.path.isfile(item): - self.filelist.append(item) - else: - # a (dirname, filenames) tuple - dirname, filenames = item - for f in filenames: - f = convert_path(f) - if os.path.isfile(f): - self.filelist.append(f) - - def _add_defaults_ext(self): - if self.distribution.has_ext_modules(): - build_ext = self.get_finalized_command('build_ext') - self.filelist.extend(build_ext.get_source_files()) - - def _add_defaults_c_libs(self): - if self.distribution.has_c_libraries(): - build_clib = self.get_finalized_command('build_clib') - self.filelist.extend(build_clib.get_source_files()) - - def _add_defaults_scripts(self): - if self.distribution.has_scripts(): - build_scripts = self.get_finalized_command('build_scripts') - self.filelist.extend(build_scripts.get_source_files()) - - -if hasattr(sdist.sdist, '_add_defaults_standards'): - # disable the functionality already available upstream - class sdist_add_defaults: - pass diff --git a/libs/common/setuptools/command/rotate.py b/libs/common/setuptools/command/rotate.py index b89353f5..cfb78ce5 100644 --- a/libs/common/setuptools/command/rotate.py +++ b/libs/common/setuptools/command/rotate.py @@ -4,8 +4,6 @@ from distutils.errors import DistutilsOptionError import os import shutil -from setuptools.extern import six - from setuptools import Command @@ -36,12 +34,10 @@ class rotate(Command): raise DistutilsOptionError("Must specify number of files to keep") try: self.keep = int(self.keep) - except ValueError: - raise DistutilsOptionError("--keep must be an integer") - if isinstance(self.match, six.string_types): - self.match = [ - convert_path(p.strip()) for p in self.match.split(',') - ] + except ValueError as e: + raise DistutilsOptionError("--keep must be an integer") from e + if isinstance(self.match, str): + self.match = [convert_path(p.strip()) for p in self.match.split(',')] self.set_undefined_options('bdist', ('dist_dir', 'dist_dir')) def run(self): @@ -56,8 +52,8 @@ class rotate(Command): files.reverse() log.info("%d file(s) matching %s", len(files), pattern) - files = files[self.keep:] - for (t, f) in files: + files = files[self.keep :] + for t, f in files: log.info("Deleting %s", f) if not self.dry_run: if os.path.isdir(f): diff --git a/libs/common/setuptools/command/saveopts.py b/libs/common/setuptools/command/saveopts.py index 611cec55..f175de10 100644 --- a/libs/common/setuptools/command/saveopts.py +++ b/libs/common/setuptools/command/saveopts.py @@ -11,7 +11,6 @@ class saveopts(option_base): settings = {} for cmd in dist.command_options: - if cmd == 'saveopts': continue # don't save our own options! diff --git a/libs/common/setuptools/command/sdist.py b/libs/common/setuptools/command/sdist.py index 8c3438ea..d455f44c 100644 --- a/libs/common/setuptools/command/sdist.py +++ b/libs/common/setuptools/command/sdist.py @@ -1,38 +1,46 @@ from distutils import log import distutils.command.sdist as orig import os -import sys -import io import contextlib +from itertools import chain -from setuptools.extern import six, ordered_set - -from .py36compat import sdist_add_defaults - -import pkg_resources +from .._importlib import metadata +from .build import _ORIGINAL_SUBCOMMANDS _default_revctrl = list def walk_revctrl(dirname=''): """Find all files under revision control""" - for ep in pkg_resources.iter_entry_points('setuptools.file_finders'): - for item in ep.load()(dirname): - yield item + for ep in metadata.entry_points(group='setuptools.file_finders'): + yield from ep.load()(dirname) -class sdist(sdist_add_defaults, orig.sdist): +class sdist(orig.sdist): """Smart sdist that finds anything supported by revision control""" user_options = [ - ('formats=', None, - "formats for source distribution (comma-separated list)"), - ('keep-temp', 'k', - "keep the distribution tree around after creating " + - "archive file(s)"), - ('dist-dir=', 'd', - "directory to put the source distribution archive(s) in " - "[default: dist]"), + ('formats=', None, "formats for source distribution (comma-separated list)"), + ( + 'keep-temp', + 'k', + "keep the distribution tree around after creating " + "archive file(s)", + ), + ( + 'dist-dir=', + 'd', + "directory to put the source distribution archive(s) in " "[default: dist]", + ), + ( + 'owner=', + 'u', + "Owner name used when creating a tar file [default: current user]", + ), + ( + 'group=', + 'g', + "Group name used when creating a tar file [default: current group]", + ), ] negative_opt = {} @@ -62,14 +70,6 @@ class sdist(sdist_add_defaults, orig.sdist): def initialize_options(self): orig.sdist.initialize_options(self) - self._default_to_gztar() - - def _default_to_gztar(self): - # only needed on Python prior to 3.6. - if sys.version_info >= (3, 6, 0, 'beta', 1): - return - self.formats = ['gztar'] - def make_distribution(self): """ Workaround for #516 @@ -96,36 +96,14 @@ class sdist(sdist_add_defaults, orig.sdist): yield finally: if orig_val is not NoValue: - setattr(os, 'link', orig_val) + os.link = orig_val - def __read_template_hack(self): - # This grody hack closes the template file (MANIFEST.in) if an - # exception occurs during read_template. - # Doing so prevents an error when easy_install attempts to delete the - # file. - try: - orig.sdist.read_template(self) - except Exception: - _, _, tb = sys.exc_info() - tb.tb_next.tb_frame.f_locals['template'].close() - raise - - # Beginning with Python 2.7.2, 3.1.4, and 3.2.1, this leaky file handle - # has been fixed, so only override the method if we're using an earlier - # Python. - has_leaky_handle = ( - sys.version_info < (2, 7, 2) - or (3, 0) <= sys.version_info < (3, 1, 4) - or (3, 2) <= sys.version_info < (3, 2, 1) - ) - if has_leaky_handle: - read_template = __read_template_hack + def add_defaults(self): + super().add_defaults() + self._add_defaults_build_sub_commands() def _add_defaults_optional(self): - if six.PY2: - sdist_add_defaults._add_defaults_optional(self) - else: - super()._add_defaults_optional() + super()._add_defaults_optional() if os.path.isfile('pyproject.toml'): self.filelist.append('pyproject.toml') @@ -136,14 +114,25 @@ class sdist(sdist_add_defaults, orig.sdist): self.filelist.extend(build_py.get_source_files()) self._add_data_files(self._safe_data_files(build_py)) + def _add_defaults_build_sub_commands(self): + build = self.get_finalized_command("build") + missing_cmds = set(build.get_sub_commands()) - _ORIGINAL_SUBCOMMANDS + # ^-- the original built-in sub-commands are already handled by default. + cmds = (self.get_finalized_command(c) for c in missing_cmds) + files = (c.get_source_files() for c in cmds if hasattr(c, "get_source_files")) + self.filelist.extend(chain.from_iterable(files)) + def _safe_data_files(self, build_py): """ - Extracting data_files from build_py is known to cause - infinite recursion errors when `include_package_data` - is enabled, so suppress it in that case. + Since the ``sdist`` class is also used to compute the MANIFEST + (via :obj:`setuptools.command.egg_info.manifest_maker`), + there might be recursion problems when trying to obtain the list of + data_files and ``include_package_data=True`` (which in turn depends on + the files included in the MANIFEST). + + To avoid that, ``manifest_maker`` should be able to overwrite this + method and avoid recursive attempts to build/analyze the MANIFEST. """ - if self.distribution.include_package_data: - return () return build_py.data_files def _add_data_files(self, data_files): @@ -158,10 +147,7 @@ class sdist(sdist_add_defaults, orig.sdist): def _add_defaults_data_files(self): try: - if six.PY2: - sdist_add_defaults._add_defaults_data_files(self) - else: - super()._add_defaults_data_files() + super()._add_defaults_data_files() except TypeError: log.warn("data_files contains unexpected objects") @@ -171,8 +157,7 @@ class sdist(sdist_add_defaults, orig.sdist): return else: self.warn( - "standard file not found: should have one of " + - ', '.join(self.READMES) + "standard file not found: should have one of " + ', '.join(self.READMES) ) def make_release_tree(self, base_dir, files): @@ -193,10 +178,9 @@ class sdist(sdist_add_defaults, orig.sdist): if not os.path.isfile(self.manifest): return False - with io.open(self.manifest, 'rb') as fp: + with open(self.manifest, 'rb') as fp: first_line = fp.readline() - return (first_line != - '# file GENERATED by distutils, do NOT edit\n'.encode()) + return first_line != b'# file GENERATED by distutils, do NOT edit\n' def read_manifest(self): """Read the manifest file (named by 'self.manifest') and use it to @@ -207,46 +191,14 @@ class sdist(sdist_add_defaults, orig.sdist): manifest = open(self.manifest, 'rb') for line in manifest: # The manifest must contain UTF-8. See #303. - if not six.PY2: - try: - line = line.decode('UTF-8') - except UnicodeDecodeError: - log.warn("%r not UTF-8 decodable -- skipping" % line) - continue + try: + line = line.decode('UTF-8') + except UnicodeDecodeError: + log.warn("%r not UTF-8 decodable -- skipping" % line) + continue # ignore comments and blank lines line = line.strip() if line.startswith('#') or not line: continue self.filelist.append(line) manifest.close() - - def check_license(self): - """Checks if license_file' or 'license_files' is configured and adds any - valid paths to 'self.filelist'. - """ - - files = ordered_set.OrderedSet() - - opts = self.distribution.get_option_dict('metadata') - - # ignore the source of the value - _, license_file = opts.get('license_file', (None, None)) - - if license_file is None: - log.debug("'license_file' option was not specified") - else: - files.add(license_file) - - try: - files.update(self.distribution.metadata.license_files) - except TypeError: - log.warn("warning: 'license_files' option is malformed") - - for f in files: - if not os.path.exists(f): - log.warn( - "warning: Failed to find the configured license file '%s'", - f) - files.remove(f) - - self.filelist.extend(files) diff --git a/libs/common/setuptools/command/setopt.py b/libs/common/setuptools/command/setopt.py index 7e57cc02..f9a60751 100644 --- a/libs/common/setuptools/command/setopt.py +++ b/libs/common/setuptools/command/setopt.py @@ -3,8 +3,7 @@ from distutils import log from distutils.errors import DistutilsOptionError import distutils import os - -from setuptools.extern.six.moves import configparser +import configparser from setuptools import Command @@ -19,15 +18,11 @@ def config_file(kind="local"): if kind == 'local': return 'setup.cfg' if kind == 'global': - return os.path.join( - os.path.dirname(distutils.__file__), 'distutils.cfg' - ) + return os.path.join(os.path.dirname(distutils.__file__), 'distutils.cfg') if kind == 'user': dot = os.name == 'posix' and '.' or '' return os.path.expanduser(convert_path("~/%spydistutils.cfg" % dot)) - raise ValueError( - "config_file() type must be 'local', 'global', or 'user'", kind - ) + raise ValueError("config_file() type must be 'local', 'global', or 'user'", kind) def edit_config(filename, settings, dry_run=False): @@ -40,6 +35,7 @@ def edit_config(filename, settings, dry_run=False): """ log.debug("Reading configuration from %s", filename) opts = configparser.RawConfigParser() + opts.optionxform = lambda x: x opts.read([filename]) for section, options in settings.items(): if options is None: @@ -51,19 +47,16 @@ def edit_config(filename, settings, dry_run=False): opts.add_section(section) for option, value in options.items(): if value is None: - log.debug( - "Deleting %s.%s from %s", - section, option, filename - ) + log.debug("Deleting %s.%s from %s", section, option, filename) opts.remove_option(section, option) if not opts.options(section): - log.info("Deleting empty [%s] section from %s", - section, filename) + log.info( + "Deleting empty [%s] section from %s", section, filename + ) opts.remove_section(section) else: log.debug( - "Setting %s.%s to %r in %s", - section, option, value, filename + "Setting %s.%s to %r in %s", section, option, value, filename ) opts.set(section, option, value) @@ -77,16 +70,14 @@ class option_base(Command): """Abstract base class for commands that mess with config files""" user_options = [ - ('global-config', 'g', - "save options to the site-wide distutils.cfg file"), - ('user-config', 'u', - "save options to the current user's pydistutils.cfg file"), - ('filename=', 'f', - "configuration file to use (default=setup.cfg)"), + ('global-config', 'g', "save options to the site-wide distutils.cfg file"), + ('user-config', 'u', "save options to the current user's pydistutils.cfg file"), + ('filename=', 'f', "configuration file to use (default=setup.cfg)"), ] boolean_options = [ - 'global-config', 'user-config', + 'global-config', + 'user-config', ] def initialize_options(self): @@ -106,10 +97,9 @@ class option_base(Command): filenames.append(config_file('local')) if len(filenames) > 1: raise DistutilsOptionError( - "Must specify only one configuration file option", - filenames + "Must specify only one configuration file option", filenames ) - self.filename, = filenames + (self.filename,) = filenames class setopt(option_base): @@ -142,8 +132,7 @@ class setopt(option_base): def run(self): edit_config( - self.filename, { - self.command: {self.option.replace('-', '_'): self.set_value} - }, - self.dry_run + self.filename, + {self.command: {self.option.replace('-', '_'): self.set_value}}, + self.dry_run, ) diff --git a/libs/common/setuptools/command/test.py b/libs/common/setuptools/command/test.py index f6470e9c..0a128f2a 100644 --- a/libs/common/setuptools/command/test.py +++ b/libs/common/setuptools/command/test.py @@ -8,20 +8,22 @@ from distutils.errors import DistutilsError, DistutilsOptionError from distutils import log from unittest import TestLoader -from setuptools.extern import six -from setuptools.extern.six.moves import map, filter - -from pkg_resources import (resource_listdir, resource_exists, normalize_path, - working_set, _namespace_packages, evaluate_marker, - add_activation_listener, require, EntryPoint) +from pkg_resources import ( + resource_listdir, + resource_exists, + normalize_path, + working_set, + evaluate_marker, + add_activation_listener, + require, +) +from .._importlib import metadata from setuptools import Command -from .build_py import _unique_everseen - -__metaclass__ = type +from setuptools.extern.more_itertools import unique_everseen +from setuptools.extern.jaraco.functools import pass_none class ScanningLoader(TestLoader): - def __init__(self): TestLoader.__init__(self) self._visited = set() @@ -78,8 +80,11 @@ class test(Command): user_options = [ ('test-module=', 'm', "Run 'test_suite' in specified module"), - ('test-suite=', 's', - "Run single test, case or suite (e.g. 'module.test_suite')"), + ( + 'test-suite=', + 's', + "Run single test, case or suite (e.g. 'module.test_suite')", + ), ('test-runner=', 'r', "Test runner to use"), ] @@ -90,7 +95,6 @@ class test(Command): self.test_runner = None def finalize_options(self): - if self.test_suite and self.test_module: msg = "You may specify a module or a suite, but not both" raise DistutilsOptionError(msg) @@ -113,7 +117,7 @@ class test(Command): return list(self._test_args()) def _test_args(self): - if not self.test_suite and sys.version_info >= (2, 7): + if not self.test_suite: yield 'discover' if self.verbose: yield '--verbose' @@ -128,31 +132,12 @@ class test(Command): func() @contextlib.contextmanager - def project_on_sys_path(self, include_dists=[]): - with_2to3 = not six.PY2 and getattr(self.distribution, 'use_2to3', False) + def project_on_sys_path(self, include_dists=()): + self.run_command('egg_info') - if with_2to3: - # If we run 2to3 we can not do this inplace: - - # Ensure metadata is up-to-date - self.reinitialize_command('build_py', inplace=0) - self.run_command('build_py') - bpy_cmd = self.get_finalized_command("build_py") - build_path = normalize_path(bpy_cmd.build_lib) - - # Build extensions - self.reinitialize_command('egg_info', egg_base=build_path) - self.run_command('egg_info') - - self.reinitialize_command('build_ext', inplace=0) - self.run_command('build_ext') - else: - # Without 2to3 inplace works fine: - self.run_command('egg_info') - - # Build extensions in-place - self.reinitialize_command('build_ext', inplace=1) - self.run_command('build_ext') + # Build extensions in-place + self.reinitialize_command('build_ext', inplace=1) + self.run_command('build_ext') ei_cmd = self.get_finalized_command("egg_info") @@ -187,7 +172,7 @@ class test(Command): orig_pythonpath = os.environ.get('PYTHONPATH', nothing) current_pythonpath = os.environ.get('PYTHONPATH', '') try: - prefix = os.pathsep.join(_unique_everseen(paths)) + prefix = os.pathsep.join(unique_everseen(paths)) to_join = filter(None, [prefix, current_pythonpath]) new_path = os.pathsep.join(to_join) if new_path: @@ -208,7 +193,8 @@ class test(Command): ir_d = dist.fetch_build_eggs(dist.install_requires) tr_d = dist.fetch_build_eggs(dist.tests_require or []) er_d = dist.fetch_build_eggs( - v for k, v in dist.extras_require.items() + v + for k, v in dist.extras_require.items() if k.startswith(':') and evaluate_marker(k[1:]) ) return itertools.chain(ir_d, tr_d, er_d) @@ -237,23 +223,10 @@ class test(Command): self.run_tests() def run_tests(self): - # Purge modules under test from sys.modules. The test loader will - # re-import them from the build location. Required when 2to3 is used - # with namespace packages. - if not six.PY2 and getattr(self.distribution, 'use_2to3', False): - module = self.test_suite.split('.')[0] - if module in _namespace_packages: - del_modules = [] - if module in sys.modules: - del_modules.append(module) - module += '.' - for name in sys.modules: - if name.startswith(module): - del_modules.append(name) - list(map(sys.modules.__delitem__, del_modules)) - test = unittest.main( - None, None, self._argv, + None, + None, + self._argv, testLoader=self._resolve_as_ep(self.test_loader), testRunner=self._resolve_as_ep(self.test_runner), exit=False, @@ -268,12 +241,10 @@ class test(Command): return ['unittest'] + self.test_args @staticmethod + @pass_none def _resolve_as_ep(val): """ Load the indicated attribute value, called, as a as if it were specified as an entry point. """ - if val is None: - return - parsed = EntryPoint.parse("x=" + val) - return parsed.resolve()() + return metadata.EntryPoint(value=val, name=None, group=None).load()() diff --git a/libs/common/setuptools/command/upload_docs.py b/libs/common/setuptools/command/upload_docs.py index 130a0cb6..32c9abd7 100644 --- a/libs/common/setuptools/command/upload_docs.py +++ b/libs/common/setuptools/command/upload_docs.py @@ -1,31 +1,29 @@ -# -*- coding: utf-8 -*- """upload_docs Implements a Distutils 'upload_docs' subcommand (upload documentation to -PyPI's pythonhosted.org). +sites other than PyPi such as devpi). """ from base64 import standard_b64encode from distutils import log from distutils.errors import DistutilsOptionError import os -import socket import zipfile import tempfile import shutil import itertools import functools +import http.client +import urllib.parse -from setuptools.extern import six -from setuptools.extern.six.moves import http_client, urllib +from .._importlib import metadata +from ..warnings import SetuptoolsDeprecationWarning -from pkg_resources import iter_entry_points from .upload import upload def _encode(s): - errors = 'strict' if six.PY2 else 'surrogateescape' - return s.encode('utf-8', errors) + return s.encode('utf-8', 'surrogateescape') class upload_docs(upload): @@ -33,21 +31,24 @@ class upload_docs(upload): # supported by Warehouse (and won't be). DEFAULT_REPOSITORY = 'https://pypi.python.org/pypi/' - description = 'Upload documentation to PyPI' + description = 'Upload documentation to sites other than PyPi such as devpi' user_options = [ - ('repository=', 'r', - "url of repository [default: %s]" % upload.DEFAULT_REPOSITORY), - ('show-response', None, - 'display full response text from server'), + ( + 'repository=', + 'r', + "url of repository [default: %s]" % upload.DEFAULT_REPOSITORY, + ), + ('show-response', None, 'display full response text from server'), ('upload-dir=', None, 'directory to upload'), ] boolean_options = upload.boolean_options def has_sphinx(self): - if self.upload_dir is None: - for ep in iter_entry_points('distutils.commands', 'build_sphinx'): - return True + return bool( + self.upload_dir is None + and metadata.entry_points(group='distutils.commands', name='build_sphinx') + ) sub_commands = [('build_sphinx', has_sphinx)] @@ -57,19 +58,21 @@ class upload_docs(upload): self.target_dir = None def finalize_options(self): + log.warn( + "Upload_docs command is deprecated. Use Read the Docs " + "(https://readthedocs.org) instead." + ) upload.finalize_options(self) if self.upload_dir is None: if self.has_sphinx(): build_sphinx = self.get_finalized_command('build_sphinx') - self.target_dir = build_sphinx.builder_target_dir + self.target_dir = dict(build_sphinx.builder_target_dirs)['html'] else: build = self.get_finalized_command('build') self.target_dir = os.path.join(build.build_base, 'docs') else: self.ensure_dirname('upload_dir') self.target_dir = self.upload_dir - if 'pypi.python.org' in self.repository: - log.warn("Upload_docs command is deprecated. Use RTD instead.") self.announce('Using upload directory %s' % self.target_dir) def create_zipfile(self, filename): @@ -82,13 +85,23 @@ class upload_docs(upload): raise DistutilsOptionError(tmpl % self.target_dir) for name in files: full = os.path.join(root, name) - relative = root[len(self.target_dir):].lstrip(os.path.sep) + relative = root[len(self.target_dir) :].lstrip(os.path.sep) dest = os.path.join(relative, name) zip_file.write(full, dest) finally: zip_file.close() def run(self): + SetuptoolsDeprecationWarning.emit( + "Deprecated command", + """ + upload_docs is deprecated and will be removed in a future version. + Instead, use tools like devpi and Read the Docs; or lower level tools like + httpie and curl to interact directly with your hosting service API. + """, + due_date=(2023, 9, 26), # warning introduced in 27 Jul 2022 + ) + # Run sub commands for cmd_name in self.get_sub_commands(): self.run_command(cmd_name) @@ -127,10 +140,13 @@ class upload_docs(upload): """ Build up the MIME payload for the POST data """ - boundary = b'--------------GHSKFJDLGDS7543FJKLFHRE75642756743254' - sep_boundary = b'\n--' + boundary + boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254' + sep_boundary = b'\n--' + boundary.encode('ascii') end_boundary = sep_boundary + b'--' - end_items = end_boundary, b"\n", + end_items = ( + end_boundary, + b"\n", + ) builder = functools.partial( cls._build_part, sep_boundary=sep_boundary, @@ -138,7 +154,7 @@ class upload_docs(upload): part_groups = map(builder, data.items()) parts = itertools.chain.from_iterable(part_groups) body_items = itertools.chain(parts, end_items) - content_type = 'multipart/form-data; boundary=%s' % boundary.decode('ascii') + content_type = 'multipart/form-data; boundary=%s' % boundary return b''.join(body_items), content_type def upload_file(self, filename): @@ -152,9 +168,7 @@ class upload_docs(upload): } # set up the authentication credentials = _encode(self.username + ':' + self.password) - credentials = standard_b64encode(credentials) - if not six.PY2: - credentials = credentials.decode('ascii') + credentials = standard_b64encode(credentials).decode('ascii') auth = "Basic " + credentials body, ct = self._build_multipart(data) @@ -165,13 +179,14 @@ class upload_docs(upload): # build the Request # We can't use urllib2 since we need to send the Basic # auth right with the first request - schema, netloc, url, params, query, fragments = \ - urllib.parse.urlparse(self.repository) + schema, netloc, url, params, query, fragments = urllib.parse.urlparse( + self.repository + ) assert not params and not query and not fragments if schema == 'http': - conn = http_client.HTTPConnection(netloc) + conn = http.client.HTTPConnection(netloc) elif schema == 'https': - conn = http_client.HTTPSConnection(netloc) + conn = http.client.HTTPSConnection(netloc) else: raise AssertionError("unsupported schema " + schema) @@ -185,7 +200,7 @@ class upload_docs(upload): conn.putheader('Authorization', auth) conn.endheaders() conn.send(body) - except socket.error as e: + except OSError as e: self.announce(str(e), log.ERROR) return diff --git a/libs/common/setuptools/config.py b/libs/common/setuptools/config.py deleted file mode 100644 index 9b9a0c45..00000000 --- a/libs/common/setuptools/config.py +++ /dev/null @@ -1,659 +0,0 @@ -from __future__ import absolute_import, unicode_literals -import io -import os -import sys - -import warnings -import functools -from collections import defaultdict -from functools import partial -from functools import wraps -from importlib import import_module - -from distutils.errors import DistutilsOptionError, DistutilsFileError -from setuptools.extern.packaging.version import LegacyVersion, parse -from setuptools.extern.packaging.specifiers import SpecifierSet -from setuptools.extern.six import string_types, PY3 - - -__metaclass__ = type - - -def read_configuration( - filepath, find_others=False, ignore_option_errors=False): - """Read given configuration file and returns options from it as a dict. - - :param str|unicode filepath: Path to configuration file - to get options from. - - :param bool find_others: Whether to search for other configuration files - which could be on in various places. - - :param bool ignore_option_errors: Whether to silently ignore - options, values of which could not be resolved (e.g. due to exceptions - in directives such as file:, attr:, etc.). - If False exceptions are propagated as expected. - - :rtype: dict - """ - from setuptools.dist import Distribution, _Distribution - - filepath = os.path.abspath(filepath) - - if not os.path.isfile(filepath): - raise DistutilsFileError( - 'Configuration file %s does not exist.' % filepath) - - current_directory = os.getcwd() - os.chdir(os.path.dirname(filepath)) - - try: - dist = Distribution() - - filenames = dist.find_config_files() if find_others else [] - if filepath not in filenames: - filenames.append(filepath) - - _Distribution.parse_config_files(dist, filenames=filenames) - - handlers = parse_configuration( - dist, dist.command_options, - ignore_option_errors=ignore_option_errors) - - finally: - os.chdir(current_directory) - - return configuration_to_dict(handlers) - - -def _get_option(target_obj, key): - """ - Given a target object and option key, get that option from - the target object, either through a get_{key} method or - from an attribute directly. - """ - getter_name = 'get_{key}'.format(**locals()) - by_attribute = functools.partial(getattr, target_obj, key) - getter = getattr(target_obj, getter_name, by_attribute) - return getter() - - -def configuration_to_dict(handlers): - """Returns configuration data gathered by given handlers as a dict. - - :param list[ConfigHandler] handlers: Handlers list, - usually from parse_configuration() - - :rtype: dict - """ - config_dict = defaultdict(dict) - - for handler in handlers: - for option in handler.set_options: - value = _get_option(handler.target_obj, option) - config_dict[handler.section_prefix][option] = value - - return config_dict - - -def parse_configuration( - distribution, command_options, ignore_option_errors=False): - """Performs additional parsing of configuration options - for a distribution. - - Returns a list of used option handlers. - - :param Distribution distribution: - :param dict command_options: - :param bool ignore_option_errors: Whether to silently ignore - options, values of which could not be resolved (e.g. due to exceptions - in directives such as file:, attr:, etc.). - If False exceptions are propagated as expected. - :rtype: list - """ - options = ConfigOptionsHandler( - distribution, command_options, ignore_option_errors) - options.parse() - - meta = ConfigMetadataHandler( - distribution.metadata, command_options, ignore_option_errors, - distribution.package_dir) - meta.parse() - - return meta, options - - -class ConfigHandler: - """Handles metadata supplied in configuration files.""" - - section_prefix = None - """Prefix for config sections handled by this handler. - Must be provided by class heirs. - - """ - - aliases = {} - """Options aliases. - For compatibility with various packages. E.g.: d2to1 and pbr. - Note: `-` in keys is replaced with `_` by config parser. - - """ - - def __init__(self, target_obj, options, ignore_option_errors=False): - sections = {} - - section_prefix = self.section_prefix - for section_name, section_options in options.items(): - if not section_name.startswith(section_prefix): - continue - - section_name = section_name.replace(section_prefix, '').strip('.') - sections[section_name] = section_options - - self.ignore_option_errors = ignore_option_errors - self.target_obj = target_obj - self.sections = sections - self.set_options = [] - - @property - def parsers(self): - """Metadata item name to parser function mapping.""" - raise NotImplementedError( - '%s must provide .parsers property' % self.__class__.__name__) - - def __setitem__(self, option_name, value): - unknown = tuple() - target_obj = self.target_obj - - # Translate alias into real name. - option_name = self.aliases.get(option_name, option_name) - - current_value = getattr(target_obj, option_name, unknown) - - if current_value is unknown: - raise KeyError(option_name) - - if current_value: - # Already inhabited. Skipping. - return - - skip_option = False - parser = self.parsers.get(option_name) - if parser: - try: - value = parser(value) - - except Exception: - skip_option = True - if not self.ignore_option_errors: - raise - - if skip_option: - return - - setter = getattr(target_obj, 'set_%s' % option_name, None) - if setter is None: - setattr(target_obj, option_name, value) - else: - setter(value) - - self.set_options.append(option_name) - - @classmethod - def _parse_list(cls, value, separator=','): - """Represents value as a list. - - Value is split either by separator (defaults to comma) or by lines. - - :param value: - :param separator: List items separator character. - :rtype: list - """ - if isinstance(value, list): # _get_parser_compound case - return value - - if '\n' in value: - value = value.splitlines() - else: - value = value.split(separator) - - return [chunk.strip() for chunk in value if chunk.strip()] - - @classmethod - def _parse_dict(cls, value): - """Represents value as a dict. - - :param value: - :rtype: dict - """ - separator = '=' - result = {} - for line in cls._parse_list(value): - key, sep, val = line.partition(separator) - if sep != separator: - raise DistutilsOptionError( - 'Unable to parse option value to dict: %s' % value) - result[key.strip()] = val.strip() - - return result - - @classmethod - def _parse_bool(cls, value): - """Represents value as boolean. - - :param value: - :rtype: bool - """ - value = value.lower() - return value in ('1', 'true', 'yes') - - @classmethod - def _exclude_files_parser(cls, key): - """Returns a parser function to make sure field inputs - are not files. - - Parses a value after getting the key so error messages are - more informative. - - :param key: - :rtype: callable - """ - def parser(value): - exclude_directive = 'file:' - if value.startswith(exclude_directive): - raise ValueError( - 'Only strings are accepted for the {0} field, ' - 'files are not accepted'.format(key)) - return value - return parser - - @classmethod - def _parse_file(cls, value): - """Represents value as a string, allowing including text - from nearest files using `file:` directive. - - Directive is sandboxed and won't reach anything outside - directory with setup.py. - - Examples: - file: README.rst, CHANGELOG.md, src/file.txt - - :param str value: - :rtype: str - """ - include_directive = 'file:' - - if not isinstance(value, string_types): - return value - - if not value.startswith(include_directive): - return value - - spec = value[len(include_directive):] - filepaths = (os.path.abspath(path.strip()) for path in spec.split(',')) - return '\n'.join( - cls._read_file(path) - for path in filepaths - if (cls._assert_local(path) or True) - and os.path.isfile(path) - ) - - @staticmethod - def _assert_local(filepath): - if not filepath.startswith(os.getcwd()): - raise DistutilsOptionError( - '`file:` directive can not access %s' % filepath) - - @staticmethod - def _read_file(filepath): - with io.open(filepath, encoding='utf-8') as f: - return f.read() - - @classmethod - def _parse_attr(cls, value, package_dir=None): - """Represents value as a module attribute. - - Examples: - attr: package.attr - attr: package.module.attr - - :param str value: - :rtype: str - """ - attr_directive = 'attr:' - if not value.startswith(attr_directive): - return value - - attrs_path = value.replace(attr_directive, '').strip().split('.') - attr_name = attrs_path.pop() - - module_name = '.'.join(attrs_path) - module_name = module_name or '__init__' - - parent_path = os.getcwd() - if package_dir: - if attrs_path[0] in package_dir: - # A custom path was specified for the module we want to import - custom_path = package_dir[attrs_path[0]] - parts = custom_path.rsplit('/', 1) - if len(parts) > 1: - parent_path = os.path.join(os.getcwd(), parts[0]) - module_name = parts[1] - else: - module_name = custom_path - elif '' in package_dir: - # A custom parent directory was specified for all root modules - parent_path = os.path.join(os.getcwd(), package_dir['']) - sys.path.insert(0, parent_path) - try: - module = import_module(module_name) - value = getattr(module, attr_name) - - finally: - sys.path = sys.path[1:] - - return value - - @classmethod - def _get_parser_compound(cls, *parse_methods): - """Returns parser function to represents value as a list. - - Parses a value applying given methods one after another. - - :param parse_methods: - :rtype: callable - """ - def parse(value): - parsed = value - - for method in parse_methods: - parsed = method(parsed) - - return parsed - - return parse - - @classmethod - def _parse_section_to_dict(cls, section_options, values_parser=None): - """Parses section options into a dictionary. - - Optionally applies a given parser to values. - - :param dict section_options: - :param callable values_parser: - :rtype: dict - """ - value = {} - values_parser = values_parser or (lambda val: val) - for key, (_, val) in section_options.items(): - value[key] = values_parser(val) - return value - - def parse_section(self, section_options): - """Parses configuration file section. - - :param dict section_options: - """ - for (name, (_, value)) in section_options.items(): - try: - self[name] = value - - except KeyError: - pass # Keep silent for a new option may appear anytime. - - def parse(self): - """Parses configuration file items from one - or more related sections. - - """ - for section_name, section_options in self.sections.items(): - - method_postfix = '' - if section_name: # [section.option] variant - method_postfix = '_%s' % section_name - - section_parser_method = getattr( - self, - # Dots in section names are translated into dunderscores. - ('parse_section%s' % method_postfix).replace('.', '__'), - None) - - if section_parser_method is None: - raise DistutilsOptionError( - 'Unsupported distribution option section: [%s.%s]' % ( - self.section_prefix, section_name)) - - section_parser_method(section_options) - - def _deprecated_config_handler(self, func, msg, warning_class): - """ this function will wrap around parameters that are deprecated - - :param msg: deprecation message - :param warning_class: class of warning exception to be raised - :param func: function to be wrapped around - """ - @wraps(func) - def config_handler(*args, **kwargs): - warnings.warn(msg, warning_class) - return func(*args, **kwargs) - - return config_handler - - -class ConfigMetadataHandler(ConfigHandler): - - section_prefix = 'metadata' - - aliases = { - 'home_page': 'url', - 'summary': 'description', - 'classifier': 'classifiers', - 'platform': 'platforms', - } - - strict_mode = False - """We need to keep it loose, to be partially compatible with - `pbr` and `d2to1` packages which also uses `metadata` section. - - """ - - def __init__(self, target_obj, options, ignore_option_errors=False, - package_dir=None): - super(ConfigMetadataHandler, self).__init__(target_obj, options, - ignore_option_errors) - self.package_dir = package_dir - - @property - def parsers(self): - """Metadata item name to parser function mapping.""" - parse_list = self._parse_list - parse_file = self._parse_file - parse_dict = self._parse_dict - exclude_files_parser = self._exclude_files_parser - - return { - 'platforms': parse_list, - 'keywords': parse_list, - 'provides': parse_list, - 'requires': self._deprecated_config_handler( - parse_list, - "The requires parameter is deprecated, please use " - "install_requires for runtime dependencies.", - DeprecationWarning), - 'obsoletes': parse_list, - 'classifiers': self._get_parser_compound(parse_file, parse_list), - 'license': exclude_files_parser('license'), - 'license_files': parse_list, - 'description': parse_file, - 'long_description': parse_file, - 'version': self._parse_version, - 'project_urls': parse_dict, - } - - def _parse_version(self, value): - """Parses `version` option value. - - :param value: - :rtype: str - - """ - version = self._parse_file(value) - - if version != value: - version = version.strip() - # Be strict about versions loaded from file because it's easy to - # accidentally include newlines and other unintended content - if isinstance(parse(version), LegacyVersion): - tmpl = ( - 'Version loaded from {value} does not ' - 'comply with PEP 440: {version}' - ) - raise DistutilsOptionError(tmpl.format(**locals())) - - return version - - version = self._parse_attr(value, self.package_dir) - - if callable(version): - version = version() - - if not isinstance(version, string_types): - if hasattr(version, '__iter__'): - version = '.'.join(map(str, version)) - else: - version = '%s' % version - - return version - - -class ConfigOptionsHandler(ConfigHandler): - - section_prefix = 'options' - - @property - def parsers(self): - """Metadata item name to parser function mapping.""" - parse_list = self._parse_list - parse_list_semicolon = partial(self._parse_list, separator=';') - parse_bool = self._parse_bool - parse_dict = self._parse_dict - - return { - 'zip_safe': parse_bool, - 'use_2to3': parse_bool, - 'include_package_data': parse_bool, - 'package_dir': parse_dict, - 'use_2to3_fixers': parse_list, - 'use_2to3_exclude_fixers': parse_list, - 'convert_2to3_doctests': parse_list, - 'scripts': parse_list, - 'eager_resources': parse_list, - 'dependency_links': parse_list, - 'namespace_packages': parse_list, - 'install_requires': parse_list_semicolon, - 'setup_requires': parse_list_semicolon, - 'tests_require': parse_list_semicolon, - 'packages': self._parse_packages, - 'entry_points': self._parse_file, - 'py_modules': parse_list, - 'python_requires': SpecifierSet, - } - - def _parse_packages(self, value): - """Parses `packages` option value. - - :param value: - :rtype: list - """ - find_directives = ['find:', 'find_namespace:'] - trimmed_value = value.strip() - - if trimmed_value not in find_directives: - return self._parse_list(value) - - findns = trimmed_value == find_directives[1] - if findns and not PY3: - raise DistutilsOptionError( - 'find_namespace: directive is unsupported on Python < 3.3') - - # Read function arguments from a dedicated section. - find_kwargs = self.parse_section_packages__find( - self.sections.get('packages.find', {})) - - if findns: - from setuptools import find_namespace_packages as find_packages - else: - from setuptools import find_packages - - return find_packages(**find_kwargs) - - def parse_section_packages__find(self, section_options): - """Parses `packages.find` configuration file section. - - To be used in conjunction with _parse_packages(). - - :param dict section_options: - """ - section_data = self._parse_section_to_dict( - section_options, self._parse_list) - - valid_keys = ['where', 'include', 'exclude'] - - find_kwargs = dict( - [(k, v) for k, v in section_data.items() if k in valid_keys and v]) - - where = find_kwargs.get('where') - if where is not None: - find_kwargs['where'] = where[0] # cast list to single val - - return find_kwargs - - def parse_section_entry_points(self, section_options): - """Parses `entry_points` configuration file section. - - :param dict section_options: - """ - parsed = self._parse_section_to_dict(section_options, self._parse_list) - self['entry_points'] = parsed - - def _parse_package_data(self, section_options): - parsed = self._parse_section_to_dict(section_options, self._parse_list) - - root = parsed.get('*') - if root: - parsed[''] = root - del parsed['*'] - - return parsed - - def parse_section_package_data(self, section_options): - """Parses `package_data` configuration file section. - - :param dict section_options: - """ - self['package_data'] = self._parse_package_data(section_options) - - def parse_section_exclude_package_data(self, section_options): - """Parses `exclude_package_data` configuration file section. - - :param dict section_options: - """ - self['exclude_package_data'] = self._parse_package_data( - section_options) - - def parse_section_extras_require(self, section_options): - """Parses `extras_require` configuration file section. - - :param dict section_options: - """ - parse_list = partial(self._parse_list, separator=';') - self['extras_require'] = self._parse_section_to_dict( - section_options, parse_list) - - def parse_section_data_files(self, section_options): - """Parses `data_files` configuration file section. - - :param dict section_options: - """ - parsed = self._parse_section_to_dict(section_options, self._parse_list) - self['data_files'] = [(k, v) for k, v in parsed.items()] diff --git a/libs/common/setuptools/dep_util.py b/libs/common/setuptools/dep_util.py index 2931c13e..998ffa20 100644 --- a/libs/common/setuptools/dep_util.py +++ b/libs/common/setuptools/dep_util.py @@ -1,23 +1,16 @@ -from distutils.dep_util import newer_group +from ._distutils import _modified +from .warnings import SetuptoolsDeprecationWarning -# yes, this is was almost entirely copy-pasted from -# 'newer_pairwise()', this is just another convenience -# function. -def newer_pairwise_group(sources_groups, targets): - """Walk both arguments in parallel, testing if each source group is newer - than its corresponding target. Returns a pair of lists (sources_groups, - targets) where sources is newer than target, according to the semantics - of 'newer_group()'. - """ - if len(sources_groups) != len(targets): - raise ValueError("'sources_group' and 'targets' must be the same length") - # build a pair of lists (sources_groups, targets) where source is newer - n_sources = [] - n_targets = [] - for i in range(len(sources_groups)): - if newer_group(sources_groups[i], targets[i]): - n_sources.append(sources_groups[i]) - n_targets.append(targets[i]) - - return n_sources, n_targets +def __getattr__(name): + if name not in ['newer_group', 'newer_pairwise_group']: + raise AttributeError(name) + SetuptoolsDeprecationWarning.emit( + "dep_util is Deprecated. Use functions from setuptools.modified instead.", + "Please use `setuptools.modified` instead of `setuptools.dep_util`.", + see_url="https://github.com/pypa/setuptools/pull/4069", + due_date=(2024, 5, 21), + # Warning added in v69.0.0 on 2023/11/20, + # See https://github.com/pypa/setuptools/discussions/4128 + ) + return getattr(_modified, name) diff --git a/libs/common/setuptools/depends.py b/libs/common/setuptools/depends.py index a37675cb..c0ca84d4 100644 --- a/libs/common/setuptools/depends.py +++ b/libs/common/setuptools/depends.py @@ -1,28 +1,25 @@ import sys import marshal import contextlib -from distutils.version import StrictVersion - -from .py33compat import Bytecode - -from .py27compat import find_module, PY_COMPILED, PY_FROZEN, PY_SOURCE -from . import py27compat +import dis -__all__ = [ - 'Require', 'find_module', 'get_module_constant', 'extract_constant' -] +from . import _imp +from ._imp import find_module, PY_COMPILED, PY_FROZEN, PY_SOURCE +from .extern.packaging.version import Version + + +__all__ = ['Require', 'find_module', 'get_module_constant', 'extract_constant'] class Require: """A prerequisite to building or installing a distribution""" def __init__( - self, name, requested_version, module, homepage='', - attribute=None, format=None): - + self, name, requested_version, module, homepage='', attribute=None, format=None + ): if format is None and requested_version is not None: - format = StrictVersion + format = Version if format is not None: requested_version = format(requested_version) @@ -40,8 +37,12 @@ class Require: def version_ok(self, version): """Is 'version' sufficiently up-to-date?""" - return self.attribute is None or self.format is None or \ - str(version) != "unknown" and version >= self.requested_version + return ( + self.attribute is None + or self.format is None + or str(version) != "unknown" + and self.format(version) >= self.requested_version + ) def get_version(self, paths=None, default="unknown"): """Get version number of installed module, 'None', or 'default' @@ -79,7 +80,7 @@ class Require: version = self.get_version(paths) if version is None: return False - return self.version_ok(version) + return self.version_ok(str(version)) def maybe_close(f): @@ -87,6 +88,7 @@ def maybe_close(f): def empty(): yield return + if not f: return empty() @@ -111,12 +113,12 @@ def get_module_constant(module, symbol, default=-1, paths=None): f.read(8) # skip magic & date code = marshal.load(f) elif kind == PY_FROZEN: - code = py27compat.get_frozen_object(module, paths) + code = _imp.get_frozen_object(module, paths) elif kind == PY_SOURCE: code = compile(f.read(), path, 'exec') else: # Not something we can parse; we'll have to import it. :( - imported = py27compat.get_module(module, paths, info) + imported = _imp.get_module(module, paths, info) return getattr(imported, symbol, None) return extract_constant(code, symbol, default) @@ -140,13 +142,13 @@ def extract_constant(code, symbol, default=-1): name_idx = list(code.co_names).index(symbol) - STORE_NAME = 90 - STORE_GLOBAL = 97 - LOAD_CONST = 100 + STORE_NAME = dis.opmap['STORE_NAME'] + STORE_GLOBAL = dis.opmap['STORE_GLOBAL'] + LOAD_CONST = dis.opmap['LOAD_CONST'] const = default - for byte_code in Bytecode(code): + for byte_code in dis.Bytecode(code): op = byte_code.opcode arg = byte_code.arg @@ -157,6 +159,8 @@ def extract_constant(code, symbol, default=-1): else: const = default + return None + def _update_globals(): """ diff --git a/libs/common/setuptools/dist.py b/libs/common/setuptools/dist.py index f6453a08..d5787ed4 100644 --- a/libs/common/setuptools/dist.py +++ b/libs/common/setuptools/dist.py @@ -1,202 +1,42 @@ -# -*- coding: utf-8 -*- __all__ = ['Distribution'] + import io -import sys -import re -import os -import warnings -import numbers -import distutils.log -import distutils.core -import distutils.cmd -import distutils.dist -from distutils.util import strtobool -from distutils.debug import DEBUG -from distutils.fancy_getopt import translate_longopt import itertools +import numbers +import os +import re +import sys +from contextlib import suppress +from glob import iglob +from pathlib import Path +from typing import List, Optional, Set -from collections import defaultdict -from email import message_from_file +import distutils.cmd +import distutils.command +import distutils.core +import distutils.dist +import distutils.log +from distutils.debug import DEBUG +from distutils.errors import DistutilsOptionError, DistutilsSetupError +from distutils.fancy_getopt import translate_longopt +from distutils.util import strtobool -from distutils.errors import ( - DistutilsOptionError, DistutilsPlatformError, DistutilsSetupError, -) -from distutils.util import rfc822_escape -from distutils.version import StrictVersion +from .extern.more_itertools import partition, unique_everseen +from .extern.ordered_set import OrderedSet +from .extern.packaging.markers import InvalidMarker, Marker +from .extern.packaging.specifiers import InvalidSpecifier, SpecifierSet +from .extern.packaging.version import Version -from setuptools.extern import six -from setuptools.extern import packaging -from setuptools.extern import ordered_set -from setuptools.extern.six.moves import map, filter, filterfalse - -from . import SetuptoolsDeprecationWarning - -from setuptools.depends import Require -from setuptools import windows_support -from setuptools.monkey import get_unpatched -from setuptools.config import parse_configuration -import pkg_resources - -__import__('setuptools.extern.packaging.specifiers') -__import__('setuptools.extern.packaging.version') - - -def _get_unpatched(cls): - warnings.warn("Do not call this function", DistDeprecationWarning) - return get_unpatched(cls) - - -def get_metadata_version(self): - mv = getattr(self, 'metadata_version', None) - - if mv is None: - if self.long_description_content_type or self.provides_extras: - mv = StrictVersion('2.1') - elif (self.maintainer is not None or - self.maintainer_email is not None or - getattr(self, 'python_requires', None) is not None or - self.project_urls): - mv = StrictVersion('1.2') - elif (self.provides or self.requires or self.obsoletes or - self.classifiers or self.download_url): - mv = StrictVersion('1.1') - else: - mv = StrictVersion('1.0') - - self.metadata_version = mv - - return mv - - -def read_pkg_file(self, file): - """Reads the metadata values from a file object.""" - msg = message_from_file(file) - - def _read_field(name): - value = msg[name] - if value == 'UNKNOWN': - return None - return value - - def _read_list(name): - values = msg.get_all(name, None) - if values == []: - return None - return values - - self.metadata_version = StrictVersion(msg['metadata-version']) - self.name = _read_field('name') - self.version = _read_field('version') - self.description = _read_field('summary') - # we are filling author only. - self.author = _read_field('author') - self.maintainer = None - self.author_email = _read_field('author-email') - self.maintainer_email = None - self.url = _read_field('home-page') - self.license = _read_field('license') - - if 'download-url' in msg: - self.download_url = _read_field('download-url') - else: - self.download_url = None - - self.long_description = _read_field('description') - self.description = _read_field('summary') - - if 'keywords' in msg: - self.keywords = _read_field('keywords').split(',') - - self.platforms = _read_list('platform') - self.classifiers = _read_list('classifier') - - # PEP 314 - these fields only exist in 1.1 - if self.metadata_version == StrictVersion('1.1'): - self.requires = _read_list('requires') - self.provides = _read_list('provides') - self.obsoletes = _read_list('obsoletes') - else: - self.requires = None - self.provides = None - self.obsoletes = None - - -# Based on Python 3.5 version -def write_pkg_file(self, file): - """Write the PKG-INFO format data to a file object. - """ - version = self.get_metadata_version() - - if six.PY2: - def write_field(key, value): - file.write("%s: %s\n" % (key, self._encode_field(value))) - else: - def write_field(key, value): - file.write("%s: %s\n" % (key, value)) - - write_field('Metadata-Version', str(version)) - write_field('Name', self.get_name()) - write_field('Version', self.get_version()) - write_field('Summary', self.get_description()) - write_field('Home-page', self.get_url()) - - if version < StrictVersion('1.2'): - write_field('Author', self.get_contact()) - write_field('Author-email', self.get_contact_email()) - else: - optional_fields = ( - ('Author', 'author'), - ('Author-email', 'author_email'), - ('Maintainer', 'maintainer'), - ('Maintainer-email', 'maintainer_email'), - ) - - for field, attr in optional_fields: - attr_val = getattr(self, attr) - - if attr_val is not None: - write_field(field, attr_val) - - write_field('License', self.get_license()) - if self.download_url: - write_field('Download-URL', self.download_url) - for project_url in self.project_urls.items(): - write_field('Project-URL', '%s, %s' % project_url) - - long_desc = rfc822_escape(self.get_long_description()) - write_field('Description', long_desc) - - keywords = ','.join(self.get_keywords()) - if keywords: - write_field('Keywords', keywords) - - if version >= StrictVersion('1.2'): - for platform in self.get_platforms(): - write_field('Platform', platform) - else: - self._write_list(file, 'Platform', self.get_platforms()) - - self._write_list(file, 'Classifier', self.get_classifiers()) - - # PEP 314 - self._write_list(file, 'Requires', self.get_requires()) - self._write_list(file, 'Provides', self.get_provides()) - self._write_list(file, 'Obsoletes', self.get_obsoletes()) - - # Setuptools specific for PEP 345 - if hasattr(self, 'python_requires'): - write_field('Requires-Python', self.python_requires) - - # PEP 566 - if self.long_description_content_type: - write_field( - 'Description-Content-Type', - self.long_description_content_type - ) - if self.provides_extras: - for extra in self.provides_extras: - write_field('Provides-Extra', extra) +from . import _entry_points +from . import _normalization +from . import _reqs +from . import command as _ # noqa -- imported for side-effects +from ._importlib import metadata +from .config import setupcfg, pyprojecttoml +from .discovery import ConfigDiscovery +from .monkey import get_unpatched +from .warnings import InformationOnly, SetuptoolsDeprecationWarning sequence = tuple, list @@ -204,13 +44,12 @@ sequence = tuple, list def check_importable(dist, attr, value): try: - ep = pkg_resources.EntryPoint.parse('x=' + value) + ep = metadata.EntryPoint(value=value, name=None, group=None) assert not ep.extras - except (TypeError, ValueError, AttributeError, AssertionError): + except (TypeError, ValueError, AttributeError, AssertionError) as e: raise DistutilsSetupError( - "%r must be importable 'module:attrs' string (got %r)" - % (attr, value) - ) + "%r must be importable 'module:attrs' string (got %r)" % (attr, value) + ) from e def assert_string_list(dist, attr, value): @@ -221,10 +60,10 @@ def assert_string_list(dist, attr, value): assert isinstance(value, (list, tuple)) # verify that elements of value are strings assert ''.join(value) != value - except (TypeError, ValueError, AttributeError, AssertionError): + except (TypeError, ValueError, AttributeError, AssertionError) as e: raise DistutilsSetupError( "%r must be a list of strings (got %r)" % (attr, value) - ) + ) from e def check_nsp(dist, attr, value): @@ -234,34 +73,55 @@ def check_nsp(dist, attr, value): for nsp in ns_packages: if not dist.has_contents_for(nsp): raise DistutilsSetupError( - "Distribution contains no modules or packages for " + - "namespace package %r" % nsp + "Distribution contains no modules or packages for " + + "namespace package %r" % nsp ) parent, sep, child = nsp.rpartition('.') if parent and parent not in ns_packages: distutils.log.warn( "WARNING: %r is declared as a package namespace, but %r" - " is not: please correct this in setup.py", nsp, parent + " is not: please correct this in setup.py", + nsp, + parent, ) + SetuptoolsDeprecationWarning.emit( + "The namespace_packages parameter is deprecated.", + "Please replace its usage with implicit namespaces (PEP 420).", + see_docs="references/keywords.html#keyword-namespace-packages", + # TODO: define due_date, it may break old packages that are no longer + # maintained (e.g. sphinxcontrib extensions) when installed from source. + # Warning officially introduced in May 2022, however the deprecation + # was mentioned much earlier in the docs (May 2020, see #2149). + ) def check_extras(dist, attr, value): """Verify that extras_require mapping is valid""" try: list(itertools.starmap(_check_extra, value.items())) - except (TypeError, ValueError, AttributeError): + except (TypeError, ValueError, AttributeError) as e: raise DistutilsSetupError( "'extras_require' must be a dictionary whose values are " "strings or lists of strings containing valid project/version " "requirement specifiers." - ) + ) from e def _check_extra(extra, reqs): name, sep, marker = extra.partition(':') - if marker and pkg_resources.invalid_marker(marker): - raise DistutilsSetupError("Invalid environment marker: " + marker) - list(pkg_resources.parse_requirements(reqs)) + try: + _check_marker(marker) + except InvalidMarker: + msg = f"Invalid environment marker: {marker} ({extra!r})" + raise DistutilsSetupError(msg) from None + list(_reqs.parse(reqs)) + + +def _check_marker(marker): + if not marker: + return + m = Marker(marker) + m.evaluate() def assert_bool(dist, attr, value): @@ -271,10 +131,18 @@ def assert_bool(dist, attr, value): raise DistutilsSetupError(tmpl.format(attr=attr, value=value)) +def invalid_unless_false(dist, attr, value): + if not value: + DistDeprecationWarning.emit(f"{attr} is ignored.") + # TODO: should there be a `due_date` here? + return + raise DistutilsSetupError(f"{attr} is invalid.") + + def check_requirements(dist, attr, value): """Verify that install_requires is a valid requirements list""" try: - list(pkg_resources.parse_requirements(value)) + list(_reqs.parse(value)) if isinstance(value, (dict, set)): raise TypeError("Unordered types are not allowed") except (TypeError, ValueError) as error: @@ -282,31 +150,30 @@ def check_requirements(dist, attr, value): "{attr!r} must be a string or list of strings " "containing valid project/version requirement specifiers; {error}" ) - raise DistutilsSetupError(tmpl.format(attr=attr, error=error)) + raise DistutilsSetupError(tmpl.format(attr=attr, error=error)) from error def check_specifier(dist, attr, value): """Verify that value is a valid version specifier""" try: - packaging.specifiers.SpecifierSet(value) - except packaging.specifiers.InvalidSpecifier as error: + SpecifierSet(value) + except (InvalidSpecifier, AttributeError) as error: tmpl = ( - "{attr!r} must be a string " - "containing valid version specifiers; {error}" + "{attr!r} must be a string " "containing valid version specifiers; {error}" ) - raise DistutilsSetupError(tmpl.format(attr=attr, error=error)) + raise DistutilsSetupError(tmpl.format(attr=attr, error=error)) from error def check_entry_points(dist, attr, value): """Verify that entry_points map is parseable""" try: - pkg_resources.EntryPoint.parse_map(value) - except ValueError as e: - raise DistutilsSetupError(e) + _entry_points.load(value) + except Exception as e: + raise DistutilsSetupError(e) from e def check_test_suite(dist, attr, value): - if not isinstance(value, six.string_types): + if not isinstance(value, str): raise DistutilsSetupError("test_suite must be a string") @@ -315,12 +182,12 @@ def check_package_data(dist, attr, value): if not isinstance(value, dict): raise DistutilsSetupError( "{!r} must be a dictionary mapping package names to lists of " - "string wildcard patterns".format(attr)) + "string wildcard patterns".format(attr) + ) for k, v in value.items(): - if not isinstance(k, six.string_types): + if not isinstance(k, str): raise DistutilsSetupError( - "keys of {!r} dict must be strings (got {!r})" - .format(attr, k) + "keys of {!r} dict must be strings (got {!r})".format(attr, k) ) assert_string_list(dist, 'values of {!r} dict'.format(attr), v) @@ -330,7 +197,8 @@ def check_packages(dist, attr, value): if not re.match(r'\w+(\.\w+)*', pkgname): distutils.log.warn( "WARNING: %r not a valid package name; please use only " - ".-separated package names in setup.py", pkgname + ".-separated package names in setup.py", + pkgname, ) @@ -338,7 +206,7 @@ _Distribution = get_unpatched(distutils.core.Distribution) class Distribution(_Distribution): - """Distribution with support for features, tests, and package data + """Distribution with support for tests and package data This is an enhanced version of 'distutils.dist.Distribution' that effectively adds the following new optional keyword arguments to 'setup()': @@ -365,21 +233,6 @@ class Distribution(_Distribution): EasyInstall and requests one of your extras, the corresponding additional requirements will be installed if needed. - 'features' **deprecated** -- a dictionary mapping option names to - 'setuptools.Feature' - objects. Features are a portion of the distribution that can be - included or excluded based on user options, inter-feature dependencies, - and availability on the current system. Excluded features are omitted - from all setup commands, including source and binary distributions, so - you can create multiple distributions from the same source tree. - Feature names should be valid Python identifiers, except that they may - contain the '-' (minus) sign. Features can be included or excluded - via the command line options '--with-X' and '--without-X', where 'X' is - the name of the feature. Whether a feature is included by default, and - whether you are allowed to control this from the command line, is - determined by the Feature object. See the 'Feature' class for more - information. - 'test_suite' -- the name of a test suite to run for the 'test' command. If the user runs 'python setup.py test', the package will be installed, and the named test suite will be run. The format is the same as @@ -401,15 +254,17 @@ class Distribution(_Distribution): for manipulating the distribution's contents. For example, the 'include()' and 'exclude()' methods can be thought of as in-place add and subtract commands that add or remove packages, modules, extensions, and so on from - the distribution. They are used by the feature subsystem to configure the - distribution for the included and excluded features. + the distribution. """ _DISTUTILS_UNSUPPORTED_METADATA = { - 'long_description_content_type': None, + 'long_description_content_type': lambda: None, 'project_urls': dict, - 'provides_extras': ordered_set.OrderedSet, - 'license_files': ordered_set.OrderedSet, + 'provides_extras': OrderedSet, + 'license_file': lambda: None, + 'license_files': lambda: None, + 'install_requires': list, + 'extras_require': dict, } _patched_dist = None @@ -421,71 +276,83 @@ class Distribution(_Distribution): # if not attrs or 'name' not in attrs or 'version' not in attrs: return - key = pkg_resources.safe_name(str(attrs['name'])).lower() - dist = pkg_resources.working_set.by_key.get(key) - if dist is not None and not dist.has_metadata('PKG-INFO'): - dist._version = pkg_resources.safe_version(str(attrs['version'])) - self._patched_dist = dist + name = _normalization.safe_name(str(attrs['name'])).lower() + with suppress(metadata.PackageNotFoundError): + dist = metadata.distribution(name) + if dist is not None and not dist.read_text('PKG-INFO'): + dist._version = _normalization.safe_version(str(attrs['version'])) + self._patched_dist = dist def __init__(self, attrs=None): have_package_data = hasattr(self, "package_data") if not have_package_data: self.package_data = {} attrs = attrs or {} - if 'features' in attrs or 'require_features' in attrs: - Feature.warn_deprecated() - self.require_features = [] - self.features = {} self.dist_files = [] # Filter-out setuptools' specific options. self.src_root = attrs.pop("src_root", None) self.patch_missing_pkg_info(attrs) self.dependency_links = attrs.pop('dependency_links', []) self.setup_requires = attrs.pop('setup_requires', []) - for ep in pkg_resources.iter_entry_points('distutils.setup_keywords'): + for ep in metadata.entry_points(group='distutils.setup_keywords'): vars(self).setdefault(ep.name, None) - _Distribution.__init__(self, { - k: v for k, v in attrs.items() - if k not in self._DISTUTILS_UNSUPPORTED_METADATA - }) - # Fill-in missing metadata fields not supported by distutils. - # Note some fields may have been set by other tools (e.g. pbr) - # above; they are taken preferrentially to setup() arguments - for option, default in self._DISTUTILS_UNSUPPORTED_METADATA.items(): - for source in self.metadata.__dict__, attrs: - if option in source: - value = source[option] - break - else: - value = default() if default else None - setattr(self.metadata, option, value) + metadata_only = set(self._DISTUTILS_UNSUPPORTED_METADATA) + metadata_only -= {"install_requires", "extras_require"} + dist_attrs = {k: v for k, v in attrs.items() if k not in metadata_only} + _Distribution.__init__(self, dist_attrs) - if isinstance(self.metadata.version, numbers.Number): - # Some people apparently take "version number" too literally :) - self.metadata.version = str(self.metadata.version) + # Private API (setuptools-use only, not restricted to Distribution) + # Stores files that are referenced by the configuration and need to be in the + # sdist (e.g. `version = file: VERSION.txt`) + self._referenced_files: Set[str] = set() - if self.metadata.version is not None: - try: - ver = packaging.version.Version(self.metadata.version) - normalized_version = str(ver) - if self.metadata.version != normalized_version: - warnings.warn( - "Normalizing '%s' to '%s'" % ( - self.metadata.version, - normalized_version, - ) - ) - self.metadata.version = normalized_version - except (packaging.version.InvalidVersion, TypeError): - warnings.warn( - "The version specified (%r) is an invalid version, this " - "may not work as expected with newer versions of " - "setuptools, pip, and PyPI. Please see PEP 440 for more " - "details." % self.metadata.version - ) + self.set_defaults = ConfigDiscovery(self) + + self._set_metadata_defaults(attrs) + + self.metadata.version = self._normalize_version(self.metadata.version) self._finalize_requires() + def _validate_metadata(self): + required = {"name"} + provided = { + key + for key in vars(self.metadata) + if getattr(self.metadata, key, None) is not None + } + missing = required - provided + + if missing: + msg = f"Required package metadata is missing: {missing}" + raise DistutilsSetupError(msg) + + def _set_metadata_defaults(self, attrs): + """ + Fill-in missing metadata fields not supported by distutils. + Some fields may have been set by other tools (e.g. pbr). + Those fields (vars(self.metadata)) take precedence to + supplied attrs. + """ + for option, default in self._DISTUTILS_UNSUPPORTED_METADATA.items(): + vars(self.metadata).setdefault(option, attrs.get(option, default())) + + @staticmethod + def _normalize_version(version): + from . import sic + + if isinstance(version, numbers.Number): + # Some people apparently take "version number" too literally :) + version = str(version) + elif isinstance(version, sic) or version is None: + return version + + normalized = str(Version(version)) + if version != normalized: + InformationOnly.emit(f"Normalizing '{version}' to '{normalized}'") + return normalized + return version + def _finalize_requires(self): """ Set `metadata.python_requires` and fix environment markers @@ -494,91 +361,89 @@ class Distribution(_Distribution): if getattr(self, 'python_requires', None): self.metadata.python_requires = self.python_requires - if getattr(self, 'extras_require', None): + self._normalize_requires() + self.metadata.install_requires = self.install_requires + self.metadata.extras_require = self.extras_require + + if self.extras_require: for extra in self.extras_require.keys(): - # Since this gets called multiple times at points where the - # keys have become 'converted' extras, ensure that we are only - # truly adding extras we haven't seen before here. + # Setuptools allows a weird ": syntax for extras extra = extra.split(':')[0] if extra: self.metadata.provides_extras.add(extra) - self._convert_extras_requirements() - self._move_install_requirements_markers() + def _normalize_requires(self): + """Make sure requirement-related attributes exist and are normalized""" + install_requires = getattr(self, "install_requires", None) or [] + extras_require = getattr(self, "extras_require", None) or {} + self.install_requires = list(map(str, _reqs.parse(install_requires))) + self.extras_require = { + k: list(map(str, _reqs.parse(v or []))) for k, v in extras_require.items() + } - def _convert_extras_requirements(self): - """ - Convert requirements in `extras_require` of the form - `"extra": ["barbazquux; {marker}"]` to - `"extra:{marker}": ["barbazquux"]`. - """ - spec_ext_reqs = getattr(self, 'extras_require', None) or {} - self._tmp_extras_require = defaultdict(list) - for section, v in spec_ext_reqs.items(): - # Do not strip empty sections. - self._tmp_extras_require[section] - for r in pkg_resources.parse_requirements(v): - suffix = self._suffix_for(r) - self._tmp_extras_require[section + suffix].append(r) + def _finalize_license_files(self): + """Compute names of all license files which should be included.""" + license_files: Optional[List[str]] = self.metadata.license_files + patterns: List[str] = license_files if license_files else [] - @staticmethod - def _suffix_for(req): - """ - For a requirement, return the 'extras_require' suffix for - that requirement. - """ - return ':' + str(req.marker) if req.marker else '' + license_file: Optional[str] = self.metadata.license_file + if license_file and license_file not in patterns: + patterns.append(license_file) - def _move_install_requirements_markers(self): - """ - Move requirements in `install_requires` that are using environment - markers `extras_require`. - """ + if license_files is None and license_file is None: + # Default patterns match the ones wheel uses + # See https://wheel.readthedocs.io/en/stable/user_guide.html + # -> 'Including license files in the generated wheel file' + patterns = ('LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*') - # divide the install_requires into two sets, simple ones still - # handled by install_requires and more complex ones handled - # by extras_require. - - def is_simple_req(req): - return not req.marker - - spec_inst_reqs = getattr(self, 'install_requires', None) or () - inst_reqs = list(pkg_resources.parse_requirements(spec_inst_reqs)) - simple_reqs = filter(is_simple_req, inst_reqs) - complex_reqs = filterfalse(is_simple_req, inst_reqs) - self.install_requires = list(map(str, simple_reqs)) - - for r in complex_reqs: - self._tmp_extras_require[':' + str(r.marker)].append(r) - self.extras_require = dict( - (k, [str(r) for r in map(self._clean_req, v)]) - for k, v in self._tmp_extras_require.items() + self.metadata.license_files = list( + unique_everseen(self._expand_patterns(patterns)) ) - def _clean_req(self, req): + @staticmethod + def _expand_patterns(patterns): """ - Given a Requirement, remove environment markers and return it. + >>> list(Distribution._expand_patterns(['LICENSE'])) + ['LICENSE'] + >>> list(Distribution._expand_patterns(['setup.cfg', 'LIC*'])) + ['setup.cfg', 'LICENSE'] """ - req.marker = None - return req + return ( + path + for pattern in patterns + for path in sorted(iglob(pattern)) + if not path.endswith('~') and os.path.isfile(path) + ) - def _parse_config_files(self, filenames=None): + # FIXME: 'Distribution._parse_config_files' is too complex (14) + def _parse_config_files(self, filenames=None): # noqa: C901 """ Adapted from distutils.dist.Distribution.parse_config_files, this method provides the same functionality in subtly-improved ways. """ - from setuptools.extern.six.moves.configparser import ConfigParser + from configparser import ConfigParser # Ignore install directory options if we have a venv - if not six.PY2 and sys.prefix != sys.base_prefix: - ignore_options = [ - 'install-base', 'install-platbase', 'install-lib', - 'install-platlib', 'install-purelib', 'install-headers', - 'install-scripts', 'install-data', 'prefix', 'exec-prefix', - 'home', 'user', 'root'] - else: - ignore_options = [] + ignore_options = ( + [] + if sys.prefix == sys.base_prefix + else [ + 'install-base', + 'install-platbase', + 'install-lib', + 'install-platlib', + 'install-purelib', + 'install-headers', + 'install-scripts', + 'install-data', + 'prefix', + 'exec-prefix', + 'home', + 'user', + 'root', + ] + ) ignore_options = frozenset(ignore_options) @@ -589,62 +454,107 @@ class Distribution(_Distribution): self.announce("Distribution.parse_config_files():") parser = ConfigParser() + parser.optionxform = str for filename in filenames: - with io.open(filename, encoding='utf-8') as reader: + with open(filename, encoding='utf-8') as reader: if DEBUG: self.announce(" reading {filename}".format(**locals())) - (parser.readfp if six.PY2 else parser.read_file)(reader) + parser.read_file(reader) for section in parser.sections(): options = parser.options(section) opt_dict = self.get_option_dict(section) for opt in options: - if opt != '__name__' and opt not in ignore_options: - val = self._try_str(parser.get(section, opt)) - opt = opt.replace('-', '_') - opt_dict[opt] = (filename, val) + if opt == '__name__' or opt in ignore_options: + continue + + val = parser.get(section, opt) + opt = self.warn_dash_deprecation(opt, section) + opt = self.make_option_lowercase(opt, section) + opt_dict[opt] = (filename, val) # Make the ConfigParser forget everything (so we retain # the original filenames that options come from) parser.__init__() + if 'global' not in self.command_options: + return + # If there was a "global" section in the config file, use it # to set Distribution options. - if 'global' in self.command_options: - for (opt, (src, val)) in self.command_options['global'].items(): - alias = self.negative_opt.get(opt) - try: - if alias: - setattr(self, alias, not strtobool(val)) - elif opt in ('verbose', 'dry_run'): # ugh! - setattr(self, opt, strtobool(val)) - else: - setattr(self, opt, val) - except ValueError as msg: - raise DistutilsOptionError(msg) + for opt, (src, val) in self.command_options['global'].items(): + alias = self.negative_opt.get(opt) + if alias: + val = not strtobool(val) + elif opt in ('verbose', 'dry_run'): # ugh! + val = strtobool(val) - @staticmethod - def _try_str(val): - """ - On Python 2, much of distutils relies on string values being of - type 'str' (bytes) and not unicode text. If the value can be safely - encoded to bytes using the default encoding, prefer that. + try: + setattr(self, alias or opt, val) + except ValueError as e: + raise DistutilsOptionError(e) from e - Why the default encoding? Because that value can be implicitly - decoded back to text if needed. + def warn_dash_deprecation(self, opt, section): + if section in ( + 'options.extras_require', + 'options.data_files', + ): + return opt - Ref #1653 - """ - if not six.PY2: - return val + underscore_opt = opt.replace('-', '_') + commands = list( + itertools.chain( + distutils.command.__all__, + self._setuptools_commands(), + ) + ) + if ( + not section.startswith('options') + and section != 'metadata' + and section not in commands + ): + return underscore_opt + + if '-' in opt: + SetuptoolsDeprecationWarning.emit( + "Invalid dash-separated options", + f""" + Usage of dash-separated {opt!r} will not be supported in future + versions. Please use the underscore name {underscore_opt!r} instead. + """, + see_docs="userguide/declarative_config.html", + due_date=(2024, 9, 26), + # Warning initially introduced in 3 Mar 2021 + ) + return underscore_opt + + def _setuptools_commands(self): try: - return val.encode() - except UnicodeEncodeError: - pass - return val + return metadata.distribution('setuptools').entry_points.names + except metadata.PackageNotFoundError: + # during bootstrapping, distribution doesn't exist + return [] - def _set_command_options(self, command_obj, option_dict=None): + def make_option_lowercase(self, opt, section): + if section != 'metadata' or opt.islower(): + return opt + + lowercase_opt = opt.lower() + SetuptoolsDeprecationWarning.emit( + "Invalid uppercase configuration", + f""" + Usage of uppercase key {opt!r} in {section!r} will not be supported in + future versions. Please use lowercase {lowercase_opt!r} instead. + """, + see_docs="userguide/declarative_config.html", + due_date=(2024, 9, 26), + # Warning initially introduced in 6 Mar 2021 + ) + return lowercase_opt + + # FIXME: 'Distribution._set_command_options' is too complex (14) + def _set_command_options(self, command_obj, option_dict=None): # noqa: C901 """ Set the options for 'command_obj' from 'option_dict'. Basically this means copying elements of a dictionary ('option_dict') to @@ -662,13 +572,11 @@ class Distribution(_Distribution): if DEBUG: self.announce(" setting options for '%s' command:" % command_name) - for (option, (source, value)) in option_dict.items(): + for option, (source, value) in option_dict.items(): if DEBUG: - self.announce(" %s = %s (from %s)" % (option, value, - source)) + self.announce(" %s = %s (from %s)" % (option, value, source)) try: - bool_opts = [translate_longopt(o) - for o in command_obj.boolean_options] + bool_opts = [translate_longopt(o) for o in command_obj.boolean_options] except AttributeError: bool_opts = [] try: @@ -677,7 +585,7 @@ class Distribution(_Distribution): neg_opt = {} try: - is_string = isinstance(value, six.string_types) + is_string = isinstance(value, str) if option in neg_opt and is_string: setattr(command_obj, neg_opt[option], not strtobool(value)) elif option in bool_opts and is_string: @@ -687,42 +595,45 @@ class Distribution(_Distribution): else: raise DistutilsOptionError( "error in %s: command '%s' has no such option '%s'" - % (source, command_name, option)) - except ValueError as msg: - raise DistutilsOptionError(msg) + % (source, command_name, option) + ) + except ValueError as e: + raise DistutilsOptionError(e) from e + + def _get_project_config_files(self, filenames): + """Add default file and split between INI and TOML""" + tomlfiles = [] + standard_project_metadata = Path(self.src_root or os.curdir, "pyproject.toml") + if filenames is not None: + parts = partition(lambda f: Path(f).suffix == ".toml", filenames) + filenames = list(parts[0]) # 1st element => predicate is False + tomlfiles = list(parts[1]) # 2nd element => predicate is True + elif standard_project_metadata.exists(): + tomlfiles = [standard_project_metadata] + return filenames, tomlfiles def parse_config_files(self, filenames=None, ignore_option_errors=False): """Parses configuration files from various levels and loads configuration. - """ - self._parse_config_files(filenames=filenames) + inifiles, tomlfiles = self._get_project_config_files(filenames) + + self._parse_config_files(filenames=inifiles) + + setupcfg.parse_configuration( + self, self.command_options, ignore_option_errors=ignore_option_errors + ) + for filename in tomlfiles: + pyprojecttoml.apply_configuration(self, filename, ignore_option_errors) - parse_configuration(self, self.command_options, - ignore_option_errors=ignore_option_errors) self._finalize_requires() - - def parse_command_line(self): - """Process features after parsing command line options""" - result = _Distribution.parse_command_line(self) - if self.features: - self._finalize_features() - return result - - def _feature_attrname(self, name): - """Convert feature name to corresponding option attribute name""" - return 'with_' + name.replace('-', '_') + self._finalize_license_files() def fetch_build_eggs(self, requires): """Resolve pre-setup requirements""" - resolved_dists = pkg_resources.working_set.resolve( - pkg_resources.parse_requirements(requires), - installer=self.fetch_build_egg, - replace_conflicting=True, - ) - for dist in resolved_dists: - pkg_resources.working_set.add(dist, replace=True) - return resolved_dists + from .installer import _fetch_build_eggs + + return _fetch_build_eggs(self, requires) def finalize_options(self): """ @@ -735,142 +646,86 @@ class Distribution(_Distribution): def by_order(hook): return getattr(hook, 'order', 0) - eps = map(lambda e: e.load(), pkg_resources.iter_entry_points(group)) - for ep in sorted(eps, key=by_order): + + defined = metadata.entry_points(group=group) + filtered = itertools.filterfalse(self._removed, defined) + loaded = map(lambda e: e.load(), filtered) + for ep in sorted(loaded, key=by_order): ep(self) + @staticmethod + def _removed(ep): + """ + When removing an entry point, if metadata is loaded + from an older version of Setuptools, that removed + entry point will attempt to be loaded and will fail. + See #2765 for more details. + """ + removed = { + # removed 2021-09-05 + '2to3_doctests', + } + return ep.name in removed + def _finalize_setup_keywords(self): - for ep in pkg_resources.iter_entry_points('distutils.setup_keywords'): + for ep in metadata.entry_points(group='distutils.setup_keywords'): value = getattr(self, ep.name, None) if value is not None: - ep.require(installer=self.fetch_build_egg) ep.load()(self, ep.name, value) - def _finalize_2to3_doctests(self): - if getattr(self, 'convert_2to3_doctests', None): - # XXX may convert to set here when we can rely on set being builtin - self.convert_2to3_doctests = [ - os.path.abspath(p) - for p in self.convert_2to3_doctests - ] - else: - self.convert_2to3_doctests = [] - def get_egg_cache_dir(self): + from . import windows_support + egg_cache_dir = os.path.join(os.curdir, '.eggs') if not os.path.exists(egg_cache_dir): os.mkdir(egg_cache_dir) windows_support.hide_file(egg_cache_dir) readme_txt_filename = os.path.join(egg_cache_dir, 'README.txt') with open(readme_txt_filename, 'w') as f: - f.write('This directory contains eggs that were downloaded ' - 'by setuptools to build, test, and run plug-ins.\n\n') - f.write('This directory caches those eggs to prevent ' - 'repeated downloads.\n\n') + f.write( + 'This directory contains eggs that were downloaded ' + 'by setuptools to build, test, and run plug-ins.\n\n' + ) + f.write( + 'This directory caches those eggs to prevent ' + 'repeated downloads.\n\n' + ) f.write('However, it is safe to delete this directory.\n\n') return egg_cache_dir def fetch_build_egg(self, req): """Fetch an egg needed for building""" - from setuptools.installer import fetch_build_egg + from .installer import fetch_build_egg + return fetch_build_egg(self, req) - def _finalize_feature_opts(self): - """Add --with-X/--without-X options based on optional features""" - - if not self.features: - return - - go = [] - no = self.negative_opt.copy() - - for name, feature in self.features.items(): - self._set_feature(name, None) - feature.validate(self) - - if feature.optional: - descr = feature.description - incdef = ' (default)' - excdef = '' - if not feature.include_by_default(): - excdef, incdef = incdef, excdef - - new = ( - ('with-' + name, None, 'include ' + descr + incdef), - ('without-' + name, None, 'exclude ' + descr + excdef), - ) - go.extend(new) - no['without-' + name] = 'with-' + name - - self.global_options = self.feature_options = go + self.global_options - self.negative_opt = self.feature_negopt = no - - def _finalize_features(self): - """Add/remove features and resolve dependencies between them""" - - # First, flag all the enabled items (and thus their dependencies) - for name, feature in self.features.items(): - enabled = self.feature_is_included(name) - if enabled or (enabled is None and feature.include_by_default()): - feature.include_in(self) - self._set_feature(name, 1) - - # Then disable the rest, so that off-by-default features don't - # get flagged as errors when they're required by an enabled feature - for name, feature in self.features.items(): - if not self.feature_is_included(name): - feature.exclude_from(self) - self._set_feature(name, 0) - def get_command_class(self, command): """Pluggable version of get_command_class()""" if command in self.cmdclass: return self.cmdclass[command] - eps = pkg_resources.iter_entry_points('distutils.commands', command) + eps = metadata.entry_points(group='distutils.commands', name=command) for ep in eps: - ep.require(installer=self.fetch_build_egg) self.cmdclass[command] = cmdclass = ep.load() return cmdclass else: return _Distribution.get_command_class(self, command) def print_commands(self): - for ep in pkg_resources.iter_entry_points('distutils.commands'): + for ep in metadata.entry_points(group='distutils.commands'): if ep.name not in self.cmdclass: - # don't require extras as the commands won't be invoked - cmdclass = ep.resolve() + cmdclass = ep.load() self.cmdclass[ep.name] = cmdclass return _Distribution.print_commands(self) def get_command_list(self): - for ep in pkg_resources.iter_entry_points('distutils.commands'): + for ep in metadata.entry_points(group='distutils.commands'): if ep.name not in self.cmdclass: - # don't require extras as the commands won't be invoked - cmdclass = ep.resolve() + cmdclass = ep.load() self.cmdclass[ep.name] = cmdclass return _Distribution.get_command_list(self) - def _set_feature(self, name, status): - """Set feature's inclusion status""" - setattr(self, self._feature_attrname(name), status) - - def feature_is_included(self, name): - """Return 1 if feature is included, 0 if excluded, 'None' if unknown""" - return getattr(self, self._feature_attrname(name)) - - def include_feature(self, name): - """Request inclusion of feature named 'name'""" - - if self.feature_is_included(name) == 0: - descr = self.features[name].description - raise DistutilsOptionError( - descr + " is required, but was excluded or is not available" - ) - self.features[name].include_in(self) - self._set_feature(name, 1) - def include(self, **attrs): """Add items to distribution that are named in keyword arguments @@ -899,19 +754,18 @@ class Distribution(_Distribution): pfx = package + '.' if self.packages: self.packages = [ - p for p in self.packages - if p != package and not p.startswith(pfx) + p for p in self.packages if p != package and not p.startswith(pfx) ] if self.py_modules: self.py_modules = [ - p for p in self.py_modules - if p != package and not p.startswith(pfx) + p for p in self.py_modules if p != package and not p.startswith(pfx) ] if self.ext_modules: self.ext_modules = [ - p for p in self.ext_modules + p + for p in self.ext_modules if p.name != package and not p.name.startswith(pfx) ] @@ -924,6 +778,8 @@ class Distribution(_Distribution): if p == package or p.startswith(pfx): return True + return False + def _exclude_misc(self, name, value): """Handle 'exclude()' for list/tuple attrs without a special handler""" if not isinstance(value, sequence): @@ -932,10 +788,8 @@ class Distribution(_Distribution): ) try: old = getattr(self, name) - except AttributeError: - raise DistutilsSetupError( - "%s: No such distribution setting" % name - ) + except AttributeError as e: + raise DistutilsSetupError("%s: No such distribution setting" % name) from e if old is not None and not isinstance(old, sequence): raise DistutilsSetupError( name + ": this setting cannot be changed via include/exclude" @@ -947,15 +801,11 @@ class Distribution(_Distribution): """Handle 'include()' for list/tuple attrs without a special handler""" if not isinstance(value, sequence): - raise DistutilsSetupError( - "%s: setting must be a list (%r)" % (name, value) - ) + raise DistutilsSetupError("%s: setting must be a list (%r)" % (name, value)) try: old = getattr(self, name) - except AttributeError: - raise DistutilsSetupError( - "%s: No such distribution setting" % name - ) + except AttributeError as e: + raise DistutilsSetupError("%s: No such distribution setting" % name) from e if old is None: setattr(self, name, value) elif not isinstance(old, sequence): @@ -1008,6 +858,7 @@ class Distribution(_Distribution): src, alias = aliases[command] del aliases[command] # ensure each alias can expand only once! import shlex + args[:1] = shlex.split(alias, True) command = args[0] @@ -1035,9 +886,7 @@ class Distribution(_Distribution): d = {} for cmd, opts in self.command_options.items(): - for opt, (src, val) in opts.items(): - if src != "command line": continue @@ -1065,11 +914,9 @@ class Distribution(_Distribution): def iter_distribution_names(self): """Yield all packages, modules, and extension names in distribution""" - for pkg in self.packages or (): - yield pkg + yield from self.packages or () - for module in self.py_modules or (): - yield module + yield from self.py_modules or () for ext in self.ext_modules or (): if isinstance(ext, tuple): @@ -1088,7 +935,7 @@ class Distribution(_Distribution): """ import sys - if six.PY2 or self.help_commands: + if self.help_commands: return _Distribution.handle_display_options(self, option_order) # Stdout may be StringIO (e.g. in tests) @@ -1102,171 +949,18 @@ class Distribution(_Distribution): # Print metadata in UTF-8 no matter the platform encoding = sys.stdout.encoding - errors = sys.stdout.errors - newline = sys.platform != 'win32' and '\n' or None - line_buffering = sys.stdout.line_buffering - - sys.stdout = io.TextIOWrapper( - sys.stdout.detach(), 'utf-8', errors, newline, line_buffering) + sys.stdout.reconfigure(encoding='utf-8') try: return _Distribution.handle_display_options(self, option_order) finally: - sys.stdout = io.TextIOWrapper( - sys.stdout.detach(), encoding, errors, newline, line_buffering) + sys.stdout.reconfigure(encoding=encoding) + def run_command(self, command): + self.set_defaults() + # Postpone defaults until all explicit configuration is considered + # (setup() args, config files, command line and plugins) -class Feature: - """ - **deprecated** -- The `Feature` facility was never completely implemented - or supported, `has reported issues - `_ and will be removed in - a future version. - - A subset of the distribution that can be excluded if unneeded/wanted - - Features are created using these keyword arguments: - - 'description' -- a short, human readable description of the feature, to - be used in error messages, and option help messages. - - 'standard' -- if true, the feature is included by default if it is - available on the current system. Otherwise, the feature is only - included if requested via a command line '--with-X' option, or if - another included feature requires it. The default setting is 'False'. - - 'available' -- if true, the feature is available for installation on the - current system. The default setting is 'True'. - - 'optional' -- if true, the feature's inclusion can be controlled from the - command line, using the '--with-X' or '--without-X' options. If - false, the feature's inclusion status is determined automatically, - based on 'availabile', 'standard', and whether any other feature - requires it. The default setting is 'True'. - - 'require_features' -- a string or sequence of strings naming features - that should also be included if this feature is included. Defaults to - empty list. May also contain 'Require' objects that should be - added/removed from the distribution. - - 'remove' -- a string or list of strings naming packages to be removed - from the distribution if this feature is *not* included. If the - feature *is* included, this argument is ignored. This argument exists - to support removing features that "crosscut" a distribution, such as - defining a 'tests' feature that removes all the 'tests' subpackages - provided by other features. The default for this argument is an empty - list. (Note: the named package(s) or modules must exist in the base - distribution when the 'setup()' function is initially called.) - - other keywords -- any other keyword arguments are saved, and passed to - the distribution's 'include()' and 'exclude()' methods when the - feature is included or excluded, respectively. So, for example, you - could pass 'packages=["a","b"]' to cause packages 'a' and 'b' to be - added or removed from the distribution as appropriate. - - A feature must include at least one 'requires', 'remove', or other - keyword argument. Otherwise, it can't affect the distribution in any way. - Note also that you can subclass 'Feature' to create your own specialized - feature types that modify the distribution in other ways when included or - excluded. See the docstrings for the various methods here for more detail. - Aside from the methods, the only feature attributes that distributions look - at are 'description' and 'optional'. - """ - - @staticmethod - def warn_deprecated(): - msg = ( - "Features are deprecated and will be removed in a future " - "version. See https://github.com/pypa/setuptools/issues/65." - ) - warnings.warn(msg, DistDeprecationWarning, stacklevel=3) - - def __init__( - self, description, standard=False, available=True, - optional=True, require_features=(), remove=(), **extras): - self.warn_deprecated() - - self.description = description - self.standard = standard - self.available = available - self.optional = optional - if isinstance(require_features, (str, Require)): - require_features = require_features, - - self.require_features = [ - r for r in require_features if isinstance(r, str) - ] - er = [r for r in require_features if not isinstance(r, str)] - if er: - extras['require_features'] = er - - if isinstance(remove, str): - remove = remove, - self.remove = remove - self.extras = extras - - if not remove and not require_features and not extras: - raise DistutilsSetupError( - "Feature %s: must define 'require_features', 'remove', or " - "at least one of 'packages', 'py_modules', etc." - ) - - def include_by_default(self): - """Should this feature be included by default?""" - return self.available and self.standard - - def include_in(self, dist): - """Ensure feature and its requirements are included in distribution - - You may override this in a subclass to perform additional operations on - the distribution. Note that this method may be called more than once - per feature, and so should be idempotent. - - """ - - if not self.available: - raise DistutilsPlatformError( - self.description + " is required, " - "but is not available on this platform" - ) - - dist.include(**self.extras) - - for f in self.require_features: - dist.include_feature(f) - - def exclude_from(self, dist): - """Ensure feature is excluded from distribution - - You may override this in a subclass to perform additional operations on - the distribution. This method will be called at most once per - feature, and only after all included features have been asked to - include themselves. - """ - - dist.exclude(**self.extras) - - if self.remove: - for item in self.remove: - dist.exclude_package(item) - - def validate(self, dist): - """Verify that feature makes sense in context of distribution - - This method is called by the distribution just before it parses its - command line. It checks to ensure that the 'remove' attribute, if any, - contains only valid package/module names that are present in the base - distribution when 'setup()' is called. You may override it in a - subclass to perform any other required validation of the feature - against a target distribution. - """ - - for item in self.remove: - if not dist.has_contents_for(item): - raise DistutilsSetupError( - "%s wants to be able to remove %s, but the distribution" - " doesn't contain any packages or modules under %s" - % (self.description, item, item) - ) + super().run_command(command) class DistDeprecationWarning(SetuptoolsDeprecationWarning): diff --git a/libs/common/setuptools/errors.py b/libs/common/setuptools/errors.py index 2701747f..67a5a1df 100644 --- a/libs/common/setuptools/errors.py +++ b/libs/common/setuptools/errors.py @@ -3,10 +3,41 @@ Provides exceptions used by setuptools modules. """ -from distutils.errors import DistutilsError +from distutils import errors as _distutils_errors -class RemovedCommandError(DistutilsError, RuntimeError): +# Re-export errors from distutils to facilitate the migration to PEP632 + +ByteCompileError = _distutils_errors.DistutilsByteCompileError +CCompilerError = _distutils_errors.CCompilerError +ClassError = _distutils_errors.DistutilsClassError +CompileError = _distutils_errors.CompileError +ExecError = _distutils_errors.DistutilsExecError +FileError = _distutils_errors.DistutilsFileError +InternalError = _distutils_errors.DistutilsInternalError +LibError = _distutils_errors.LibError +LinkError = _distutils_errors.LinkError +ModuleError = _distutils_errors.DistutilsModuleError +OptionError = _distutils_errors.DistutilsOptionError +PlatformError = _distutils_errors.DistutilsPlatformError +PreprocessError = _distutils_errors.PreprocessError +SetupError = _distutils_errors.DistutilsSetupError +TemplateError = _distutils_errors.DistutilsTemplateError +UnknownFileError = _distutils_errors.UnknownFileError + +# The root error class in the hierarchy +BaseError = _distutils_errors.DistutilsError + + +class InvalidConfigError(OptionError): + """Error used for invalid configurations.""" + + +class RemovedConfigError(OptionError): + """Error used for configurations that were deprecated and removed.""" + + +class RemovedCommandError(BaseError, RuntimeError): """Error used for commands that have been removed in setuptools. Since ``setuptools`` is built on ``distutils``, simply removing a command @@ -14,3 +45,22 @@ class RemovedCommandError(DistutilsError, RuntimeError): error is raised if a command exists in ``distutils`` but has been actively removed in ``setuptools``. """ + + +class PackageDiscoveryError(BaseError, RuntimeError): + """Impossible to perform automatic discovery of packages and/or modules. + + The current project layout or given discovery options can lead to problems when + scanning the project directory. + + Setuptools might also refuse to complete auto-discovery if an error prone condition + is detected (e.g. when a project is organised as a flat-layout but contains + multiple directories that can be taken as top-level packages inside a single + distribution [*]_). In these situations the users are encouraged to be explicit + about which packages to include or to make the discovery parameters more specific. + + .. [*] Since multi-package distributions are uncommon it is very likely that the + developers did not intend for all the directories to be packaged, and are just + leaving auxiliary code in the repository top-level, such as maintenance-related + scripts. + """ diff --git a/libs/common/setuptools/extension.py b/libs/common/setuptools/extension.py index 29468894..58c023f6 100644 --- a/libs/common/setuptools/extension.py +++ b/libs/common/setuptools/extension.py @@ -4,8 +4,6 @@ import distutils.core import distutils.errors import distutils.extension -from setuptools.extern.six.moves import map - from .monkey import get_unpatched @@ -30,13 +28,106 @@ _Extension = get_unpatched(distutils.core.Extension) class Extension(_Extension): - """Extension that uses '.c' files in place of '.pyx' files""" + """ + Describes a single extension module. + + This means that all source files will be compiled into a single binary file + ``.`` (with ```` derived from ``name`` and + ```` defined by one of the values in + ``importlib.machinery.EXTENSION_SUFFIXES``). + + In the case ``.pyx`` files are passed as ``sources and`` ``Cython`` is **not** + installed in the build environment, ``setuptools`` may also try to look for the + equivalent ``.cpp`` or ``.c`` files. + + :arg str name: + the full name of the extension, including any packages -- ie. + *not* a filename or pathname, but Python dotted name + + :arg list[str] sources: + list of source filenames, relative to the distribution root + (where the setup script lives), in Unix form (slash-separated) + for portability. Source files may be C, C++, SWIG (.i), + platform-specific resource files, or whatever else is recognized + by the "build_ext" command as source for a Python extension. + + :keyword list[str] include_dirs: + list of directories to search for C/C++ header files (in Unix + form for portability) + + :keyword list[tuple[str, str|None]] define_macros: + list of macros to define; each macro is defined using a 2-tuple: + the first item corresponding to the name of the macro and the second + item either a string with its value or None to + define it without a particular value (equivalent of "#define + FOO" in source or -DFOO on Unix C compiler command line) + + :keyword list[str] undef_macros: + list of macros to undefine explicitly + + :keyword list[str] library_dirs: + list of directories to search for C/C++ libraries at link time + + :keyword list[str] libraries: + list of library names (not filenames or paths) to link against + + :keyword list[str] runtime_library_dirs: + list of directories to search for C/C++ libraries at run time + (for shared extensions, this is when the extension is loaded). + Setting this will cause an exception during build on Windows + platforms. + + :keyword list[str] extra_objects: + list of extra files to link with (eg. object files not implied + by 'sources', static library that must be explicitly specified, + binary resource files, etc.) + + :keyword list[str] extra_compile_args: + any extra platform- and compiler-specific information to use + when compiling the source files in 'sources'. For platforms and + compilers where "command line" makes sense, this is typically a + list of command-line arguments, but for other platforms it could + be anything. + + :keyword list[str] extra_link_args: + any extra platform- and compiler-specific information to use + when linking object files together to create the extension (or + to create a new static Python interpreter). Similar + interpretation as for 'extra_compile_args'. + + :keyword list[str] export_symbols: + list of symbols to be exported from a shared extension. Not + used on all platforms, and not generally necessary for Python + extensions, which typically export exactly one symbol: "init" + + extension_name. + + :keyword list[str] swig_opts: + any extra options to pass to SWIG if a source file has the .i + extension. + + :keyword list[str] depends: + list of files that the extension depends on + + :keyword str language: + extension language (i.e. "c", "c++", "objc"). Will be detected + from the source extensions if not provided. + + :keyword bool optional: + specifies that a build failure in the extension should not abort the + build process, but simply not install the failing extension. + + :keyword bool py_limited_api: + opt-in flag for the usage of :doc:`Python's limited API `. + + :raises setuptools.errors.PlatformError: if 'runtime_library_dirs' is + specified on Windows. (since v63) + """ def __init__(self, name, sources, *args, **kw): # The *args is needed for compatibility as calls may use positional # arguments. py_limited_api may be set only via keyword. self.py_limited_api = kw.pop("py_limited_api", False) - _Extension.__init__(self, name, sources, *args, **kw) + super().__init__(name, sources, *args, **kw) def _convert_pyx_sources_to_lang(self): """ diff --git a/libs/common/setuptools/extern/__init__.py b/libs/common/setuptools/extern/__init__.py index e8c616f9..67c4a455 100644 --- a/libs/common/setuptools/extern/__init__.py +++ b/libs/common/setuptools/extern/__init__.py @@ -1,3 +1,4 @@ +import importlib.util import sys @@ -20,17 +21,10 @@ class VendorImporter: yield self.vendor_pkg + '.' yield '' - def find_module(self, fullname, path=None): - """ - Return self when fullname starts with root_name and the - target module is one vendored through this importer. - """ + def _module_matches_namespace(self, fullname): + """Figure out if the target module is vendored.""" root, base, target = fullname.partition(self.root_name + '.') - if root: - return - if not any(map(target.startswith, self.vendored_names)): - return - return self + return not root and any(map(target.startswith, self.vendored_names)) def load_module(self, fullname): """ @@ -43,13 +37,6 @@ class VendorImporter: __import__(extant) mod = sys.modules[extant] sys.modules[fullname] = mod - # mysterious hack: - # Remove the reference to the extant package/module - # on later Python versions to cause relative imports - # in the vendor package to resolve the same modules - # as those going through this importer. - if sys.version_info >= (3, ): - del sys.modules[extant] return mod except ImportError: pass @@ -61,6 +48,20 @@ class VendorImporter: "distribution.".format(**locals()) ) + def create_module(self, spec): + return self.load_module(spec.name) + + def exec_module(self, module): + pass + + def find_spec(self, fullname, path=None, target=None): + """Return a module spec for vendored names.""" + return ( + importlib.util.spec_from_loader(fullname, self) + if self._module_matches_namespace(fullname) + else None + ) + def install(self): """ Install this importer into sys.meta_path if not already present. @@ -69,5 +70,15 @@ class VendorImporter: sys.meta_path.append(self) -names = 'six', 'packaging', 'pyparsing', 'ordered_set', +names = ( + 'packaging', + 'ordered_set', + 'more_itertools', + 'importlib_metadata', + 'zipp', + 'importlib_resources', + 'jaraco', + 'typing_extensions', + 'tomli', +) VendorImporter(__name__, names, 'setuptools._vendor').install() diff --git a/libs/common/setuptools/glob.py b/libs/common/setuptools/glob.py index 9d7cbc5d..a184c0b6 100644 --- a/libs/common/setuptools/glob.py +++ b/libs/common/setuptools/glob.py @@ -47,6 +47,8 @@ def iglob(pathname, recursive=False): def _iglob(pathname, recursive): dirname, basename = os.path.split(pathname) + glob_in_dir = glob2 if recursive and _isrecursive(basename) else glob1 + if not has_magic(pathname): if basename: if os.path.lexists(pathname): @@ -56,13 +58,9 @@ def _iglob(pathname, recursive): if os.path.isdir(dirname): yield pathname return + if not dirname: - if recursive and _isrecursive(basename): - for x in glob2(dirname, basename): - yield x - else: - for x in glob1(dirname, basename): - yield x + yield from glob_in_dir(dirname, basename) return # `os.path.split()` returns the argument itself as a dirname if it is a # drive or UNC path. Prevent an infinite recursion if a drive or UNC path @@ -71,12 +69,7 @@ def _iglob(pathname, recursive): dirs = _iglob(dirname, recursive) else: dirs = [dirname] - if has_magic(basename): - if recursive and _isrecursive(basename): - glob_in_dir = glob2 - else: - glob_in_dir = glob1 - else: + if not has_magic(basename): glob_in_dir = glob0 for dirname in dirs: for name in glob_in_dir(dirname, basename): @@ -120,8 +113,7 @@ def glob0(dirname, basename): def glob2(dirname, pattern): assert _isrecursive(pattern) yield pattern[:0] - for x in _rlistdir(dirname): - yield x + yield from _rlistdir(dirname) # Recursively yields relative pathnames inside a literal directory. @@ -133,7 +125,7 @@ def _rlistdir(dirname): dirname = os.curdir try: names = os.listdir(dirname) - except os.error: + except OSError: return for x in names: yield x @@ -162,13 +154,12 @@ def _isrecursive(pattern): def escape(pathname): - """Escape all special characters. - """ + """Escape all special characters.""" # Escaping is done by wrapping any of "*?[" between square brackets. # Metacharacters do not work in the drive part and shouldn't be escaped. drive, pathname = os.path.splitdrive(pathname) if isinstance(pathname, bytes): - pathname = magic_check_bytes.sub(br'[\1]', pathname) + pathname = magic_check_bytes.sub(rb'[\1]', pathname) else: pathname = magic_check.sub(r'[\1]', pathname) return drive + pathname diff --git a/libs/common/setuptools/gui-32.exe b/libs/common/setuptools/gui-32.exe index f8d3509653ba8f80ca7f3aa7f95616142ba83a94..1eb430c6d614a5daea4139badc09c222a4b0e72a 100644 GIT binary patch literal 11776 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 z zuc4jxwz7KJ_rCH-tFJ@z@NXc!Qxa$m*N_NRtT_d&`a7duuH`>P zd%}h`&|B{GYny6$%@oA-ep8*S_YbNQ*wMBx)7fGDgK2FaWZ0dLJaOehDVhGlqZp`r z7Zz^Qt{~7!1nOpo+s>!!UDMjSGVG3o1-MTD`U{)X0)7~njK(aO!mRqVS*o4ZX4diz z7)@AzBH#*!OwC!#-^rCEBXGL5j{ilBGXRTvrZEnIJKR9see4J z?c)sQ$RrZUz7CZ}&@|&(WWQ6oZG7`cz^_)daDP69Az2FAzJQhYnWChD$L)$+G%bx z&7w9mR1|a&sE6y@t-J-J@>a|Gc{fUJ9G}Xg6OuprJK#0?Jp<5bfq@`8o;q|BAqcJM zjQ48!rGWu;JZ~b>4p%t2&K3ny&6 z)6|T!KS#l1EVxey4i&6w$J3D-fJnmY;zyL&4M}ieC4Y4zD_DwoiJ30 z5_=SJD^>f%DnzwDB3tkBl@`9nM7`62cB()9jX5~Dm1WqE>OH3SAe#W)`7_C8+pfMB zJFd=-^{P|*4uT0K)k$y3)D9UFllj~KNTvgXauGr@LJse7Q7R@RDA(z2H9$+ML+eE& zl=voVrX{czY;0=zrsg&^7y3DBQcnlbCHkTK6wlSv)Ot^a>WupS(t25KWYtdJD_Ul0 zy-WLUG9529T3YX>gnVr^CFHB&()t2Q@MyPDf=8_?tuNH(m)6hH=0j$@t^Sg!YDQJ1 zuYFT*)BGE?V&5z3C3>UFt~~e`G$NV?B%)>wUwRqg;i@z=IXRJXAM6bDgMFlKS|1}* zTJt0-&ot@>P~uYMKt_iv`@icGQ&50s{!#;tR+P0W?sZB=UJS z28Qw#@F%T&Xsr_aIZ!Op21>PA8)rgy4p7O3{6Pz%JAtoM$hIO)F4a7n)$ z761{^!~%XE(hSewuU#=}f4+5c{H|(n(tWZhp^o;Mq!< zRjo5}SyjYX;$XSHob{6zO6oY4v*QvB236~|OfFpmxC~b5@TKpZgpU&#G7W#1xq3O3 z<3MV!e|?(f)~nX1p%Pni43kl^-$5TcR@NVMSZL^H&E-&ixCRksAc zLU`VdHD75rv;+qczU;=DL2Y_V&_vjEBUm9@4-7a;8wVN=CKo8r`Ay}yo6Te;LW2km zCg&ma6+&MnuR~}6p@HNqtG1-l;zB9z8^>xc|3Wh`P+C9Ga0W~Xtd-{^<+-e)w&b4$ z@#5nT;nQH;igvjVF^ojjTuW_pKostir4{9NA29mEyNid}uN|4TxhrlC)WdXd>FZ z?h-VBx_toZ4Q;2-s*De{^r4;Sf;^URlfi%h+fm{Ob0O76slOabjS9;G-(|(y5k&(3 zek#h$5I=h*8r>7(VIL+i{Pd0V+%%S+M@0Bp@q8Q%5#q(@z7U^EjPS`!G$(+(`k}%- z#O*6nN~f#>J!8|-`3^7o1-QI(ZAuFGL9cj-g!Tk8}ZggIXanNhBaH* z%$w8Ym-akCd{i@ElJ?9)6rRw2KnzPg>MHL zWA%sB4CVRi!%2H|Ot>Z(icp)l{Aa9616{Nh!pveS`i2Ma03DLWEO3U&EX$~V4~xO) zi_s8B{5_ln-a`((@w7x)Y?Ng>9x2X(W=@XB{D&Y@N&83*@i)+~?fi2zqnK&lp^`u!hZ&&FuC{jXb#dH{4o*tBfc6Xo9PY^qOa0PMpSJ{ZCzqsyow}p zf%MA>yy z&-gy^>=Dmb#gmKYQSodQ&%=1~zFyPB`l*;#0}pG&_qGPaB!9U}cE=Aq(N(&^msURe%fvtfy@-U04P7ip72!ds&zS{&BQP zfb0S1(?^*E(%8XXe_@jn|0by6J>q*uiPa<2GTum>1O`T;OFUo1v-y$F@r)f;V$*<6 zxxSwOBxBbhyp$c;NNYJb+cR(3rm@O_gUW%XWqQ=+o~LhwQWXHG_$SW z5jNrvBb%>H`Q9&KJunO7*TYN%sn3?(GrjM9l7u$cB1!?on^i zxm~?p=dyZfRh62Dm=dqUXFWmia`&ynVMq6Z;jpdSi|}><(*!Z>E*$=p)}4=V)0bCj zv$1@#`k8GT@C_RK2^%GGo{Z!or=xEdC3Sy{6c(r8w_3+22VPE8$VUwk?|v1ZjJ?#d z?luIe*vr0NEPYiH|0;?VH0b^(Q6Pm!7br@3K$LQ`y0q!bh+5I~B~(@{BERM z?U4}bzJtJg>$C~wsYFPs)mz=A_+;Vl>b`0??CGA4aEpE3_1cuC2W)e-iRD9CL7-ID zLCiMic?H0A0^lhkGFc%~0KX@IHA?JFdf%(WUZeMSFj1hlro{Hsd$SVTOYdb$?3Z{O zdx;woaT2be^4!6ovG*{7T!u=A;%kW$=Y`c7EJ1>o*h`$ppM(Z)v6oxb##)uwlhE!L zK|BbE?rM}zjMBeG`2mMsRATo-#`XSMNL zPiK55szNTw;(m*0{!-DMiCyRLQJA!hU8fN=;!ohIB&twBXPo+q?3dk7A=(!wGR*;f zmH4Ab9Mw+-q9dQRF(aRtkO%#|sinU_GzQmLfG(6X%$CM}s#}Tu+JSZPpq9P+VJHV9 zPKiuBJL5!5YDD)oz~~%Qe-}8Rt@jtTDY45@HnsU*=;L2kq0UjBUo;Smkm)WFrzQsz zaZ(FGek(>;EF>{BP3w%4xKbs_@hyu6ngw8|fTKh!qlHy>F)CtYnXuY`0oli@9KP4p zxmNRteU+CaBSCFY-H#O=Jk~#|5j}R|7;01ZpAg)=bGW@hevqcf-LE5A?_aO{-~#Ga zVjtqE_ur%Jcu}N(Q~CZ}jI(RqYcK--f` z*$u-u^BYl7987l&tm;-akLp~@;>4P3jf|vh1&xdm!gT*1BCt>!eya-TOo@qvzBZ|e zQ2iNDWtptbp?AvNZz7_NZTj+?+C3IKAuc7urGmA#W*FkVeLpeU9(>ulfC;|b-cb+0 z5TB6^X%XtM(`pIQ=fw7l3m7PqEu?nW_-d^ex*@!pOr$qxsd${!Og_Ogsu`H35A(O_T{B-&NY!RG*-ckbdHk+HO0|vjjb;+l<6Mq$Ue>zCnpS z2ekn9jv3VFG&VekjGbcGz8tU@^*K}|I^kYGwg>=6O-KB9C~8h~{7t+%<45rXFG$@q z7euEagA%`$O73*@wt3Wii!!}!nDQtuEgDEVNO&H@L}t+dCE6duOzQXu&}83R+a_*t z_&PR>?K`O-m-^lvXQA4JXT_&C#wmJUf{F~PzJ;U$!y{?@r5_;)a ze{z;kSR(>#DXe7X%}ph+4-@QPELf`|eLpD~P<#ctkO^UZ+OJ**V<{Lc%j&ADlKD^D zh9X7D?5ESzvDO!l)qQ}Km>9K-c6Fh+qFvOf78^LViKdv`C4?Z?Mm>D}Ux7K>T~>yb3k%G<(9(Q-eiF; zW^X3gPV@i@BfZ3523R;XaoaM4t4g?fQVe|xA*Ok~9;8Dmc9>rVFv`@;FdHt*cs>|&PpyPe0UP`2eD=g zvFfgbQ|!MPHa(pX@+5W&jIJDok-l1%npPJ!4WXp3E&+NLPGjwF!I|Z_iN$Cc<=?U^ znZZOzzo$!rJI}YV`NpupW2zzj{GeLXVuu9W`n0TN!|A}^<;Os!&SP2^>!5w2kEXSK zlwqH1ZHplztSactN=M`gEK3rV&LEFnX(6w~j-W+mrHrb}^}uPE_qw+H$a{*Nr4ow8 zzFGz?FS2RJF{5dTqbb?YQR&zY>tcGecNr|O?N!1;-1-;v**su^4QMcbISfGyV8u(} zHrJScDG^rhPt&Lre=8-P)A48e6~K=WdCcfqdgpaqO6I^4`F zK}}d6kG*)cjinU7J8j5RgJojK+lx)wDSSUVPHfMn%&-B(Q)XB@^Sg$Yn#i#yh~@O~ zVsRFx43?7=Ef)2sPGY2yYNLx2@%IoSZ-cY2)IzclGvc!#BZ>GNJRx94d^Q3p^_h5& z!jF)M8oNlT7}k16tTxu}c%&amYj-5hh}SOCB5QZV4~f@Pt>X1d63xedAT%NiI1<&4 zPEnH$n$emj7>RQLVK)z0v#L&k)I^8W+9{AF*2UBSh?;rJK)tBMPMUdlAe0b@qx*u0 zz--_|=gQGEUJdhoI6@_ud5iH05LI|VzDc?VJ|^iFrVO)~h{mtX2Rs^&JPJgM^)vaFePM&_EvDU)I+oE9Fs07GIqHqX z11^%P9Ja(^f5Yo6;XnHbcrS5cpTmkjM)3ePJsfM5_ylButt7FO8?^&$xs!Gcs?X>b z2Gv#YpGi2Dv&9d&6BQ4+j6e@0KF|+?vzxumV=x1vQd_)ri+|f97U*XuQLFZPQzNv0 zA%k>}M&Ys)3L$~QjeLSY;hfdNb|6kIP96bux0l|%;oDvCM=09?jfL4?gx*}APLf3? zdW9{Oqqf`4JW7W@2etzEbQtSkrV7NztT#^ri)SK{5ncM`jbVKA(V8A zqm5NETDO0WB>jd|L}{&4iQSGss@PZfoA}gSfE3HzR_E;{tLUXvReu=XF_)L7-vPGW zI1T&ug(LuD|W&H7y!uIhCFTlmu0not*lf@ z%PpJ;soA9gr~1Dvt?jQ$qirwINSJ_!P(z8X|80r;trDZo$YvUmPe56~N*V7}HN7l` zUbJiFQ3s!dfm&=5g!m1pD2!1O-JKPJcN0a2?d;iL6=5p90XQYcAZI!V9BvPRgvII= zWVx{*aQ%P2W9=~sEz*<6$Ha^)DE+C zm#>U`NgC@|U)x7%!fC|bQJSw-Fsaw?)Kw+OUnVmHjbnB*a9TIrTV@F`=E$%dDJoE{ zNHOPT@UOs6VaxZVAY)PTUsB>f>;z*ISlRduY1A6QU9eATGOKj5!%ZL9;a7P+P4oXu zhQz9+kmfozzo;Lh`0P4(oZbabsc?{gTtRZ;^mW2kS?P?m-mmCgUm2CoWTw8v>Cs;? zS0SUm)`78mC2JotUs5$NFlJ#(0K^R^uLEPJpG_u$FQLQ_~`{8sIac%$yfJ|br?mbEn9!Zyl#plAg(29qyxaq993=Nu)WqY^=ggyWgg5_M&Y zpdmD4((h4i*n9jYW9dMOmd~&%XK$OXUQ@bM*2V_;Erb~neJY5aoK)H1r@w}B5jB_~LP z2GvBz@Gwye!c#g`n=Ob@$5oF-2yJ2=AEdmT4d;TyC9{qB$;>+bA$=O^jVu&HK4E_b zWIKwTm7;yh4(lJs-b$e-^uex8 z_YNtpTlEe_{|I}9wEOK#Uk`1z=?18z#e^6*kkn=swo*x(4YhC;wXpuQ?+@x&e6FkI z8K=b5&i4oHt`OV^Qc7$M*n^!!;^NY>CiIo+4e=k6IRnWQ{b0wsmK&RX%S`$|=X#ookhCNZGc? zMGp@>=Fr1Wk03o((_?+&r6#oIX6-0LNq?%hiiHo%0Lbwe>-T3`g2EIsFYSshpOGWKvb0B0J;;R3Pr9Ne=4_JFJCASN1ch-~a<)#uLsJH92a?)!t@ ziGq7585s9aau52IEp^!s7afJ`bq(Jt%A&4Fp#vW95D%=z4hro*uT^HX!3zQ!R7%dI z%{YlkWf*Ybj#f5>UUqM5dusBp-*XyMDxo5XAHRVjECJKc!11LP6L%wU4tUl+zKk7) z-tcbWELAvkSWx|4Lu$xv}(&QQafl&5^VedHR?41qOhCL(SzYfG{apR7rXi zehd6DB<&$TH((+Lff_Licu&>&&Z=;Xa&GeQ02a#831Q&@0{)cwt77%-W*x#g6dew3 zZ&xR^NH?~t(2;R}5E$jTfD_!&veX^B!!|{mD)!dLfiakI7!4&)nwbF?Q56J6xBCB<2Ts%>w%swm z5p;*KBsC>VeZc1WcEMA_>6oUa+}=pE|FnRHTlYl^yFJg$z<7}J3wq`~P0uM$(zEyp zdX_zo=h_{4hs7)BMe&;QsCcD6EMAxH6tAmx;PvNY z?pKA-Fd&Lp!bN`fM?ZqJfYZweK*9>n#u>pxsO*bYa7Ws&dJ+>Tb%xFz>O`IAsLm=O zQ2QL1+O_W+C!P+B$?f~bQkVu*9G$TNH?NtfET{|e3vWV$wJOgaW^Kk+2kj|ub+&!r z%5F<+b^ZM3KYxLSLd)A|w*O+oYkHMGSoBW;P+hf!CE(DpM0 z5b}`~H#WHA9D{t&+~_d#B52-Al#k5v7eFU(YjZ4}1Rw7A4d+_op8>QZP6-}Zt*%b& z`Wy+$bBC4Z?7qXBCKR>#gNcW8=zG+2J1;>KfMPkenBcs6613dtOvDF}1+@iHGXVyL zyW9I-&s!VRgnTfUyT5WT@?XTEPx7$YC8f{O>dh`&23to zF~!xgBb|y(j-~lg9wm7w2?aIp$RKhh<&KyLNYvB=$&f|G&iHAR^HX5#J#vKzvqvZ; z5zD1q_M?eAJ^F=7o19IHb5YANYaSx^JC#C#K4-ABlVk?97?-pKri`J`C^lj@Tbt2mo!F*JPJ?y@BF^sVe{vm+d zqdEL61~0Kn00=xne8s}G?|LjIF2RCpJ-QOp0mYg#shJ`Ey|aMdO+dz?2ouoA2GDf? z9U76r98&W8OgoJV_Ce35rr%IF@VKibjibJerNfk0;jX6-4r)_7(zBJ1RbB^Yju~&e}L^~@^yQUlTv1@ zBA9`54bp31Vp;A`Vs+FFo;0-R!Oux1PR36uu}UPq&R(Gd?_QH z-I&v|IKQB|xp^Xe=(awPG&MqF<&%bKZr+(s-#&t279BQ>_IM%5!-)So5yF^4AhqV( zL(&Wq!DjXrC3Eh!|EY z7vSS$K1aFuPf!CESr0vX5x~160L22pe2&WF2S?JMN02hMS{W-)vY$P42(hb(MT7jG z0Kgu46=5+oFX{|(T_hbv62&x8SSw;YiXi4Zi37hwjAfQJW6M;XSo$borC~ii8Pgl{ z23`)Za5%9Q4#YA!CT!oYBo>+6HO(c(p3ZS!CvGTNzSBX%-rEqrFFu3 z0Co?&&;<_o%rvUkg%%s5cxToQ5N>rh48y<;K;Ii;b9{a3 ztU9BFw-Hxj#G4%AwBo~BI7~y{qtquD^1>whtP>}mT4}6p>h;5OwHsqC9ZqIF)>vD) z9`m%V7;6i79wo0|ml|-tf?lQpw*fhjoj*v*f!0om%5|)ayzKeCsC3kNR>)f$KpTZ# z(oS2Gu8>(A12ijc0u{}-(1z)|n~*@Jn~B)-r;p}a=23i*SyMmcD|z_=^+VW1hTN%f z(vZ(5bO4ecS%Xg)sAi!w$^tEC9))hiq5*bPOw_*ztWpE_|GlaQ{!Z2H$A+rj`9D={ z=EZ=LI3$p&*UY0PvmQ`%vRUl96ePQckb_@ts@ZwX1kkaveV8H>K#_cc^bsVyzH^9H z=5C@AQ7jit-+@eej-XrjZy-qM+$X4WAH<%?*C+=za1i?FCX6GUl`D33`!UI0WNdYV zc!d@**%TtCdBS*zs2`zLnixwFCz2Rj*LOTbOR4gXhi*l@yt6VwDin(KJ|WcL2{ELQ z01xS2_@d%yBd;a^VFhp+mFvhrvzs^vVRPd;PL|GLdruy6@N~4G9q0j96kkkAf_QJX z2+%UYGU1xVL=^aR|05&-o+3oyB@x=T#j51j9Ez_8cDG*jM$lQ1uh>l_uohmV!0kO(LP#4N@EEUEoXInA56`O0t{sKJlZJrhT*oyhB*gICN!iv3O#j32> zek-=3jJlF4`2{6_TwNHotTB0O1lr;fG+}riY+8d}9p6U4L%mdI_0qplMx>#0CAM`P z^3JT|XEDzY`-GsY?(L>fDo!{8YcSNAFr^I_G8MT({BkOn2e5fU5+J&7BR1$EhzL7* z)C!{q|C&MXejRWO7HlQ95-6}@;>JkpheGE@o~8F5C;HEPEAq66kR&1Ugosejns4c4 z1cAIHP*Ykbt&Ao)n-mt{*6AhKP?jY%94~Hblx12JK-Y@>_8|Ya z@ic!yo#WtT9ZhQv^f%X^?+AQJXI8yOn(O;J0_UZLCI zvK2;A{g4N$!BrACM+=}HS^&Y8>{gx+49pBTn;Or7&0)~d?^^%W(6Xq8yvIX)Ll=!e z*wS={pMFrA$mhcL+bNOhSZs5^_4yh!1ui~0e3JMy1D}!~Vl@W`hY4^|f7+$QzK1ln zMAo|oja+PzpfJ7bbNw(p+ns=bCHrT>9ey@n*N$Ez=Xur1SBo$?&gYQTNOpk^Xaw}_ zR6l~)D4|tHof2!J(sAHyexk~T(_~BXi~4W&UBF?rtyAjg)El2yL=?b=>p-$vKkPxR zwAFGyjIrd9F_|1PCa^X*UbAC3yDeO=Q^&Sbr?DL#6@K`&wKcp2YIo*AFcyszm!j5| zYPnfXPJl+OgQ-YV_ZoaNtm<&qO3g~q3GRleK3%mOhj1-}V-2>KW!mcyelxy;ubQEC z)hx0P>gL3T&+t(6O=xD+&fle0>-{z*HrGlxLJ6P* z6xe^eG3%&($pfjV<2y?PZeXVz>$Lmt-X}S6iyKo8lmZ5udmZUzmo0=mihCbW!DW$U zC?|3ujnvSR;S!V~*Z7@Q8ITD0$oqlgyp1Ix{w_Jpf9A7yMC~ukowZPk+<`)h4#N-~ zx`B|O;c=|D*FvM(Dgs8t-bfH|@N`=*_|`ds>J=6Y_VcmpvIB$y(5+twa-`bh^4O%v zERS{8j64{(^7QTCPawj{E9(rUYit}h7g@Mp(B+rD%YhBM7<1yhjko^ zmY)OsH;9v_@%1SW(nOfOU-XAWxkK-FG;FHl#i#~n`^z0+U;l=xeZq~Ye?uDUw0FXS zq=3~1_=XRtBH%J1u?Slf4StbYpGsA)ZM%?$#y!g4gc&=$hmLyDlC={t181roA^xKH zK*znnonf-!iY8+`hF#XfJ0bma#_17&frO%jJp_&EKzcMEXZ^8tMkn$yLF%Dl`Yw>4 z?>r1>nzNv;ej>%FDeTauQzHP|`F8+mk%?fR2YJXB3A>$Dv}_6O>pJI`4$z|xdtn_L z6oykV;-p@u!#CLQh0w8~eVm}^@jpS;!SMOKAImQEat9glJ8{GzLpNtNa1>+tdtj3z zb%M&K;`9!1SUAt#w!K80p86b@7Gy)H)|OV~D-R!J2Zb++b^AohUj#H{RrBnJmFE|_ zYeUNO-_7tI$E`+ke!O?%WY*}!{;KbMLl#>m+u!kBXc%*o-a5Rq4TZF7J( zuYC{P;2|#eZ$@ns1XCPM;#jMHR0+Iqo+R;gfNhVIEl0M?$&$E-bVmD-o(%ETU_qK5 zT9z0VTCrP2XVN;7yg+nn}yeXlfp_N`W@{h;sg2D!9UbKq>XwL38e zq{ncRI$BE>X#GOE<|NlX;M7fa82thi>H7$PRKC9C24uAi5c_&!R{iJ)Q_ zaOio=e%|+XW8t@sIN8<}`Wl?tU}fU-6#9IV{SQFMcVf#QS^WTZz_zX_`#$!*w5-m` zH6-xKm1R4J;@c^{qzuMH>wApi^UHoT6pvH<>axU8{6UIOE&IVx{2_|xmi>_8nJB*n zadYDu>~fw68(Y`FEdh`-aY0k5DhzSZlrYqH+z^mR0xLDTKk@=9OZhIIN2I@h;?I4VwyW0G+f1n&T$xSJly z)#j!Z>;$g|Bg4t3LuMJtJ6XHV6?LA@Gt{CgEVf(T88SN!jZ-e9VBAUm#{oibH$9RQ z4p5tS(<3?N0JVBIJyKhjK|TR(Falj++}F_91H2Y(BM>`j-*@0pxZq2!_fd z?y@N3(^ z%P&G^^+@ezF-7zQ!m|l?sHj(CaaV|o+_Jn!u--yr&%?AHVFkK)fvVRhFEUM$v!Pjt!3mawm z$cOr0u}Y{--h>0H$iPmPH_a~#tJg+twfrpT3RoIRmxOAAyzy!<5uD&a$ss{`>32d< zFhttVlHvaaQ((lOBmugVkdySwv9Nm*6o6ntcZQ)%Aof&0-zuOeDA7Fov^5QaM?$T) zHDqM6KVt{HldRJaBw5WOT@a8R#&`%%)BG8l3pXwW2L5XXF21XzDf>J#6V3{9OGa}V ze3hInQ%(rcr%lZo5J{5?QF>~1I}h!B`QF5u~Rs2ipwChpEX_Z;6|?t zS=vuglB44$6TCJcp=C;}8)#79sg8MBT1I8^?2_b%;sY6R>Fg;G#63WSpv$!3ShV*@ zGOco9)BF|cdBXNG>;YmXNOw+PuhiC5G6Ta+Pcp~b3eTUw0Nvgf7&z7qU(Rtii^|hh z+=K=l(Y~OzfCbd00!JAr+&V8yU4-lV%5dg32;iCgT~aG(WKK&4nrAi6#7b?brO6!r zd36tj-g!*n>Ku>RA*;8K@h7Y zXIh3Wy??VdCYrWv4}HK5RiXqes^Z%LMDA8rR&n*l%Sd9KYfGo8xqkmz7~juZuRpWm zXHXlQLW(+TkM;Y5b-30gaL#-SE+?SMHSnB!6a5C_AU3@g%m04N%g+IdY#Zd^Il#kc zJNa;7VgM`BFHjt7Pp*J_y$X}Q_Mn;fG$r-;&ML76&=B|Mj3IB23-stM>hK3q7yl4) z3c&~3PMC6^L=NGYg!)2t{NIa&T&F&eW9ZP*o&*eo19&q+r=wu++=r}t$W0CCrI8Bt z?;&^5lp@9Mtk@yd@97tUQ(O1al8^lV4HFH{2Y0GD@pd(<@8}+KbV#noom6OT-m8SZ zHsICz&Ah`1dwVQ1AiWQXI3})uYbChAId7oH+XLUP%mcTfl2|s9s?}qu+GD(o?7bga`z(b7AVKfwQ9bd&7(*ohyh+`4}Ub+Og zv~|&8Yi1q(z`|cSP+@cEU4GcPtrj1);c|rZ&7h1mZVgY->F%t)Hmt1SgWY1&+h`wk ziIt#zPP^Pv%D*f1Vm5JwRO$jLT-;(^AH~_i0pz?cc3Lg`8R!Yedb}i4O-sI(SZGo$ zMQ!bgg@ePPuZBYdsgTgG=p#sh=EN=;YjpX}YHr_!jV{m#ESP4%jjCI$Fh$&sGdARG zV{Y3xncoc?+o-#V&cN^r^5AYFTt<{n8}c7wSq7U?=`yzxe;l~sE+qF0w9H+L-P`LS zyb5Z{uB#34r~ixcI=Kr)c1o~lY7N}$NT3DGrK4abA)Kgo*3{O8qP9e}yQbEtcfuZK=8>=> zqZ=+=N_-_{sg~iAwcoHMUl`H~|DeR_&;rTZH|c#rd1w{h)U0FwDVo)N8{&f24QDbFm0TU4)q%80Ig4cVPW_N8w!k%Rwl;KX1G`F?VBP#ecb2HVzT!58yi4SA`b?HokcpJnUbfZl{PF zk>oRLejvmQH=%*0+DR7r7CLCtbRWUtdQMc0GX~zneB53WmY7JsxgPxBf|Zod2bsaC z^#TUXFw*vsD8s3eZn3<={BD8y-F)-Avv^(#5HmvD4qVGVp>f@NoD6p6G0b_;>7TGK zSQ~alR?VS_5WXJ4chmd`;}eKP*Ud!gqJH>H{=^E&IvG)+-cV%M^_&01SS0H0MKv$grs5Or# ze{;CeD&O0U=GE4*vNezey^K^nxg<}=whvsAzk~U#Wx3i9o(+e0lk$hTOUuO;4{qj4 zl2>04XBKhf3p<6i#H3_&!u-@$Y5C=joC$cF{3W!jqt2D3>B5^fj~M$Vm|SQkqX41q z2T%b2Y3>2D36oLt^mS3MHXxT;nz5fClr6_(g z&5ZNmC;~14*6HL!T?_*!%vVHtjCz-|@_{NWfYVq9UHf&K-&hC=^N&yg7CXr8M9E-I zy78zABU=W%n&G@W?8Qu0LFxuGkGjMv)ARK*Kbna$O|6T+L`^#69$NTe%8totm!w@g zstZths1|A@RqXFjEbE6;4?L#pWi+}9BOlnJ@if*Y@t06S%G-H%h(Gyfd?E*y<6uV~ z#6AVi5o+s34s={NLIlf5uA;m&lJFu6NR3z>mHe*2h>?FG+|6B3U|-OciP^-Shp#}#vXgWHA5YNa6U!+q zq};yuH@J$N+-9bU!#^pzU+qcXRI%2RJ6N!&X5ogfS!cW}_M>(lIwZ zfe*Ebf@|4$_;a(+fU&e6F5DR2dJoz(we3sCE&7)WHrk^L?qs(*e7DNlO|*U1q<`tz zFp0fyeZ{_t!7Obi5STtGS&+D;Yxv9K`^c{aAF<4kr-vQzf@8HZTke1_ zmA(3$ai@cpRCwMl!x0N;(N4*zTI>7u4{b*MIVBEz6z)~*XZ8JU7aY+A;K^H8`rhA| z#@@HXm?m-|yYDTeyybfrCsN?||6PagyRzmxAaK6m*)Wm4a^kbTx2CJWcd^}}O(&$T zOD1is$|nkYqPH#_KxLQx{SSvHo)AToTevB1O*7qscSN~{T$U_eed zkFhYIW!is2{v~+Ic>0#e+UgdNtGQYkY->h?AtOhv79Yn zC|3L;L^vY(C8_NL#a`w7Z<;&Q)?kGqzKblWva^D+h~g})^-+JanYz>}7pa3)3H#&j%?M%nM&-lef!)5j zxF+{ot!{W}P%Xn+lGGUvThXOjoAq?c<+5_^5yIE&whQ>kp@q=!7ai>|DzP=9c19f$ z$s>&8F1nuZB+A21Ac`DkZgdS-L#<8zL|-DCxMORp!%Qc{SfvY7W`--&hwRbd0Jad8 zc=lZv7M)4Ey|on+;3sDoV)i>|hh75n`- zH-jEcA%g)`CS%Vo^jhM_(t0R?r8p(9shquB^hR5^6FWQ$^{ReTZ$6`7g^<`efS2LI z`*Ubd|3D8#gO1K7jsQi{X>oV6_6pY4m`A6R=Sku=CoWqz7RrfR5Ri?94t>qPR0wyK z7ypI$rKPgGC^KCCKePnH(pwNhEInLUcsSYH zMK#c96Wcyf*vntjXy@2%131BRv+s+&8T)^0jzv~DGRt=!UY=RF%PA!+PSEVc;+x04jyWuz`9C8z0a zP;et3AKyt09HrxKlTn%hWp|r{ZIg}rF;RCFy>6=>AcKtZ{igs;$2D+d$8_A5SbQzE zWQCGl#p=%`3N9G+E+|OKU+*%)vT>_}G|H_qp1!cG)wL|ngccc3S|rnlI+%#ZR zT-V<{52V9tuLLh8L3{Ji5gV__imv8s%5AodpfBay=|iYK@SFKaA)n! z`gu>Nt}$DG-8}J`UfpjdbHH}`%ci&Y#3wXN=Lo&`4(0{54(6M=w14Jc_S@PRz1T~Rl^A0wq2=ksVQv3&T--P-z znVBn^D-8S%Dw>y7pTWRCJv%uY(qn<`5JRE`J$=%kf*e{lfB-uER!3^0(2sg#_74u@ zeg`UK|3HdCiDBCf3TcQlZ;=fE)DVDCBd73MX>n%uU>mry8C=>pv#Bv#(y|5XL25qF z^05&n9mv|!TtSltfaHuYXx0NX=SsY2p}M3?Oo~o?mUROZ8H~u;#u#JqSQ2{ZLaoPs zjN}?g*Fmh$vE0P{He)`F%a{13&^QZnW3DA83tFarDJ79wHRQxiju9p&yOE5s7iX5S zPAT9u2VnQ0f2q4R-q|na&DrhAn{dUUuHF#hhY!*=#Yui>7P*An_97irPU5O2oo*Uy zOh-vz=E?#LyJLd@1MDHwJ>lqR{3b&uuKRc$ zRa&(RM0m(TfwmKzbj_mbq{47k@OqTc9^%A+hT{dTmTLg5;Yh9^SeHWDVf^ zPG5p0ObJX>BS$}QtpRL@Mtm;(zl^;l;yDM;Qq3i-!QHSe;4YHOc?FQc!u3kLQijC| zsD%F~sDR}K4dDj>ip4gzraN(+OJc5dkxPd4`v&&TmSu%$r;c7Q_Rd1_&ATqgv*|(_ z?NHdXIT(ccj?t#VW&9LM1V(fCO9+gvYLQh{cRA|8$m z-~lI6RXK*E5J9AvdGFyn+a;(a3c&7Xd>(S*x&q~)n?QFXUV&&!oZ5%W|Ki_-47X%6 z(Q0oier1I=N8(f&F4phVH{(93yq4hH=B4MFtN%i`>qOJ&mZjva%7L~Zf16w=u@t|N zC8*A#SM1f;Df0UcD-S(|f&m-%BOMFxd07fk6SCe7GO?X$W$1$etD()gv9Vi~;F zCn%}JBUFzlG%bavdIc_e2^!)%?=Kt;>=SrU%PeegG`3XKr#yK6E3D-&$9I<7GTy?n z`3_|+%QY&LlI~o5@E#!+04sw(UjlbAOA19tfaBt{6O-buYH*haS#ZIU;3SqHLg-Hs zuSrFMHxltGM10k*4W;Z6`f7@B}+rAq7FL4k^cPF$PXBT7m8RsSpzmmpDjw z(ki70#|jhi*+>t9d8k}VN=CZ*CV?+O*aWS7?aGcDMH*FIBw7N4g!15Gl-=#Y7fUc8 z@=E*|8dge8sz&-qlL!y}Da!v>O{!#%h_6;(D$kEwxNxnGW=+sVv(lnD%hwwDe!ni- zoR)g6HC%rGcEK}))V{s{`}Tc9qC{HC`gjazkX!(kNl;e$`2}+?sVj5N5W~RbMG#Yeilh*{Kq7N- z`TBlJleBgEegUIi6-{4RDkK!Ye(|3$(WdsYeuJPfC%GUcy$8s6o4ht97ee3rVQ>{3 z*i>?fSUVT;29du2q~QO6pzaa7^iC!aDH2SyYB^>J-q%+0le@$TI#;BJhU*x>X_1dz zx5<3Im6y*H#lbF0#fZf#2J+6~4Y=t%4*)nya{)$p3vFvi*Ad5XiK~d{2YC_&;{G)_ z^N738ShjLt@wE>91DpC%ke8C8!RXHHy%lqCamNHAt94P%)%{coTzgL^C-6sytKd%{ zXq3?0V#s7l7}AWv0d&MKAn8;p*_K`XXxr1skZRj_e%o+C)TVz&PM8vp$=Ak8g~#pgOEkaztzB*z)dvpU#TW*zC*i%^otfUrgsgxN5v5AXO1A$2ZMX_kg%wV(7t+Gz<}TVG4u+y55@fqQ~6UsY}D@M)fS$(ouQTV5b`>jrzVexEzt|w)aI#N zy*R^HVsFpgJqzGszw-<~`_IG)*zc4z>|D6(fMAI483X=4!x@xnA5Z%tk@9F=du4^mXSwa*9zdvm_ucS4CD1|OA7qubHlHmx|ZnXXEN7wgnS z;0*lz@p~IMQ+O2fS>f%E3)S)CGy@y{NI!rx@H7_Z?IdD!#rd6>sbX_x)DhIFP=QW{8&p4&QuZtn=V zZZ64JWj}sasaHP&)^HcKRrvz$Mw{OVxOWpg+%}ZhFHktf{@9bmBIHp*J5%CknLM~! zDg$THjev(0pF!ntz^E@IzYsSTJS0hu-vSnn7@Eg&KT%>oK*H8?Yd@n8?Q0LdAhvwJ6fe`RYRwH-s~!y=QFLVp5(V+N``2PuwrW)S-D;7ncuuNm@@yQl^5 zq{4{+04@|hEdqVZ!7$Z_Giqz;*Q^}1waE+%5ds8dJ=VAn`)kNLqK&-#SD1*x6dLXh zi>|>AN)PEo(K~LOaHQYF8ty96%N`FY>%bYTCBzzVI`a7f9wl}PErhQVybREN)Ngz~ zK(XBinxh53W5rw$6x7C7i=e;-u05IF-tOm-duy5A-?ga(-DGv@1pdNwP-OsaOTX{T z6jbRHRG||$U!zJtr~(%S^;t9)hal$sQ0PuX&ztZJw0smo9EP4mYn}Lg zE^>m6i=>XkJzX#^h#3U`@gu{ROkxZINommdMu`JO2f|PrvQbQc$+@G%oE*SJV!9|q$nP8I z6q4UgyoLO71cdzNgDEnF{N|6yuZQHrRF!-bZb3l^*8N6734 zE>CLSUJ?$0JlMN{egkf}CFo+la0=L)c$Q$ zUfysYQH_xMymQ19{rHMwSr7e+IHEIg&za%wfAmLxqx*k|M0C99esJQ&eLrE4S_+%) zUwg>Vbb$Q-w?hbVkqe)I`pk_o&lPVc&k%1HAN&tWck^EH&gY-e`+EMdh#!v9UY=kcH7tsnB68~yxYkyOEVh<6o_iT7f@ zMZAMt74JLvI`Lk{*NFEDzCyfL^E-aqJUeD)>x5{UW_hw!w-dlJ9 z-h{$)P2e(~OR3MrC}3XE}-^0h*?;$R@I?@Z;n!79b&OJ9~sxztK=`_fmWQpQ^;`M&hksT7-)Qs7Hp zlS=su&r1?|-{HaPr;z-S7Q8-#O6UW^C%za^;g}z92r4(tvF!fmr5a zJS;8b)P|e0exUHohGYxhZ`mP@AX0KDZ5H&@jzzaO0|%#HqT8=uV2JGLdyRwY6Rw{P zZfILze29pq3yoW+h-X>*`ylx9UblY0a`M9B*I1homJT+iV-t39e{gq<^GEivs4|2< zxIctH(uR%w)Tfph=Ogy9)$eh8aj!dan?uoa!GU_A&X^QuR$}#!sT!$NiInD|WsypK z@cl@oUX5VR2hjPJdRQURhZNc?IBxwa}Ch{Aa>SxA)w3SZ@#Yhsy4 zP|l_8>llZfjds`wlS(vm=`-E#+XE-j-OE!V~k5Uu8(XsT{F^SjbV5Wo>62o zT<|wAW1Dc?Ktd9tk(*OB#{DS-|bmL}j7PX|FWyW+mHw#8tcSev`A9oJxVHI)r zIzJC}fBtuzsb`lhHyq2B7q(vsO*?GTbSPF)F~!QACEpi5d@MBfo5$}?)3ya#pOeb^ z+wDFs;M#2aFzVB}Ee+c~O(*3$?mBTD{FwqQ1;$A8#-k^weojo|>{!yRpA+kEvH4q7 z>MwSu&baIjt3t*2TVnmKu~LS|yF+cW!eGx;N{A6zzSehtC5^Ypb04q^cm{Y9*a18Q z+y?|QzjnMK^RDB#Ca#Hl0`~-N2W|)MN!*jTow%L2@I~+HYO)IpN3(UXHo2uY>8 z0LRzUv=IOkf7x;r-b;<6pRL-5ePmunw+PJ<3EQM!11~D2E8GcVdpcp@Cm%l6MZUG) zAeYeTH)!c(9!V?GCugianJ9g-g|ZMr0&lyA=VyR6pmDZs%%S=@HvfC7_1;&l_b*XN zOWDF4X9zb&)&27-M#UiQDHLcXkO|BK76Uf} z#lTvCwjM!SkHAgBO~M_5i$(9Rxo{B{{aPX}0;*qg;5u;axG3t6?i;I(wvpa_zz*P- zl6ItTX4`0isJ>9|)HbRgs2gD{zg~S8nQXY9Z@mqK)Iy6ygSF6p0HGslrCqpCm`1G2 z;9Z;(^RWclWeyq46nhzTuGJW9#yt`t)dX4tuLo}cfojU>0>2U&dF`0O*a&!`g`0xV z_4k;kA7(QOzN}0Egl%J6RIw(gU$yQ}!0lkN%H_SXAtlK|yb2Nn4zyTm#DsuFp&Ma7 zD86p=D&kt?qCiXFwf2KdgFYlWA0Z&oE$t3yk?7jCs|_Kz@3TpCaH_7c61cce0^hR| zfE^y#9lXh7R=MOj)kDYw_3Jrdm_JacpQ{0d!b{qMmzevB9VT=h;!((XN0kPz2uUxI znxI8Eu%ykLM9zxn_0N)pg_>Bl_LQ`Z`7HfVfMfuoFEsK%|J+1JYkHCh$OH%TVsAA&K4fHf7Uk66I`ltZsj&7R0VDxhlW0=Fkw-#@dXy@ zu!@b7A95+hI%W^S*JI9mhC12D9vA;dB$?1_9`icO^Puv)C+vBd<@uEIyf5rI5YK`~ z9^#E!3@LfgO5S6Bgp7W{BM;)gUH*W%EJztC!Sp#EGnYuAsq%&%{n?U&=mI&VUx|R@ z1a*oS)|At^uneK~6R^KLq1Q>g-zjw58~y8YXd<^3OxZ5wBHd(iksOFkOUX!ORB!u+=f$A>*d;LXqo()}ik#PvqOcQxo7xa^` z@U5Mxjg)?i`Azae-;PKbp!Cpg?s<&Vxbtd;>g7S8Gt!{6CPg@Gm!dqdbrnApUK0RyqDO0h8WWLVO``+2=Y<3G|DjLB=$9ia`_xPL_ArhHO^tYf=jil8$%&$eMWkI zi4vc`?|vp2)R?@>G_6q1mZ(4el)V47>MBBZ*W`WXWm}cJzboLGuqfaeyGU%~LYr}X zO59&AF>v!?iHD2!50OdOri9fKdp%8iV} z+*$}E{;UCe_Hu1u!_T<4aItl7A@gSrbFQo>^01tT;L}p!%(riK?L1{NizEOZ!g>MFyY+=aimhXD~B5Pl#LWVaj*8TN+T5|=FWEG;N3xQQDI zp@R`>{}80hh1PPy9JfV?0WL60S@XFHgl;qAN^|vty=6Q;f{xDws;%i1O)wTw7-IVo z7Oj+;A$lT+eC&q({2jXq%NZwf8%HrWFxKvW_Qw=GX5+;|faYRmnZsj>B|O3~3NX%n z_ddS!0S!0TV{e-=9M^d1oM3D1$5$Es{5eUnLBt*=8a6zktU`~x^G5O%`pcH<)x%il zT`4@k75PH#$H`DPvxY#6hn&+GKXV<{Jf_V9jV=?aCN2TCS58VA02|^dqCPIZ-x?;7#1{bN-}o zi0uuSK2r4nwDHiU9o!Ay5o65qx5euH>!5ZZySBDJwVVjmf6aLFMYs^BvXWw2H3q!~ z(;%lS6m;T)pvO`cGg}L5FC9yR#x_hBf8BPvu&Y-G!c+(*MZzTa`h*7T?%V$yJG&R< zlsGYzZp4?Y8_s}3d(e-V;|z>mx-JBb`a7IgHZbhZcV4;YyWqYN+&KEYvg11nH-1#U zgCkE6_Zj?-0}fug&mf<5UXj$nXS>6m`@EvcaNhGuIE?^Ftplon5?}?e6z~Aq066a7 z;k+W51wvBk9|O+-FN#kDC;q>7UP*pP@>S=Rw(p(yyfTGPa-t#dwoIN&fNenJjB(EM ziiG}r=M|N1B&}|&{TYjGTJnR>t)#{$@V%5uk7VPX)tx)}9i~;_$vBro~X_@fGK`p*c(6Shm z_ccfy4kG%9JhMigIdnL{Oju?TtP=+pgkUA)nQwrAeEPsq(87sB6bdBfn??76cEAp| zFgA55t4gq}O8mn|j^XANy!bhC48jd_s9~TBmfYvWp%H)+$2)KWtZ>$eqk?x*}%En;RExS~IXSp9J;Iv|J~YrNURrg*tQC773oWE%2dA{FNFz}RpRg_uvaG0X<4 z)KO#ha9-1rjzt~`h)KCbm8#yvWnIKul`Kc%2BF2HVwY^#;84=0h8L9xUmS)sI5efu zrMsq&67AV?*ESC6u?BQ53x=+at{vtpUy=Tn>%hjPRv@fb>>NZei@|TH*Pe_fyaRH> z+qn}v>wgrKRZayp#0=C6%HTf}vvC}PLL1zZe+v)J`OV#n=)i?}W&PEaUEz{$-9>27 zp&VDLisExmUlyYe57bJ0b^X`NPKqF`ALem;0ng^WuokSF$I*omA&wcc<->L*C)w^$ z#@105(>pikRtXe*PBn`NCWH?v<}230wAUWEut~0FW8dub!7=*+d&g-odQ$iK5(3Qy z_h7xtK6cMla=P5A1>046G*w|;{F2`5r2AUC14SawNdSxguK5Tff1wp(ReX7WYCr5Ogjhy&`?wYGR z=ANe%{=|N?Z*Zu2VNWTB^VlE?Ocdog(hMR#lw^kPwpNPcxZNv7g4Sid) z6wVlH{)&i*#y*M@7L64NAM;8{S4rUpV*{F;2Dw!$>r^WrA`-cQ)8U#`$0fv znZuaInX8j&uMF()eo2pcLnnx>(zYf-IaoN1od1%^SY&iYDsf*+$~R27Y08`qCv9kw zOjU%BzDgnXV4bl>PIk|Hi{z}OM`r1#lo2###z@=|#HAWZB~MBt)U+%SQ46WK zB&rYRMQY-2Nega9LlI`8$l&K}0|k3jgm`SaHx-?&M0K8 zpVK~(`KfGoUd_k~D_z%%ni5q-x@~s`2G{LYmD*i>aUc7g{$0pyv;}|H{B9h!nN)WL zUiKfmwE0-SaEG;II_xp|W(#Pq)Xsjc&7=7)dXaWM%_h<lRvOXO z85-I}-KDi;2ThPg+FW5{1GBi~x37s}lTPVLNDgi}h!h;*XoQB5g8>Z+<530+()tZK zFJd{Zq2?7VEIGFRYp3 zk*$D3t&n7nnB$*kl5`ZzPCdQxrn<9=cb(gmIV~)raJ6}nWV089VtQEacB93s}thilfElNyKiX5FB zh20b=d=UdqBPF8|xe|g0#4%;}rNMjB4)Fa%gu-8S<#aM?jA+JXZZks&=UkaMtsY8^M%zQqUB);D>DSY`Fu^Sbnz z9EH?R_5+6qyE$#m!}kwpE@*%Aj0mNMed8m(d-3J$gc?6^mj*7%!t#ONljFiJRIp#u zw`n$PCsp?OyU0~523dloHJmcFbU zP~8$~Hm(%6$A0)&fb!Z@qM~U}s(4aSiKMN|60DmM&JR=xyNS9Y5{cTQLKM`#N~?$Q zo0C4SFd!5($($SLEhu>i$`o5mG-d%t7uwW*Kd}{0RewR9?YS|sW`dc}C;Hbv9UcDh ziZCuU5_E%s?J)f;3)E6_$qeH*!BiRx(LTW&J?5NP%1SGDICsWdK2z~QIB`xW$E7>K z;_T?p{nv?5AA`?EQ&$y+s*d;QL_}$vSwe}zd#92F?PyRHRFw)|o?;~GN9$@_QpL50 zmld|RlMRz5f)(wwup+itb$P<(DYKQ(5NRdz6g_+d$jKvuobFKwFjsu#0fOAh6Kav3!dXq z?80KUg~bXBPJ0m=Vx*8_SeLKkt19#q93Pg=6hqVamD`4n}uFnm#d z-PMxyNw@NAd()E6GTWks!eGk_RjC4-b#F+Uj1@sg>J}2h;?As2y}xs3&Y9*m$AIQu z%CF^|W3A_kzLm?mJYc_`1BZ|K{dD@z{%NOMXcprWjyJ~Zm&45;17{F6_KbIZ{bu}e zZEWm2Gg^7t!&A$QHqPbkF~*_E`)9Q2{lOhWAz$q2Hv-K!375J1@D*NnHdIKnx(>RWaAK)m75saoPQOP!}E< ze1oA{77AS_p%^*SP=cQ4F^^FR8A&yRA*$-stIIql@yG$)hLVY~J-k8+UUo_X?2-UM z371>VH8VBt}wcFL?3AnC^RvY2N?V43;m0q+?)mX(uQ zq0UY|3&z$*Xj!~joxy-y8^^P}1W>JPEimlCNvW@I9L4Elk$Dq-frAANOOk>YK&1}V zyv^VeArC9o6YOa ztq(}POI+yjj9uDpkXY(L=UuCDxd^z?US;MKty& zqGQGZ=N%wsAuIB+;7gXkrXY{5TxbhO8@?u2qF;d{xFy6G{I!TRZ+&ZHnkB3Jp~xyD zt~uP1+KQa@_)|34UWyzgXZ`3-1_)l!IBlC{*+^9KIJfK|Swu41)K-aUUX`gVK zj-MbS2)iEdE)9a7U)gwlRQ}V#`Cnu{{t@|iL4fAIVq0 zSiD|Q1yX!hHJmt9k~u!L34tz=Iv!Bbg~%oQ*tDag5`PK7=eUZUS9p}s(3~%va&`GH@`wk7UTQ#F4tl7D>yozE_0YEh!wNxgDVXT z^lP-oqmXtastbojFsL^IEfeDeUu*7+J$*!Qsh)S%Q^CX+qM#iF>Sf01?38#!8=LKE z{uIqPotIW-_m~Bn)v%J~8DuZ1tiSmtofaH~-8AOB(pWEA+eHby5gd&=z^}3FcG=(Id)dkFi2JZ*0m)g_4diCv&o6S-8O*OjcG)lN*C_|DKe> zPUqJ9SW6KAxSHWn5Kcn>eM6EJ-?)%Z7=huFBnRnrPXof{k`og8l=P{IV&b^VyoD|m z-KGT_7GW-We$$j+A=;cs!xfMT>ZV1t5G~P=q!3VqaOJgQPSccUuom4x2BMF(tjvz2 zf+TKk!b_0IJ^GU1d{xf38J4LZ*TkOwL(`mC)S}%vjX1L;p3^S`7*Cl!95*8p*SX~a zK8Oz2#Ag}?i^>ipZHB2zN*k?1rwGJWr9UgJAPqSn#-g-1&3$uTp7|uwx8k2~e(-8| zjOha{LEEVit?4$=cF;Pp#g=t~yHuy&7{34Xp)vawvNKLlJEP(B=bXgCWlaP(%s0=F zg*1uI$-c`BN`@FXpiQ$*wwKU`;wzKQ@?{&$m4=l;${>=7EF$sgij8i%C|{sscAoiz zCwZ{SeHl{%nV_`31>ORATngM8mTc+X_hl7PSLVJ^ta6nbg~kN)I2DYZ@a0y8qvt3E z(GfB`Dbz_0IEfzfF1o0o05xVi51q=qcBEauB(2dke2I4vFvme2^slp8n#QjKhFSgw`}{Rtuy`-1-Rmi_v|u&`}#z>)mGp5{Ng z@&+6UB>Xyb_UuLkUQbVc0qM*${trU_j?meh>y_ZW%a&VZz8-;Dihlhk zmctry)1J_{gP^dEB9 zbgEKdd%5{4AsUj*U*LobqX^v@l7L#!+7}W_G4Jv}Magf>wu>%_A?96HDh7^~U9ha~ zFZAc8wI1j)Tuw_`c9Ao9xU*#o~1#2$fy~hb z7ztQga~5kD9qc(0cw7QlgM=I}A%{uGA(4=TV)Kwt;}f_zV{%Gzc>?jFDg8o2uT)Eu zbIVs`dx28+g7eNQ9=Z4K{OYaZ7axNjI_?0U(rTSsL~kVdf_q;?z6`5@+={GCNigDS z9jKw%ROkZ%zM_bzwPMM@T4? zpg-GU8yJXh%n70CCN4NGweY0TPknd@d&?n?V)W6GSER#T%G*x(49X+gK{n4};01>U z;;q`JNga^`YK)=m+{({7DIGu^om-`bf;kJ7;l{=RTlTN(m(hL)FB}B0bjwk*)4u6K zGWQL-(YbR#TJ5uKkd!ptY`oC9^MLbL4f4t7EMbB`R_1o$S?AUO1Az8v_gik@;>r8D zjrPrE+b$Ann0HZfu!T`Eh*7c1|JlO=CNn9yoKHJe`Oh#iUgw>sfx2^5!+?y8G*}?6 z_NOEe7QdR$V!2~fQ+BLMb)bJ2w^Uta35sVg!)OcP{8=ufj?_RwBTMIb2g*%qpe%_D zlnJZ+HJu6izo0T?RfA0iOQ#GLc{szvxIlbMX20nQx@(%G7g<#wxK9KNUw~JOGJa; z`4oF7p>eKfv|6V0K4b9dW-TpVGvZRR+H`wuPN-Hau-PW=d5%f_#k@9=3S)C-4ChR7p z^M{nV#Lmohz!!j#fXi>D8QW88Iu)kh5gZj>&Vxh4tA8+&2dS1^qwZi%Jx9XWe|uJl z2C2=;l>MeuJ(>OgO4v%5&JrRFhh1XK(pci1Thr*n)~pkFYr(5|Af6T+&jVkz;K*50 za@{#gL!*hlB6YWOtJ8`gnUY^CYavftTQN{K&;h;<-kX!eG8oSn34`Ii3+i%C@?@{e zp}H}eKc@rT@(}8DTmPDqJKT})jv(5DPmrA!e0+yXkGEpE%twyVxcx*v_o;+ zj6SZ;+bN@2q7#d_=ZH8ZFzwSKNYl&3-*^SK!zr=?8iA}P5C{!_6uMu z>r%`F28JjbfdyC%C}10`-5(>`Vn6kr&rO-JV{6^D^*Nu^dOyjo&q0H7Em@svX50TM zBZC%-)o(A0<g9vVZ z{UbHk*={a@gmH<%S=hXvoobr-5CezT7;c&ouct1DHajH58i8tvh((V#~ACbJv(=lGD=vyeyU=ORe5lh28~WP4z*#s_HE3Q}BM8M~WU^k|;Ko%bPN1fzwP=H$50VDt;~T zZJjAKCpNvsAQzoIVY3-B9b}NljBRvWn{&4I*rsHm9G)|TV5@MtUAvCO*S@_e;Xpk? zW1kqKnE?(2yNJ}+AP33XYaQ-DjkTl%URHx?gIZM9bWh^&vQmaIb7&mz%1Q&t6CnXv zvM7BI7WVDcY7U<}ANN`6{PLSLYx{j46K-1IrKoBu#Y7GEL16{B+`URV18z`Bin5yu zcd$*kd?H~6t})W=&lhW}wl@B|%cZ*&3ChQw%~oBOW^LB8Wi}xm)W9N12xL4We7g%| zDAgQIJ*&?&pCx|7^dO3_Qj9hoIq{=N9AzCB5w4u$y@XgWIcTq?Hi#~K=PjzUhhXLa zieqi+3l|D27#8qI(@UDFbXGylf4{A}j5i1a`1fF9g7T@gM&TCb2DU({2Atd@YU!sY z(EiOO>@84LxMNf!ya%JxG;pD+VmqRn-8Dq1MTAU;>YI}5{bFXWZooNo>R1u454oWxAviCN5S+ge9!p*~nCs4tt5Z_aw3 zUK9hH9~#y9=G+J5jk~Kti~4sN2x6f~mBhJ4W^suQ=Nh8UZF{8LqW3?HzWf9-Bvq!K zd_B_K=j+|p*QT|xNOA-dAlBJaThMRb!B!k9o0Mmkh`k2EhOT6wazPNGPy1H++{A5 zL^^FXodxC^4ranbMx##W#M8D8u!s|vieB!Mp=7G&>zm3>D;0{}X%>P$s#-Yxt54eN zYEHHhvu1B_l<6i_s==KPhI0eEWv40heyc9>RxXWQ<0wcGd$`gBH{l`5L!iBM4-L4` zsL~Ff??Jbqrdokmiu0%py6FY|g#aZ7% z!)!tn!gohXnZXk5o;iXw&YO+}HKnba?BjwJ)QdmAXri*(wdfLrIGi zVFf75tu}tV%dFEx3vE<+~hpHUppdnPU9AUdD@*%~N+pf$wDXN9d35AqN z0X;L0SW32h`1ugPPsHd#n3gJHv68V0+cdzxPr`#7Z?0xl(=9nvufwsYXb==`ySgkxc2S3+5<85gM*j%_T5~2 zAU0^$7TGri2ljla9bLOssQpH~I^q=WkuDgg?GiogWF0O$h%{@j+8+M2s`t|C zcG1#cLSSGqtXL&^-AzC)AueaJeC7qGEEdC|2s7xejTeE1Yy?-e8;KmnVnEmE^x$;! zJERBQ(2opeX(F(S>`hIn%;+4*DG^L#ken^ zsFBQQR=0^>EanSTn;ftK5L z#X(?L)sS_-`SdQ~;@>JA&+K}U)q9JJFsUClBnPryY|6GbZAiv4c<06xx$Ydsxxq7R zc7=8~dhDlm!*i}5%yJeVjH@5!=j4>tnGS;}#pv8{fJCMjhV&~*Y4UI75aB;-tFZ^p z25n`w<(OPmxx^uT#6tPCx~40(S=MBCG;fhgpooLJIeJ7QjoiH>cuX}6`ly9 z63$^a;>GVZQA2%Hn68du-KX zSRGa3Bn>%jXfb=VEVdzQU!arL$}xq%T6m(NaPP99%VS>q4aQxoU2IAQ;!#3moM5wQ zFkUndFj5fHrGNV2I|dAt;WVYYJmyUGC=Dlr>1vxs#X4xY6AYVQfZ zH@J;W8{%UE{ZvV}i!DkDmtmf`3&vddZ7QV>O_ST==AWew6nqq{pLTC7gHUP_sM&`? zr)h#Rd_eJMw=ZGnA=3?ZF`*I3y4o|d^h@*1B=SQ-_c+!CVpL8|Q?PwwP#P0%W$&{}&bHEhk=%U><{ln2%<%(NFhdFH0)R7dsT zI(t^AJ_=oD4x>miDi|EWX&z360WA`1Zr@l<-Ld|-jSlP}PD?-cY!_4vqJACP_iVNErc=6xh!R zvrzm*aX}7R947zkP3G;{-2w|?%zUi*duj%~Z!b1qY@SqV`^VY#0zq zpK;jOvphOOkp_q$lb_~TDs07nLbQs)z)`yV9$+pg!HyHACUvt^ev0%|7|UvXMfEqC zIJc}OaJbaU7PTmMhkGqrNRbr2l=?@v$M=`1u@zlBh8L2;<47hCMywNdl;YJMnsX{M zb|mstU3y02#Z-#x6kWlkaBvCr+f@VDDEF@ld@zRqt5U06zC`|Bu(sbSTh)-@G@dW= zCG$6F?HBO5BskXjwD90#PotijVI&!nM9}7Z`hcVXCmyaPU;1NA)+#}F0kROd zZoD8;hWwr~SV2`0vQ-hXRS~jP5wcYgvQ-hXKUWc?DlZwMS21h)(;3dKLD0$Qwqg*< zxnTG%E=Om}2PDQV4WaLLGo&M(G={jWmA&p}i3F#}Z_-DY?cN{y^Ajj!Ld^XAn8vKc zPk3vMnI5kTgFiOV+J!78v!L(q!M|`%9C!&h4x9o8fh3LvW&(?W5}*p$3~U1)2A%?1 zfY*TIKo{WZA|8+iECYPNX5eeU1Hj|JuYlKpHsAzs7D)U=(~^MkKr)a9z;KHvf1 zDd0um9iR)i2=dQZ;96iFa5LZo?gZ`w9tU;;Ex-}r1keRs09olWUg#w?c)ws(Pibv`U{;wSF!6__8Rd$10tst=6iwm0G3d)4cqfq!nxB{L{1v zT7_n)=PM*xZ9;`nUT!@KBcPu&p-Z#%)B44_>{(e^aq^p*ta(&m_jJ$Fc!zdfa&o>0 zQjFUz`@7~?QL=)crmd@5$In3sh^!6=j)Q;ls_ht^PA3EWVq$IfxPI}D{s{vT2M%(& z248UDkf9e{oHXo`;Uh+ly3{@TvN2=FjlX=t6a$y26IyKZ{QjMSO4 zzWAlI^y@P+vu4l9o_oWM^K#}d@GM-EyBG_ZOAG$#rke|wEniV|%gSQ!s#{A+%Wf-Q zT~S$eyRTX|)~sE({>xw4P_uE9BI{;VNSAslODlA*k22k;Wifu{^LL&$S-X}N%j9XE zDsQH@ci7qG)w6wGuZElJ)$@wV4fQ-H>N&l1war>+@Cm+?qC!&Rslj zL2j<)Bd=QS-1&2&UbV~xIq7rf_xLQDmOOdNz=ZS)cTrVUdFjd`y_6wSQdI3;UBs{~ z!e7_DtE+SwvgMUU4BZm1JHs8xyS(%kUy*OUyOcWneBPCM`T9u-o^o$dwU>cip%<+r zCNZK?zr5OAZB$iN`uO54TJ2s%;a6AsyrjY7YE^Lw$~Spn!d33{o?;lJos&Cv zUewIdOG>NVMb*{b)wh(dcNZJJ(u!N%6(qGria|w6D@yg!qVm!&tK<_FOL*ppRM<;Q z_btY)yt~&|8oubVPIAxH-2`1-S*^RvOKU#Ktv1SacjYSg%A)de$&8kgGF`Q@ za&?uO;uEf3S?;^Sy~?OqsoGS{@S>hVRaEOfW2H{z`L8}^mY3%gl~$;_OTDj^daLPO zQEA*-;;ybLTFFX5a0WmT(>bcaqTB15KJC?AcdylXixyk$t(Q>f%8HfVNuR$xBp)eT zvgDCLN>aX_42r|wubnR6jS98uFmifAxJ$f6RaR+9=i2K&qmFA!qavz)>xnn*yz#2_ z;?IaTRpM0{jJ7qUKHVrP@97}vNtJ<=i#c(gwqIUZA;a#)xz3cu4_^xUQfN% zddfVguB5w)y=zKWdV9i#+sM1Fih0APAT84~GgUiZquR$H$8ea{47*ajggv2HM!{`; z!=Jxh!jX!L^dgEd(CYH2X{jc?&wIP!t(L;bC|?v_VCX`URaRH7(%pHbs+JiOCw8~TJZsTodD0S?50fTM(q^)E-|AyE zt0-bcHY#qbs9am|Mfxz@gjupik4{Kn6O~{y+!C1|CzV~0(baDx&%#KT-@Q@KO+2g3 z5Px(|bU!05+5NmN>KW!*w?DG^-Ot~MdhS)#gb)Bk#huhV+|#b}@JUvvtawVr>m5R*U8zes%d|M>pb zKGpwjG%Ef-9sx0R-Tx3U{#?IE4~n}vrsrR5%;)=Kdc|G=+r_|I3{o=`5W=h=FSiIGWATesQ2W$PVZt#4=y+}ZTCySCl^^>5ts&3nIf z-~A7K`@!#g_j?a*fB2C{AA9`!JAUxPAN}~BpZLj>KmC`VJ@xaQPe1eQbHDiI^S}D_ zuIAl)_Wq`&b>IF2FTD7#FTH&5&~FdF^6G1^A9>@=w~qeq_kUGk6IwC9E8RK#-14xVpO%wzb#d|4Jn-}6Xj(eJnV55&Iy!6fE7x>C zFW|H!-nrf?j-*zAbmLZ|TGzB2jB=I64dBX>R(h4MRA>@8MZT3KxU;>t_zVuJ^6iGA z3iU`nlD~ zXta3eR92|3xklJ6(j~4&JdN-g;UtX4ca1}Sn8uRN(X?`HuC5L};=iQY>sxS38Rvw# zJ%?nWc<^mrQMI1V8FLLJhbp5=`C0E)GFlEarJ`HC*H^Af*OugFEt-7oq|AAcAIOue zDFFqcJQRx>TJ1xXsW}ZmJJ1}o3XMY>(NwgUG#tN-1@jjySv*#o#Fr{jxOxbuAhpb9pK?62tatqAe$8HI;A z*M0W)UvKXHy>EX$_08Vj`=+0B-)Db6zPY*O}qIFnS_5Aagx&7B5%Fj|K+XxZM>C5F>|~XULQoJ42xox zq5I0S)RYTwi{6wf3ajBWBKHi+p_ ziDnm76qkcZd?cynR2CcM-q{ds=R><8^qX3iQ0_B)kc=S;=CbQT6xXzqvGcq|YrLQG z|4UCQR>Jw3HqoA2?ggi~ES4OkAnC=$5RJiu;$otiDOD0TqjL3XN;I#ug6wBX47Pr# zlU1_Wr)wQjdMjmEKGGUrw89iyo^Y)s6{*4E^;KTv-ZQ=BURtqF1+KF%j!^NsTkwY} ze*@BeMFjcKvh7PMN>mFKXRTWavPJDlTro2)wNsY!ets=>Zgr*?TKcVCpNHy7*S#w_ z2#%siU~uYUv!Qb;CWrR0dbSuEH>;9(q{`ZFV&_T^2!YdEJhuWCm{9UGtvT8sEF|Ke zD{<2^JeoE{T4q63jy$(f8aODW#cIre0cl^fFD|bpfW=ptDQ{tJ%9rH1o8vM|-c%7! zO4~=3{)wpeTCB*hbHQ=GWzVOr)fm!F#m<9{7$y-inx3P~VctXE9!ak#&aEn~usZd| z7|AfJhr*ew3m2n0UE3vje)@wp?>sT`wJrAi(qeB$Ns(`HWsXpcuV1fwwcY1Vhtc|| z>IZAqXj+jy&!Ua17AUYSG`zm`9H%-;Y#{a!bEV=`yv9^2%y&c)H$cjh66wl&(DxRhtEd zUS;SqdhhKODqrg-GcQ-~p7ZO&tDIzty+F9MtE-B9-tOAw_4c9EN2H8V<0!AlS1Jse zbnV8hMf0=faV{t>=g?GPTLgPS($%zAtvJOCR$1@kr7gmpEAtpkL`ts;p)+7_G2o}s zX8-&9|FZ>li2^!);#w4{a5-IJH_Ab&!om zNmFB|{B7`Sfa6oBRs`+F{GJhhXJJ=y7KQzD!!FCSO1}VC z@@5%U>8!?e11z-K2*3wOS*0FQo?1Z4To-mX@cVXLDc_@j z5#wK(q(2=Cz0y z?uEEF;|fkQ7IzqK*E?z2CAfQWhvVLfE4V^2?kL<$+)HuW{w+;&VYjlEwB!#0!o0J0S}N3%mk(bQ-EaPN?-yo7H|V2fFxiD-~ti>JJ9)O`UEfm z3Ezf$1ULxn1%3%U2|Nls1Uv|A12zCvK!1BrpG%)kqCT1Q`JGq%b=VaC$ryH_z)OO!z2Uq0lAnGi8F(51;AS1Uf?O~U+Hub29w|nB=}+{nnL*TQFIt4FJ$UuGM#yY zU@1Fv63h5HyL78t+uBMaw$|GHptTtAM<<3QiJ$^?EvQ?w>dM1ljX%l{rG5K7_q|Cb z_@kfwZ1<1t`tZGT@44rmbMCq4o_p?{*l^=^mdhA3;#F0~x&i5N@$X;w7#N!{bMFN9 zSpIXfx(%M^W;L}2qPB3PeSO5&X7l?(p?1l(PP9d0AzL71tG!{lt*yOTtSBlfufG%nUtLOcU!yS%Y9R9@da}Iy%*a4_a+sWbm{+;vQ zLjCdSKXG(%c;h?`{*2d`1^lffid!#XYo zEaqhwhOL!z7|Y|RQOLFfLB_v0jkNHpRH zj@u-R0?{qNoAtOjofvEfAr}=#Bhg)nH|udRwm7T$|GFroOqVTXtoNY)2#J&BoddB+ za#1PF)HxuTokvvb1REj)d6SUXS3W{ZU4&$lz1WkiwU>C39=mP%%2nXt5af@9lw@Bk z$j6i)sH$4$91xO{-R6<|1o^juY*wv_S)7~0d&^DP4BcGJi7PN|Tq7a5#%>mpL3=SV zC=rsI?KXFld!?J?*udWdQGGp-p;~6vf~DFMuP$V<(|gTOSG8Ua&7G=8{=l8Q#v>0p z-w~1}mZu9r|yl81SQO>gDH^ z$yUZZxu=~fF}~Nm?aF#}|Il4yNZ!Lz;w0|UTNUd=rKQ z&8P73j;QbF*x0-ADrV`dggFFxjono*?-S&pT@Yl+-YCeM?W-|^Hn-P(i+c?>`={D> zrDogrguXs9tf2PAs~5tiDdRPJ4KSTDP>yo|9Rzj%wp>(& z#5;!A)95lOtIJD>Ht0e_sjQdx(`Z~Y8lj~xx0KOH9eiB3^tx88T;*;OQV-j^&qDL9 zCxvn7Dx0R)066yv$xa(0QY{>|xmNxU^4^W^ZiYd@d)Er76~)d0)oMYocb*bb4czG* z@ZP>!dwCoq--|bnijDTn+SF2mYTb+INB)hzg50k$Q>_mJ%Ng{gH4~;?l?x29(;S!E z!$DQghRN~D3d!Qy=HLjmU#RN2?Ie{6r4F=gcU6APAf)OIu~Y9`icMQ)La`$z~l)Tp-C%dHX zF2>M3$;a%KsA{^)FL;tq+8wAX(q~kOvpVZzo49*MEyEBLhau!sA-|9H#*U> z+uSiA6~xaLNT&GNqS$eFH$`4PGV5_tCzYZ;s`XhI7w&tRY#IcbMs)}<*;#^C%Wl&P zsBG(;jtT~-iAgoVZBwa9z$qs`RFovFx7M zaU}LODT!GEnFpe=l?j-1n+I4ZCV*$7YCSa#Dpq1?SUO*%nSF}d)>u0FP|^E;R%f!i zYW+1B(8!3THq&5dz>K*Ju|L*Yc6XR8T-vuebjQ!EiJ5?Qt3nE|rLKoi(yW#i%gheQ z9A#G36Dj0A&>yc{6FcFN4`=~2OMQ}y$yBZ7P=c?^Jw|TfO0Lx1)HrlOcEe0+kbmp$ zCOYyD`HX6v4^M6ARAc4!@(g$Ch8)#83^I@03(r}KxdNRs@O8u?iNYD8i$w zC;715MShbqR(t%42T{7UKGlFVv3n|G_3{g3@zsKSRQYGjnvl#DQc-h4HEg-0K6Q&( zeZex*1@48)M&6nyq;4_`iT!xrHN=h!(V* z-}nS9&MY%wmGQ4C--0=bgrXhBTlLyzC9&DwjD>imbcGrr(_A6NIDqmtJh_${2wqQNNrK&9~c8bKVPW0r|C;nvEGDq82Teg2$p1e=@ zRDHip$E$N-TeYWNP3QJERQ>DbR|Nk6x3g+}2Wg-t_hV=PR}kc3Wg8dBrv~zRA#Wz+ zaXCwWL*r~2_2&lJcn(_kP;dVMA@_OZI#9q0=X5O)K;qQ~QlJ;6&OKUR^{36xT2>9z zV!Lm2ru*u4w36jkrG;eOb|F=C(`i*r))_4m>*A?KgJojFPP~ud-G}!@yl>+DP_-^c zA`2gi^VFl(F4Q^GsO}Ktm&jFI&H)buRMN*mqRJ_#lkV8>(X%rho0<}^7T$ybXxJ(1 zqnHh)2-1hHlk2wc<&G&g#FM78NH0}u?-U|zi0hH%NmNIe+&97`MNIlnfr+V>wq&=w z-y>_Q$Rle}Sdg{g?2)x7EXXO^vIOTr1?Mv+Y~4p&ntP4+mYcDIe^&nWvYvmPRi;9k z9zM!53yKB{UVN3yze7GIBnGf-kMogR1i0lsJu#zvut9cL3i7k{@?oV2oyH`BuH|(U zieGjo-Z1U(_b~Pl&n1WJ{k_-8ld(xIDV7B@fcaFd zA=e@mVu>1Z6NAH^+^{PN6J>BVDJ!Vf zNX>=FNx=)TVV~`$-aVK`ru+g8lt;tPiWgoffZD6#=O zdl$KGs^~ia%H9G#z=Ce_wG+oQf_9ByH4%Il1bwEh=uMGn$n4r};Ii&KpiGA?m0s8m zAzw&TGHC|w9U-omGzkmPooYm~C&dXCa1xqolIk0aGW=e`$(~YXuqSsey@T!B`gxiDr8xH1OD4Y13Qlft$lD4i7SX zAj4EChri$mXj82}KsU87q2_FU1oB+^xslJueOO6gI7<{{<7_(l3kWbnCZVcdn(Ryu z3zkQUUcC}Q;}7$-#kI>YbQ$@VkM98iq~vLS0*2et;ce^?W>AO)2zJnm6#nCYazzp#mR4={(Dy`)#O_npU-ccHBF2xhFX42*>E1=mE^SfS~^ZhTORMi`k zgoIcOuch>VDJ6h164hkguxfRoCZ~mYR7MI$(A}$W2`bJ}QBb)Dv+W}l5UU!6(}z&Kj;h$7Z|-%bdk^x>7!KIYT>KUX!{%{9 zo!vEod|{WN_uw-q;GSn5%%SZoSP(gpXk6A(IYZqnhTTiAjm+~<^imil9Osi2o@Z4p45G0wh4-()T7`D6n5bY0z z_)t}~&I7R^|40ED4x&d1FCRdpP^~{hnn4kwIujvMgS?d!jYbJ5nNp~M;sm!W3N{MD zAz~nR_H$VtST5N~Vn`8=ba&rFQh6Nh+(vpz6w>t`$Pd>dLuy2La{)F{=v1xOfoI|j zj&^9_DJ0U#Var{x2(n8OnomLzw_G?^BE!H&KOXLF#(`XUnt~+qx3~`jHjGc~p%}|7cV`wFzo3I3 zV7tO~*z5?nQl5x;)9=hK66CX6tNhWn!PxY~*Gr7CDe`h#>YIZf1>Z`X$=P7t`Vn(| z-yjw7dljCK*u1?ww&3zikiBMQH9`i;%FBRbc&9GDU^1FgHRAz>CSj|m0p+RI56L)* zUOMbVPK;dGcr}00^h#ZP`~u#05Fop6_viG*(!;eNF(`JXago%8OQt&KE4A_w_txQ% zBnI7E-w%Yu`daybJnS5BrEjH`FFgPop?A4!B>V(2ban`y=7ws^QI-lG;$DR(^#P$*5u3`$m4W zpsuh7eZPq=eWqrpLELMTMLrZQdyh}HB)+2|JU~2wP{)!N*J*9jqMJZ5JsJgj!y%#P zb(>I#_|asyQ9@xW?Y+7;wn{c#2QL<^M{BE&#^4O#WFM>^j1?t{4x-hSy!aUan&5JH4w5Unm@D=oRxLE8(E>N6 z?Q-N%XD==wOE2KxMf~gH-?#bq9RL2Ae;LwWb@uJNJf4>|{2`9f=%3`}pYrcL{9C{& zC0^dbzk~c+%D+bboxo`{xtczYar{#LP4n;T{Ckprf0|EzKl+TGPt?It>}V;geKzUo zS^G4qZBI(&2<9TKh+O{Y@P{tHZ~1_+uS*=&)6X z`*b-P7sfu)^%&OSq(xdwxeiNpc^Ue3IzFVs4BqC_N~_Pq7wGs5-HdB8B@O=5dF?K} zzD9@rI{h7b{fJ(FREH1gaI-G=UcDaD;hj4EoDL`G`0YCM=y1LcH|la;)$2@u?$z;o zb^Jb^-$OdQM~6FgI)@G))*;iM_jNi&hrichrv1%X+Ru&|*jT33!{@;a{Q@1Isoyhg z?9Y}N{Gq8De%oiXXXws{ExtfdY_>`5Hh)C)Nh19^?H8lb3amnUM%&kml1!)H#^9|?q|itwh}c>g8ZkKFBHF@)v4&qCvLpY;e7*d!p7LkrHqZg zgt2#LGxj0gMVErOn6Vx3XRHyn3p?3{vaFxH(dNPx1OI*B-WCEibLs6FoZaCV>7(H` z@*mI}G4_f8{XTDpR`?ko7X_7o6%L2LWj%hTLETLCCChMSy~Is4+t0%h@}99coJyhZ z7+gK?``~#vLhd>bN8!ACEROR_kHIx^e*I%{obJ#VTob1|Iu^(227%i_!`8x3+%^%(Z?AWAIjv)GH*z`QY+RE@w{Z+k zql*LAVSt>C+TTmi--+~-KHB_1TVy1_A??oKoP-+$E^8ifl*80wuRMpuLhp-f1U!xK z3xLeuZMaZ3BcH5T7u7=0TJ7&pGJmf@f1d&r#*M$rusd)oggS6>(ow^4oe2MVk70_D zO<6W6JaKKY!!WggO_lQ5)MhiA+BA+$U69A7&Yv=9m@%HskP6w1<^ndODWA<)U}iJs zPaQ59oRT)+r!3=Q#xA-vk68^TSYFx~&Rv_+Xq$?7J&v(iWqGXZ!?)OlZysmn1;gyn zB*y-D0c;0*&NAmP^NR|vS5JYh2nS`UJ-eRW$B<)SISwrkH{|88y!knfd`lj)H0Lu5 z^s+1~4Bvq@37261j-P5?$FFO&9tlptnl8tisq6h44fzI^zYaXjMrMw1{T=x*b6&!vpi}mFtP&JuwZ^(*uZs! zo(oLOGN~|3Jn11wK9YG}hi}jy&+MqmU>IAv2uUb#N)~Rn3;uywKa_=g3AkaeGHRoDUS{exXf{i|@^?F~MxOkJ`3pG<)ao#Fl zKw8=!SsnN=d*lOxghct5uvdl&;7iH`{ovV(3KW`_xhW6%IP$W^r8rW&3 z@NW8DTZBr*2ox@Fhbd7Sernu9^C1J)+CMMdDv z%kpe5kE@a*|->qU81|r_ka4;b83NwOd*z5KBBJ2Hh#V}IaKyw4U zKO%Vp?d!ZPv5=qT0uz;*1MQI7*47^KZfFTd0wJk|6@8*5s<-?Di%OA@zb(xEQ7eTv zu@DPHqhX(4WG-kUwt8CvK}n2YtRns_5E#kp3x~bZrm)EJiL)rhqGF^C`il|Rr-ae4 zTLSB25z!m+MI!CD(FmZklX(N7fE4fr1GkSfiJ=XFNPDP_&qX=&;+T=4%Jj}Axl-c+ zh1%Fvqh+tiRxmo9Fm?%pswgyRy0*|?)M9UeR$99iC5@tnc_ZTbK$J!&wZd(iy_gYS zkcR4ouYik}W@(+ze=GL~ZCn~Drxb}VStKOh=H-G6un6XiPXWnYFlNl{AV&fR>nDO$)Z3(SWgzB$4IO_(VVKSQgDecEWsnO{`rdh6X87V?YG9lOLd#qg`~(P{86t7 zEh_^NDdr2V2*HF%W8X5Z2x$SJxh}Kh5q2;sjP0bD^O?=_DNZfX!bK*W?K3rHW;26g z59ZcK^-6nWDNf?hwJ{<_X<3AgUx|s4O^srtr5)=m ztRNSfEa$V_glTj~DQ;4U-I-fIvMiT-upR{RrXA3zR$Lcbzg~=FG`X8NYf)DVt*gk3 z5i$J;9DkF~b2r$t-?2z&?Nzu_gk0=UGEI|blt0pog^;FEfp zU@Ojq3TSY*%9i0hgEH<{`MHu^twA~`ZU&yzVn-#wK;^jKQ$S1re<;PD40 zOaL*R{l8%t+ly+|*KBMH+BS%hD6+_!vUwGbGMgB}6)QH%nz9v5OXpOU*`gAXPhSu@ zLQUBwFgTj^FQM*dB3%7_nGoydR|7Ez-Vp(B= z`n?rQQSV)(BIV?J_#uF(@5z23B>s6Uma-|8bMIE~#`s@=DAZ}W5P$pd*Y95dWHH6e zX8H7TBzS<6;dt5w=6Z7?U&E9NGo$Du`fABS@~H3RL)QQQ-~X2wP@;3ZP9^%FH(QCS z-W(;m*z1vJ%Qwk4EBY6nF#AZ++YDbrh@D(ZgZK3-O82fg)0y z4wrw`Y#Fb_O08kmS!*o4R~lPQ{gS0sS(B@e&C%>ebK?B!W8*bXZP(IaLDu~G9EELR zr}>XjgJL_7+tqBFqZmzzG+!4A*(WQ;CcK9HhwBQB#j8Mc>& zVsB})ZG3Z~)uOOD-av>oEBZ!{e5ZVeJf~@E>L2wt=N6^ri!w|Cg*o0Dg8aUXN;Kjv z5ixre)+ntSsIcRaHg)I<#b~HLcClt}4j6Olosl-}OC=WZ27rrjY`HgpnHP=)y#XaQ z+na~}DAAzT!*3W24zbvqXOU`O0S*uh%#k9`A^1NP-eDFVg2E=!l^6;FF{EjJP7+sd5;F?+^aO$e;nNSM7Vh4KHH zz7)3C>}r@DQrL-DiBk|5y1~1_r+tRPj>^#`7HNGZ$g0TqsS?fM_oBJl2GuQ%4O);g z(+V=-B_dMmlvd^9H4r(h-X4(FZ{zu9W=B!&r)nrreToRNC9xNw@!Ie}SBq5}aI@#7A(7jyshLwYD>yb|O>C7$v25F|AlJMg%xi2)9U zg}o*EW+UqO6>2fuccBguN7PDi8}4AL+ULw_C#R|%{R7oT%nqO3Tz~%1k00JbywK!? zag$QlQFlV@RH&STR{j4`*wAjSns%R}!^fW!s8 z%m9?JLR@a4(RK2|N*i-zp$UW{O&wqXZFA*(t4Z zT!&DdoJIZjQazWVZGP-HX1BRMIEpf(hZ_aWsI&_R-t|W2HH9C(6Z& z(&88!%*{8vCCGwR&Kr(C?^O^Eqo1_)6vZZAxfXNPBFBoXv>Z2r>J_$)Xli_qVd$r= zp{U&(!hkuKdKA6MX>3mLl8M-2>B0C+LCe7 z*a(^-%Fp_cw;&7Xu3v`52XzPzXxfBTX#tg6Eb4_J_8!3DYySc~Sd;yPR7sr-vrT*f zG70=9h8M9-$;^+QB;>Sm`GjGFS+c{-?686-4X}dchsagI@)M<1s%9h6vwW9)=Uun= zXMhTG-+zwP!d!RZR~9@n-Xj{onqLB;M{$Ouft+wu@yxmzvmJ9CgLKTdpB-gQihqmr zs|J6Qc0ONmp2gB4gk9pO9+S=acKh1+e^0bn^j0J8COSircT+{~_`xDo$s!-4`{CGJ zZv`h}UeR@JPC%;t6(Wg7KA(VkdkpnLz2`LOt{gLav(k9X5so=pF0fkkkH;zx>@E%2 zhJngm6Em!q#9#!@K|o>P9gb&_scT05GHoK&GKy+()0AM1N@I^h{|Lp~P&})lOU|!W z$MaVJ)c5yrqZg2DH~dGn3kk5|p)^B_*;c{mXM5*UWSJY0oeJB7sb(35&QRn(2_+!<&hN^nHm$p8tgAYER2G?~BL5ih1-iU5( zHE|&pX4iudwG{u}%Bet9XF7%37f!*tp{)Mv%i`aKO71SD`;gLj+$IPjeswH7IGazy zK2}=$K#r8iP+~Ll4EHQ-_>zE__3OumDQw>oNpH;NgZk&b4!I}x64Qa-X#^P4NL z1St0kP+Aw}N^5_TBPqF?`@z#4KO2}=(PzM+H=^cu-xY9>R6_Uw6iXy&ZDo#t;|Vik zj6is~H)9gsx!!;&T=VC!870n%fgfD}aYJ=;Y~_g%)J)zr9z+)Q2BIJcup|@pspUNR zoHsAUzd-&Wy~kNOOIo!%w8onJ7m{Axh3G)#xk~q5{iAesKsdKiiDpCCE@rJEz2oXo zV|;*CV7{c|#ikCPH*emG6-sn4QB}xj)4nMNJQ;O^6{9g^v}#>V(%687GU0!y=9uLi zi=`@$@<(rkgmGgw$_4Oj$6p7^ZE!se|7f3Qsfh2JH`e;uBIbJ z`#g~qVogm-)Q%2r0B+MlI(Jr{7g}SS7XOxpZIE4dhV-wEV&AUN8jFd`n&R4BYFkKe za7qz|I+NAY>XEE|QRLG)?_gC+zTU4i@@$byy(bxUvzcR7^7Y!j9D!uiWoC{`lCKkc zs~DS%8ER(8HeaRMX*5l#Keo+^Z#Tv|yRxXOF zp@gb~=n{pTl>?JwP9++gh_Y6ui&0M;r53g(=W`Lu!F&s|Hd+6qNA9xN!)%v2RAvEZ zae0ZoyFF~%1s)fkuq#yFbR8R(t+2vurZ^SbOlOyDlhiC}m2A^HI+dph(Z0cg6<5T*pX;hBP-R91VLtAl@+Bpg^AHX_GJ-V9QNg#r`0S zJUKVf@<$tgNQe3tkUO9EzKB5!W5s=%29F(sZ0Orv%#N|m(b?V##eZDQ2>ZX*q_BU3 zDy;#7v&7%RFTEZK`!{P@O2Jd!6^Pb81~*8C)epk{LuS%SN@_8aD6Fmv`#(05{y|B9 zGm|K+t~7hc4&)D2GsR9AOYMe*N2>i(waI`&9fvWsNsnVWu*hq$j0jl@eGOp~Hxz8f zw_AxlW=%LLuT8ESuF#J2YXudKQ17KJ+CJdKw;QlKAlf8G)Z3S=y2n7(_ zsQ9}p!@z_(F3h$kD_Du53w}Z}pn!WDzg-jtQq&S9_d})N886{t!S%G;U|3hFcU$@8 z$dv#vs7uK`K)FOklSHoGx}@H^>~h^OudgBgU#N?1PT0XbE5a<|t;RcH2Y_x^Kqw-B zU8!-Sm=V;-Ac|RuybDm#O(^lP86`jyb%QdriTutnL}PQk9?Lq?5%x(;*uqzW7qX_r z5D>{8emOF(0TZ`Gosdni4PFG&%p*~bR5y3sc?YJHpi^*7l{T~b7bPK*qmP?nzrv1? zI9QDuNVw^453$DL(ff-hv?Gi)p?LIe+NpxqhQ0a46LyN&7KLJ=w4tdnDI{Wnu;S4T z3SvDFWMsVqE9`c@Pe_Y%Xg8`t*3mbX^eQ)cS!^GFRs62|v18H(D~*lW^ST=iLrXi_ zq%^i=$NzlBTHh?^U;*1L)jkfm`Q=cjD$znPffWtZkLXZ^)nO-u&`j`Nmm`zb;$7-+ zR^5u&TF2snXvE0}`X~$Fbd)=hqoB~KjuwohPGoc4MA-)NLzn=l9yJwacZnL(G`BAD zq%{}jU|JlN9!WbYEwlDtL&Z8A(5EjPiAklD@6`aF<8}y`(wp{Dy~CNfnRW~w-)?>$ z*pGr8yGLK0g}m0K!)e>*5ds_p!Yi+^Sc0rQf%4S>qz9!p&nX34bV4(hZ&9Vsw?A5bsDQ<;Hy{zq&h^as89R@S~KgR~5JP^cxuUM|nq#+RWF0<^L- z_7^4z^o>8s02)NJF!=Ji)RIUG&DeVDjQU{%vD{4Epxr{t?Dg1qUZ-?7(pE|P=(^aj zf%9rUHl%qq$9trOyA)={sxS~tPTM3T3@kmNwW+mt0T$&>BW&9p@@)v!HmQvO)Ys6Y zfPD3KqbagmJwMW=PEZ;TWg|Qq;StHOgm9)AZI5(mbyN(UFl8>bm)}r;es1BOD}gHJ z`uizhChrnVP}qiO$?)8+7#;ocW6SYh+ei^}v<>O#{76WSk01s+IOvO#k#@Gl*eOb% z(bk(70HnBgARFpj<3tQsoU^=0Qltf_)%hG#)>S{J$NJreP0Lk=@Y0q zbu0>wqPqWpy3tDs1nX;)VvKS7z}8Q&3Mqx|WvsoFbrHmG~ZtW9__&p3!vU zT{N0W^{zJ)@cIq5?fg}|hOzy0g#BDaLq}N_{Ru|u9vCJ!QeEvSxt$UPm$H)%|b(epDcg5CRlTT(< zHPg30YKkI>>(^vL)|ywK_vVC4L ziBpHdEH2gl8;!wY5LH^CBimVUmGlJEFCdsZvshtI*xw;N{sMBa!jlx%e~+;KnB5{p zNV3%ZR&^wJG*Oqr-VfPYjGbT~bwn6TtK^y`mh!5HIv1U^cpy&1QZR_J34)mD#4A@%^CRSL$dKg&qTwu`;lLjUN&>c%BcbX&*;44G0xgA3dO#ROuFRU5IcbBF1}B(n8_cx` z23YWXSX_m*6$@;hQ1MA?@5zCHx3B6PY*l$9m{?7Dj`1aQ)8$?e>ID3iXQ#MRN)G9o zkpoP%Lo(EVnvGd48 zyL)L^$N+t|ZLy+<*s&1nWcvd3aoT9H4+8buj4iwt6ro>jsP@|Z%MK>{16hz*e1K{+ z=NDER%%qg9T+}Cb1qf8LQia9UtdPD)fNUL{xDrtK>Wjrzlzo6^&P6k@YojG?1fLF! z>iHLHgH1qQyP6xAvH)P)4*)>@Ib)k%^Tp0Ij0$sf9mT`6Vz(lOhGZ{Ez4J-*!3LgN1 zPY9PcAY&CWLj8(e*I3eW7eCNYT5OB7Rl}a2$bjAgSxS%v_=ZaR0xEqjl^!V+;~PjD z4z0GS5r3+YN|JMpktp7mwrRA;25i9DLR=RMABCX#vLt4Mw z*$GVOA4v(D%r-0K88XtDZ!DI^<94()hi#VqyQRpZ00$~&DN=_8NdzuV z1rn*GeW}38RNyygRzGHi3Jd|*#5d_ZbEPMjf;~u)YJjQt$WnxMWqMDc6xm6m*;6D% zrihqprN~4Pn590X_moPJPsQ79>Il8(ZYe@G551>cioAegam7w783u5D6AVWi)Qc5X zioibgJXu=%X{Pj!rE17;vEM2|DNF8#T|Mz3C_&gPi8~Qe*qGuYsOJb2TypouJai6I zUt0S`W{BNkDe`yAta%M)&@w3qCGI9C@?;~A6d~n0+DTQdNWn2#s0b7n{~Ar5Raak0 zb#jsPW^oT$5gU+?W=gP_HSymB#JJ1o!x&UrO7JFz%JoG(cni{7T_joJ8S#u417xI; zlb9t?y~!i%TLVQHe5}+Bh?3b+DRxmB0_!mdmiPk*>OJ>L%iSoa_uRL1hu(9)6amb5 zdsvG6O9UQ~BEJ)X3iV#Sr%H-^3;v+@Xi{XWh+ZVszK@DlpO3f1ETeT^uwXDu8+v0J zAlJT9aYxQF zvIrU!xoe|Gb1ex zYI?EsPEk){1jY}KY!Nr0xEx`75i5ea6?t66{tZiAa3?wNs+b$d1W&h@74%Dqe^MQOJ z%-QZEknLhK^7Nj9r8e2tQfE_)Es34v?L$?_?|^EJ+$Jawsr`Y#Yf#cjt3o6;u-cy| zMIh&bV{9>y)NIR(p9K1~L2y&KPm_~C79;_bYfe9h)TI~5vGsRQsq!8CQOKC&!}K%~ zu&Ar)*g>%F!~l6cWu-}pz0`{12!i^-1WqaC*sVnbx8fz^P>5EEAcGGQwq|vy10a|RL<>7{@f@lam!GhV|QmJ+(`X>hS5<;A_DxE0sqC_U* ztZFvB4~ zNbJFEoP$Moe+!Ty)-zfGvC`Fg;k*#cH#Pet0xUO0fIqjQ;!{vdBZ7nwGR=Q^2=WdV zMGxjVO!OqJ^h&w-W+>QwyBS99_Epz6Z!LhaW?6Pbx8tFL}ggMFrjUb7O_U=-Q$ zg_uYPc;XKuP)~f~3u)RF+OXD|Ppo(8c+v_rN04nmTD48ASG)(iNne-089H|$3gZXlLzLvx zzBLRW3Qz~8ekn!LK)+{Z7>x|Tc>K5E<>>8&+Q=fNiD?OjB*lJ%=pxn~e-h8aSk@|9 zu!AvG*%@CVQofFBse)tVBzMH1gDhrCvD=UY_G{)>G7i!(zm9?4d$GL$PjPASNd!a0Il!L1|~ z1Ki=*hk>R?}r>7 z45xehT)Bxk9-%Fv(c*7f908$>DZ^_b9l%h$%naFoVChmtzsgV_!0&1GUTl6XR`pJL zI5C;nAj2JggBGtAH54vCNIqr|zOjamEq>rri0xi5fdS-r1d+)iLsoExFl5&VaUctU{TQxo3#8! zyffEufN8irXad`F8}gH?hDa9Me-F0)&`>;6NzGN zqGzx3W{Kf$d7V)8jMqucV|fl>Rl!{4r5_uBBSUP_L%!@FzvB2Z$YurPBSjfNRagJOB`#ejSq!>pg=P4p@!Nsimo= zF$l_9Jse^E*dSTD21cHzWfp9-LzheXzJ(^RFj2=G2R{SG?NAYAqpeABhC%u*{nEFj z(uaxkUYn1vU!E6w^T19!3JGwCdJ=Jj5PLXQk_~~wPsAThLnWkAPU)}C(2J0x@ezF+ zez)_vJ`^|IcP14$Zu=IdV-Km)TVEyC{U;9LAm|@61MxCDAzgdQe@cS}yjT4KiUJ~& zhMnHEVLsM|3g|Q!;kW`i>Y)Z<&W~eZ!ukpVpz-4OLjX%QePMy)z&B`mJT+Z>M$;{b zN7J%&?Mc~xQbXas#vw(LO*91oX}5kDhAv@h5-`AmOaOTL`hKwjw{bvms|m$+%)3_z z0e?&)Ko(FO1r*=N{%^GP{|``n7w;)wWnY&dj}sh%df%t@<-YF%v-PMz34ob; z1~6|R9=lcm^R4XvR$JGPj7@9^wU{u_H<2~%N}=ovlL6n=10^+irB|ay%+V2i7UTqs zg5jQr7)YHbupxxeI!Qh$`hjg<3}v3LD|Wq={}__NirAet(mMIaTsG8dS#p24{1Yt0 zPB^Arr%&s!s3q62td1@@M_04?>*yTu`T<5Wq ztJ#eFh|8elFdMT9?=yApCl;fLnoB$>yjl1`@Iw-4#WaS`6d=w60VMfI(ig$QLrnXQ*QMYAdtkkQOu(i6PHoU^3f!-A2{F9%;pOy)mEH!wdPv_PCI ztu4m-9gmkFJ7I6Bvx)93dSWJhq$!W;tX{|cXh zTu^B2F#OYB!6`N=_5>Qmc^@Emsa1>wx2Qjcv6@3|tE*+Oh}7?ay#ncXQaa1xVu&u6 z;f|~g;|0V$umVrS`WZyy-o)sl+AeK4GNoZ0N14g86zm3!liPC@oXt;>iVvB~gX)cy38Z+Tb(j;=n(@;b2+`$+U5^_u)0&V%dP@xoMb5u*S3F`}XNhd|(OU)&^= z@#fG0o_vDGoG~Du@)pI`5YoLHNlMt?3(Fb&6V~E!07Z#ibQ@L7PAKe3rM62QtuJ$0 z;mFG{V|TtxDckvC@=(#wNAoS&ivQGNxLgYhcb4eE0K@$PWdv+=KmZenm}wt}Gqu}7 z^XPcx05aOz6o&2@6LY8-<^$-Y7f<3a1bjh+-UPOrOrfY4!E;7Jxq1B<&aqMnUjaV6 zgQ)(5VuSo~(M_m0q%S^&iD75WiO1GV0uAvdkY|!ROMD7mTEsCyVC6PpG~@G-YlT@( zyI2eZQT5Xvldn*?noN5~v0+aZ?Mh^aqH|7J5^&kt!tX&U=+LzQ%^PmzrPOpr|IZkd zJIpyPH2UbA5}W=!og=aBSM+HI;LO8G^9EK1QDZRQ^&vr>b)auz0#~0xNg{AXb->co zPAdWU;-%zwHlqU?BE{cQ<>iX-yr1j!^xF@apz}Mrg;nYfMSAs^Nj|lPA_aS}nCV8x z!W{JDk5Hn(^BEl7a9@btU{TgC(x?9#(H5w}F+tuMD{!+#sok%>-eSWsIZNVYdKqB8 z5YR-3B#C^#JVc8qAeSO1P?kKDBBVp5<#jJPw~UkP;nS&(BE1$|lJ-bXyhVZ7t=2kg zvu!FgIgo0K(Q{d@F0ep!qzQ3a(tnLy^=WX&B;8n3^;C=Y89W+!dp_Kw^DkD1R_D)w zADPHp^^kcKkeqPJ2#F&TLy{@8>aC(Yl$WSogX~5|4rIBc-U_I4r%h4EC$mm!w&AcA zoXnE%IcFD*U29eR%?q-di$IG1z}8_MW;49#n{6~NC-6T|6bW8uOXLuYUc)XvwGLt` zohjh;%^4zw0NV$Le6eSh*)f@Q@}9j!Ktb=MptNeg99e7|qm9MX#-t9C=UE-`vl;NQ zx^+S`acpAjf*yLkrJ$nIO?3+mCzzdzgIjP!pfP0|*e-bu)=sd7RtQ3ZPj20sili-g zTl_YY2hzSn>^AtV zY$upwSG(Eld=%c63|AQL*Z%@Vx8oV)Ggp&WCV|><-su;J2L@(hni=jTc+saXKqiZp zVdi@R`3(0QB&?;T#E#<{DpRwOfc*iv7!w7C(D-^RX#kttIN?5b-!9S#?N?$;vgO#! z0kZUFQ!sjm9e+;zWz9SKS8${s{Tn56Pu1JUnlk{$b~G3mV(^!-tffBI+Y9R8pW3MC zhbZNH*}RzZSn_bxm;67f9R!8r%{_RS=EDjRbA*N9?F#jc;okDR#R5k*;wn;PI-cg( zSJb89(1WqT-&FZ+eb9R|RI%_bz&WFv6BkIUZn1*28-j4q9WLkYgp&NaSlEsuhcm3N zd-$U}LHcZ8ng-`6?Tms+bNS&BHjvY4wAkyf@JvbuNM2lS&LBdX<8z^TMH}BK0uFX&5%`lLE?H^{O40V6AW*Qh zVN2a*v#MFu1GDQR!>B#7JJ{0HA=Lvt6oaC5HH4`|db4;!$I?jt=Xw*iN(rm>PU31> z4Xz&pMEpsP1w4As$c0YS7n|WpWXbe42z6n(IIA9?^a?Ly4)*92)fl@z+Z;o zqcJ?w6NLDWaFg}$|76er_pqcp=rvdeq4?ETH-JLn$)K>OS0j*kc#R7W-i^fx%jKUa zjw*qt!I(@egldphkaIe9n*m)u&L8ciTFJ4)--<&mCt*7V6@By{D)lo_m^t1RZy3)` z-2$&tRA#n8x^2{krF5o;KLK$rxw{g+19zF{f&%6lRoGYf*7soYn)p6uwM9R1TASG7 zXhs-F#@q`$i?u^|kj@g&Bza<@NI!8(8`9!bbwDaeP?83Eb0HDvpO+&T1Pj>>qA!66(;5jtsI11ma(dyrjv z6T8*B{){a{lN33K2%45+_k3wGvROo4e-5d9h^z3C+pxP@YLDKT6)b?DAw3ZjIfCBv z^5=NZQ!mOdwW^b(Rr%5?#p*w{(4D&jbzV6J099w$L$>!qxm&ew0a#joj`pq+yXM?A zr%^$*(;2dD6lv^wdrka#Obd0A9=EIK=y8{tE&I1Zv};O?T5ZSTlNh?1Y`cl9)pjQy zj@5(l7QH4b7@g-#*rInr$F?*ZY;Mf}R1N+X@4&NQ%$HxF$F*-l*uqXG{sH1JUHW=< z^;VEe?7@eC*)fmpN22YpycQK(ietgU+2lQtpQB!qf2&oUEUg-h^AlG8&V^(wxpa(N z54+rZveQbj#kQ^foeO~c#>%d90gb0CcJ-5R?3+*P)CfT3;ktQ9azx8;7gNMJ+ zE=8UMEv)f?4EY>*+d#~Q2uGUf#fVqfugz)NDz6qW7gJN^TY@b*rI`QkZzbPHDsYWJlVn4&o=jg5w(W#}i*gloA!dfLB<%o@hn6G^rL&=$0-= z>po0esrDq|Ojc0$4SBT{+M|w)1i&wJMjZ|j$cj2F6xc)RHXLQV4M5y(~_9C^-+x`@?tVQ;37Xxmt05c60v3P#iV z$Vgf{DOVo++RSZb;zP{v5#VoNTL!%NnJWV?)K3Q=hJGs1F~`~|)n+w2(eyPspGyu% z=K%wM2X6@Z{|)Opb|0St@B9|HXqmQ-gu@54ekIeX?_P}p_Jxpu<_h^OPsTn3Iy-&3 zi$rd1*cuFk!H?j##nFAlWP7w5Al)9=v$-!bH!ZAY68a+a0uAb;kXx!~1LJR0A5xf3 zidoX%-L2Qt@+qPwPE3UF5_y<{sCTLnq2%u1Z<}!?lnt-1n6Fd~f7T3_Qc}#} z0W+l)XOzCC3^4@x-Oy~H3Ch4V${c&FRJd3m``s8PrQq65bqIWoX^)UWy>;+n%BL^u zp_P!`;Ov*;6DchoIufnDjUh}5QM6ao;RF^Rf(%=?VkTfkt04pkt*E)e)tE?ymNfZp zqOk8hg%~qECYPG#VfaG{`KzF$lTJcpW6MQVq~XNsBEX0x1xH=`;=~~|tA;fVQH zuO?hrg&l!*ZBGL+GLG7J2CZ1$`vDoWf++g|X}rE9700knLq}uIOKU2 zkRtAEAcNLAf)dAb2+ouaYaew>Cj3tev%z5)!!M?zb!;>L9aaFGuT{r}@G=pTK-RHg z#QA2&GguVD{+*bO#|7u3`(kKDkRsZwm&Zj*?J1e(M<@aB{glizh_{LKryGE%MD7~e zA@kFi*(;P7qc|v>euJ*^o6#(|rkUYCMCU1~W#@KEApt?Czqexhzv;K|3WsIWn7EEY z(CHWx*HDP&Gjq*Dh59i=bs26-*Ily_0V0H(t|3Uu+>0ltvN){}bKLkGfQiCtr!NQYvY z%zBPL0aZ#=7g0byH%~n$u zY`k&6qD>tm7TOUgQnnq@DKUEh{}sxuFbiIfMa3MHpjky~7}Z=-0v(0gOYu+NiN#1A zg^KQbm)h=82kBSiG#KT08_Kriu%?j@F;=T91h{jOtgdgK^1F9n5!wn*4h&HlR+hhu zABnC$eO_0)E5kqWljBov%Dr~25zJ$3RAZeM#dF`)-uJl}NfzTSAr!d^>5tkh2 z)kM}9>@Aqqy)&A0qy5#QWlH%moZH0qE&z{K{%R`(mDpWYx#k4TiiJXh5=d%Lpg?&v z{wGw*x=CgZG@gdz)2i+KDtB^63HZ(p)V<-Q-Fl$zEpHUh=7_f*4_IZcvnGa8ETtlr z5^;tNSGb^U$Q=3Mq*8*(!^Eyt#)g@ago*=OS#!5~I8UhKhUY`aVV-jeMVO!T=k=mIlCIOr3iJDjtS}? zorXhrbY>3h6iCxMzS3LMV5xXXIF?_`ed{sGrZYN3z=`Ht89Ab7Ld?B?s4#K}F=!Xo zXgH*kRYZ!=UW9>2XJzL;kPXc!t{$+k0uRy(+?AcISd`OV4Nu`4(ER;i%#NrB)7nF zg$ejwST9D^fMpnppijiBLYMtORy$=ahrXGz726taV8Lc5AN51o-~Uix;TOLrEM$A& zP=dRKS3%Ba-6}s>EQA(Wi$uVz43b(>U|z!5d8* z%I^>&DIq1>hy%5;>vH(F!no23Hp`ciLM7^W_cK5cb!?;u1QkaNM#TYizM_wr_U##x zHZQXJK|p~X_6T3rEY>0yLk0XQ)QLNUu=`Qz^5Da0osAY8)g50{qL|3C*g+ETXY@x{4~ zSfeSX4s(mL#rnq%Ia34op8D1rET=K zt6-`+lw7{`4cSU#hh4EX61~PLs`s_Zj$F7Q=-m*mc#7bF2}~k0oW-Phl>ihpdljU;JkKJAR_(=)>kkmF^|qRM`Ju)H~yQj zjUhEi}_A`llr{{tWdE9*nf9p;jIcRJ39x3SpBB z>P>8h()3n4Y4jVR{!9`pF1Bl}Qj3N9Rse5sL2;6YIF5PId*L#3wWk`9KRf? zx~Gq$$Drxs>5)F&68NoE8^C`CMf6r78}#yE@YmPCUk&$f>V%n(cx&I<<}(VWFZd7m zi-X^iAi^A@;0?RWbr?d39B@@=ul9Qu;y8;%^Q72Eu-AVCi8!(yC0p0DBa4 zfjj`nG{18ivLjG$gC+22a@p=xFMJ9wY|GiYY0i~<` z(_8VjY~Syf z*eByX=q|-cFKLzG5!tMbfgi;n9B8&y=Z{As$Fo+BBfRX!LMUJrSq~8UGK%~FtAZm|I zuZFoLwV#8#X|tp91Ed@75-jPUFybdlbo%cwB``e*vlh)pF7>dqE8=tzIfIZk#?)23 zO`DB!ocvMN08;ulR`DOHnxm9sqoY85S#={0r^1hESEWKqS_jd!xm$uZ#NOFgukd|M z)_Nam4GKDrPCw8}lFSxgLohmK2g1Tdp0H4oa$yk;(!I8?vwVC5%=IgD8SaVj&XZ%R z7v~(eYL^=BcSMJ2f1+l!I37YCBI?9A!~HF!Am+LYF?!D;DYzYS1cm81>{?`jsYY`f z?q$8@#gYeCQ{e9e4t7j{?Z9>#f%CQQRNzZ;n9Qf2JSF#pvJ0zalW%u0c7qkyc_0>- zt<9z5DdVZqaxVM7fQ}nni_+?$X9T~ApuMefFZ>%DxQN1;ue&oi^Xu=BpBMRbEz$)1w`dwsA8aKYl{WGj9eP$gIojR zz`t-Cf{YH55<5Tgpvk9lQAeD#kC-D9$i*Yi^i3kNYlWK--Qfy~9e|u-SrhWSpnG#4 z#vG&nh0^fe$g?Q#T>9*Ri+&3>3p*y1Y2A<{9d;xq7Le*K&u|}vj7m@<_#T2-fkVFi zxZk5+_zlW}+z?XC#NQ)=eE9Rj*o>|wWYT9a!V}t+)xKnNVgG?J7PoM8%+KEd&2+zu z&~k*#`HQWkkO+FWWC--#2L&gab~{*@ub~*`0iq1L&}tI@_4O!Uvyswh`KL0HxbIOQ z5(>tgAo690S{i8)PdJl#R`g{CdEuXs9Uyb)$4+Z5eh8{sQ|FiXQEl6zDSlT3$get2 zcz3#2&_J-p{wg!vZ7Qt~I-%YRB*ycw=7Hqla@^3Q->3j>t$Srd*G=+GJUK=LX1E@dyAdlI z?xPgfY84=SaWXs(;SpwZ2Cmgw17>K2kb~dT;`fyJJt=-qh~MMl_n7$Yp;i5o*G;Lb z&8if*-r5O;-&5Fa)4q0I5LDs81&vq+%5Y(cIHp1-4FCJu(6E2gfFxZPm$5-FM{6zO3nIJ}L5354;2Na= z?$dDh^Li+wJN~GyLe#Zz8ut>g3PGh=Q*5uTUKAtQ!CyXYzHW z1t6L6AoiI=pefCJ`~!-JMTBZU`Zw{A*-X3X(1T{6!!>&<3xfu3$;VChVjaf0x24!n zY*L38nB}BeiNHXczksRg=Y~77gqE70O10h8$anFx_$A<{5WV<;4wi1|?cjZ9!+kSF z^!aRlWGV;qoAiml-GT0Y*CzlUS2)(OaIx6jL8+ohMaMvAw?fl|H{3j44mo}exV(j5 z0#lZ$a=c4SLf2);BnH)RH!dc&A-18D3mmyffQSXj^+vdTfvvj|f8~{cI_brHUvH4s zsUbWUx%iKIBTb)x?-=a&`QlW9({D4s^*Q-)~AgwE~^E9?iX=3wa z)ds?QsC(y&R&|Bk6_jA&a>2y4MVPpLhlz~7eg$1Ux#}KC17Pr%K>gP-dndA|JFBJ0 zK1A~tXl_XLjzim6up2PO$XSV;1-A|(AaL`OBt6w+xLq=E4nd`~sP?cFS%?(UgCoLqVecL02N&vs-Z`>97fA%>oJ5GOdfFoTrd|eTN+q``WW%Q| zU_JZ!4r&83UC=Cw$-yrNWeRiO0!o9b;T+jy6qq=alMhQ}xQQ|d4`fry#1d6XI~m-4 zfNLmHD*!~*Ne;pj)^t-uFI)t4b3%@}T@e275bpqq>-^2g$+Dmo$DI-ae!?iMi-!B( z3r&p9K(jb;n0wN;*c&K#&>NPP11lDRIGl!(BCk?wv}&0GS)lGgx`V*A6}vf6Z7^1Z zEkRaeZ}m8Dm#q796oo5(*t+;J9I+1IdpGxjgsg&u(zFrMn>Gx^JiRAl9=d{?Tb{yI z!cA%YvRom(NjRE+9(*(X$RgE3Ic$M9BOt@2ZrkQz1_XI1m8>l?TBsq`BF~bN(bK>pr0I0W#qDISg zEc`7UA(z6}u^>V%!SoWK&O)^({$jX?EkL+E@oVw^XOQt(0V;MTHJKMI0wa9dweA_5qpqo-%IsuJbETd{ZQX7 z!JRoE`Aum=0-7{0I$YM9;iXD{jpA=!6qZB0)*L%c-Q4v3-IQDY7v20qHR=62fc}GB z-3LkLtgc>7UEP3qF|H{%!6C-|k&KL2Lw)gPWZ7#pn*MPNQjG4dCe9 zXYUkM%C}>fvxpRmuQF0y`6C4JTf9#J6@$H zTS5Npl-XPG2N|vij}IVhyov;>LaZ)=s?2Yu81A1XtHh36@$HX4iH!JOPo9KGnEq(5*d@nilpTloPGceTT^NU2& z1JN|Cl0?rw!+$_p{%3^zW7ciN4n+SI!npSpYbPz5;n?)I5UqcXZ<%zJ&Sds(X?-}) zsefeEa{1{7aFcw#2M?3Kh|6gENe_qL5$kc{A)x15$W<$-g05g5&Q}gDVjJOBfCRc9 z2%acz{$y`G{CQC`u@Zvr4mjGQe{?OSi6n#4J-tonTj++=tAJkYF(>d)Z-Tk3^&5^m&9(_YWdb$0`aO9@ zkz`ef@2PEpm#3kcvnxp5|BY%OGcO=Xdk@_ljWbfvJ&?Ot^|R)lHebfUSc^6iepd>X z>q5A%3Ae7)`H`tgY!Cqd7iQuEQ8R#nF?RCb--6F(fV!02y`rqSqYb3=8mK7+ zeF@3g(1pdP8Gw}b@ckUwXfjZbifAiOH%E$Z5$rAYZ_@^a%%Ar)4?1xb-qaBx|N9Gu zP@*GPcR_*|`!{JTDe3Cq|kG=j1q8LIA zpa171UW6rMOHsiCPR$c$JD>{WrEq!)V)w47ubqLT=Wr$!msr-*awtxn$x}C}Q^e7; zMB=kQhGfI4-3kLGDLcddPbx=AtDwq< zV-`Ojk~8EAy0dP(;y+sTxy&}^HbV-&u&8dbmw)q?VXTEbXNhK;pbAApYFKc?@=>gk z0$yw#Pgxh-pv2VN(+WF{x~LV&Y^4z%Fv(VS&~EB;)|}gdMm)i~DZTYV%t<=%tu8@} z@uyLBuLpnPX%Z;r{*b)=RBCgIaX@IcT^ffz3l5seUPA*4gEkP2qIZ-i zQLR*oE-AyV=;wa|&GiYEbAd{fKL~*z2Rtab}(9m|9;9W~-Go=@ z?SoSAgJ9JCFT91>9k@oJxFYD^vGj78wc&#+a_+W3e!iL!vTgG3(2l_MU1p8BjdJcL z+26P%BMATFV6?a*feU(DqeUqBffShor~#T3nT0?RkzqB(u)oxyH@LaVe^5)u{p>+j zX7Bz3O%&V;iIXv-lbRsx)%A~^vh97t{X8HIm-htya4npMI+S&=LeoDoq2}}z%0@>dwMaGFbZ=wq!KhCJ~v)XE4LiR)U z!97tHO7%)~2Iw^0H~bjgg`I0=XRzQB&B1M$ zbV}@oS$rj_V}(d=HHq zr}IOkPFR7$VYXxu4I>@anud4Z{&1|gg6(8G&=IpYycWesCkJOa+#!!te29fLpu*lP zhT95g!{x0YetXcr1^0}fh-afZgiX?1dJmklLZl(QmHbB_?GvdkybMQ_L6LhGX7tgr zqJM%#s)?_^l?LV$nAC|j_p1|=1C!0G6GWH7>AP=KitS{VxBK=d^y2bHARGeIV^4t% zG8}F;p~hg5D+GMVnv>&n-Th$XMRtf6b|3EBG6xG7!1t4yXh`s77P^QDRLz%-#ds`1 zLI=Dxa0Ph~SGk&FGl|~^BW7ZpSvuJkl?IALS;PJDd=%~>SHz=qTx&bO93`;s(7mB2 zVQ+>%;snHy+*_QZ__pzJzoRaKA2RSm27Va3*OQXpzULb?6?7euIQNe=c&`j~nFSTF zh?l(mgOHsY@T3K}gb+ZE;O*e=ngZUAJ~>|hEx-}H-5F%AFrXBA zW8eN_)){2SaUpzcp_K?}ItBxPyZ;U$kl=y)>#F;}51LeGbowxqOI%^N7tff@<7hR$LZ@zZTIl(6+D);k9R z=Jjg)*faX9x5k3h0Y4n?Dp5_28zUJ*}xX?=w{uGERApEmWOpxRa zOqrkLC_Bp{+h-5N_wV3-EQ?Sot1af$9b-xBM_PO_6&TNM@X|>jcKqJGDPSc zXLyB9p{voZy38oMh_M&r+klO6hjybGu&Fp*ZqHCeqWC0WXGrfz$E_(ec1=z6JwUV} z8bCv^KOzzz2&8|h?-L@J`d*+1mRp>kwBz>k*%?l-Xpa(=JHqstKo-pCq}U$u-9Q;y zV|@GXJv25p{u9U^{p(wy)Ep;Q?8<+wMuiqB$DSeO1Tz9kO=C6Q0mc_NoJl!W2k;(d zS!R1-sc9hoZgk?3j*M(-EC;WlY>LaFI1j~PHZ%q(zJubS9}g!1Gg>LOlVW?cmqRt2 zT7W&09+FN#nqMkh1IhQh{Ra+Kglw&64-mc!o*E-DK#Cqu>o-VZfDmWz9i-F%mGlje z9tTy^K*Jhu)p`dAT!#h-O26JF{+Htu%;+IZbfRGzAe;rkcN#H3K-@6185y6L9jv`C zhNsFLp1$!G;{%?x&>SC(1r1B@Fqz}i*l&Eo$@U1pJ%nFSLO27cpPfO25aJZqL2>OA zw-a!Q5u)L{5d#@EAu|WaiO9kK)A+2Voe7%fE&cf66oh=rVdfG`x!%;u+HDu%Tu zhks)RJUn3rCh?EWKpx*K0-1c584=*EW}3J1+FEwen|4F7||lg%)eE(`aV z;RXs1GsCSEcADXx6h8S6LI7*0aHkpWpzx<=m{Yjj40lp^s~PU0aDy2phb8`o8K#3$ z{6#ZN0vmtE4ChdIg&FoxIAVsyvF$}>IFI5VG{gB6E;GXc3ePsfboiPpX1IjH(fpmg34D#t?;2~y*v*)1#JJ6vuU}2oBxr^f$G*BkImq}8 zc95v7jWV*CIQro_WX8N{#!Ny?hZ*x1GX^WN>jN|9mu5^pVz!zwHD*izF&oU7N6Z-L z&|Ry|m^&yY**(+eBoANZB-^BmltfPA&y$07R{poYB^4@XtCpbAYWOQH$)uOMy@~F% zg4-%iMTm=bVEuE*b%PV{;ASj*30SaqxD!I5f#d`k2PGu)>#6qfz(`^xR_TAiSw;B2 z;5yiLT$cqmEc0i#(EMCY;Ef>ghEO6jKLerpNdap69{?TE4^Vt@6kpDOh;L{)xBw#r zAH}+~kg);KO~%4z)ea?aMeiB$_7(3K?OX}NupRee1|2gY3d|TjGo%#&l zJAI$u!-x0i`+HdYoXHRHwIrm}$M_4HG1f?#@lG!O0A#2Pn91n`i|r;NyJI$^xFH!vhdB~ zRz+%qV#92`&*#7c#XmMf^p(wgYzKQ_bb&qqS8ec%Uh30J;~vXfm^ft{^iHGC5|Gxp z3~B+0fccbtsNo)Yn=qsdgy+GfD4M{P2pBH-Q@LOG8!AnHCcnec+*hv7f`l;%n&p#>DWv`*6wGh z7>elcGgM6GH=#aQ4yN=~OPkw%n(^QZ#K3@(p8#Pqfv|p-iXpw03c54l|Fm}|@KqJp zJhJc-NFZT-NK_Psu-FCy^*wme7fCci5VS4{Sxht}F}aV$A_NkY@JN5w@>5&2 zTC3JpTm4%Xv}zM}+=v^Zb)l{|eW-B*+<5=*nR{On0<`{?{`&dOs=FXkv%$YMY zXJ*cvLAnnOHs2+@y`}mk&K6Ez=)DTrK=ZR%akBZg_BQ|69kB0a#q)PrSqiZ#kG5N( z`!07lR^1|LzG_`7^%?2uo1{c7h*QT-`}(NRAYM2hJ{$c(siHt#+%I z`nb8}3zG4MUm{f8ei{QOL0pf0m=^j0saEOib{Uh*(euO~sc--EAaKl=kKa?f%LTb>wUCWJohXU)&5?JE=QyL}l^_hqB0>TdcnYDH4h zm(hX2!PxYhpu@yqY%;JVDPG>jm@e6I?6Y5GZ~0`R@k8^VO=G{1^kgJG!F&_nV?_Au zSMrGlHPA9xeCDrNWy4@`oK&x*!u_Mdrk(GvlK~AK-n(PPg3*s}K(m}HBjfpI9%8%F z42aScl!|{;hBdRE*Zr}V5-iHNL~218G@N$nJkn*BnBoS zf11CUE4O;rjTak^=(y#zUhMEjt^gjY`A%-k&}VMUNwgUqE;KMNsILK*Z&+zy3C0Nt zot|~$L{sOC*A{}vw0xsa#%LzEbsod7<8drPd?k!nH3u9JL>+kRD7%-83nRN(!jsL`sO)a`Y#&+Y;aJL)iwq*$ zi9h0O+&kR|tEKHtZp#hsK6RNP2s`$+RzoAPv{u7>9M)hABkAL5mauR= z#mO1*-mgShSch8+3-9E$e}h)Tsqf?6EiCxnQ@zw0P9!~~1=XEw-=TZ(tror|;64&c zAS{rArPq*v-_?f@v=4>`m`@PU#!QO`KO?YKW!S<8vbd%Dd*3Yn@C&QMg&f5q98^-B z7%!8fk(OK_nxaSr#&I~D1_n>_lFi+)DOW!pz%~t(WYFizNlbnaRjepMJmienQ=6cK zWm~bZX~uD!D^?W{*ke>M#F)II(R?V7Xg;4H6ieD|`LO@>sE|+(526|4lO0`;rSivl zC@NoOFfD{>n(^#Uv`xCTyoA$UJ_oOZO9NLm9sdyi_zWYkBoxsS5)~kQUW%r0gf^gX zIpPdptTLoW3WU0zYI`KA^XiMn4P->lw zn{7YTctrunj|MNj=NGWj^tfM)^EVcirX@rJwXKeK{rQQsyP;ClUp>Ttj>s9W=11QjI<+Gy?gN0sDfuhPSQ&H z;D*cTo4_-On+*l&^xDJV$@Mxx-?#J+qU3WX=%$AaPt%M)t`u}nIt<-mM?qJ_rh^3< z;cqEyVzemV3^q${>c)66&Lc3^$jW#j%{k4SV}&tK?v56^2-GL$ByITxsGsC7Wg{)A z12^`qd)@WPN^bjpUox1pr5cmWO$bgqrMi++MLv&Mh4f3UVigh@R8!zNJ=^L_ z0a8ikSkv*9BxBeA5%)TH^5kBW;65~ed)KMNzPYkrHX=||8f z$13*ClCbtbtc_f+w5v_ykl^EpwJ6Mv4MlU&k`>|dTSfPCe?SN4Tuq*pGC~Q_*#;&?(~i=d+^HVPLKQ(^}jE^>PpOCk+Jw|Sh{MR0HP^p9^UPNdzm zkv%DdcDH{JE3<#hlX6lovW9W_PSN3O+r~jX2l9&_0cuSfw_SXLIZ+91)!kG^W!t!D zu|AwB98?Dfd8`dOYi<;b-T5Q1u*TT2BBQ&#+Fc?wl}$)t5&dN{4fPsfY`1ih7Nx+)!x(yE_)WA{ItcAEXU z(f%B`aywU)@q$nvHj25U5~Y|Q{{|1CWcQvhmN8t{{8W5f^ZR%23s)a&UwBtGA!T3K zR(F_gt2>-6iVU}J4~JWqIzrdy2A@GS!B)E2MSVned)IwN=X}Y>z*lD6K@tJWq+%GkH}TW31&>~W|(EDxEwk5=mmmhKeeaQhfl5$ z0K+Twe!r~cJn2V7!(+)qG6BnKTAHc?V~}6$JFQ0W&6>bn&|5kR<+~mhy$n&9jEZJj zVQWvqYT>PBm$WQSE}(;HIN`GxG^KWp+jF#upk-3^Xfh;1ksh;WlndVk#B^)mL^D8{ zj#1oo*Kv256eTo5_A*|w52P-6+FU>n8ge3Snb+g8`V!J+z$@dZH-E;W@J}fyP*UCb z!st8Yz&?5cnu%I-`O*@*`)WYb7Qdc9jAcTwReNA*6`j*BxhF83mLnm9Np~Fa;W+uw zB(~M;F*9=hkb53vjRp$}r>_<82{x2bV;ae-;}7t_Aka7_kaUmd5oEXofu3hc#c{*n zbLP6ult;Kk-@!Ao0=XtOiKDq1uXjcm&>mWbyf z)vV?rTZQpx$`VbPX$CP`q4NLHnSOsu0{N(>(giFPB35liM`>%`Pn|gkonQI zoCtVW3My9z2}{`4;y8VzqmMCf`Ww;jBYNmcDex0gfqLClt9n()LggBc8|W@8zcn*T zRH??+5J=lh;RdK#q-!5>%*Gi^7h^#jk9bL-MW!x)-XmU*#^~%&qT5X*c(V1SER~bw~wF&Tsg>vUeVbfzW197ZKmyxj0 zQrX#MUd{fJ{w&L}t38BZ-DfFg%Rnp{AK5~6JsgwWX+l5RkfnviZP}6A1GabmMY9lT zM%Kf=7yMWnXJPxdVu$ou^INNx4`y6eO8)uFq@)2E8%dWq}W^MPH9`EuONrs9Thb31T)qcy6kU?S&yPVw06H$2&TF0QFc%4|Lv1Mt?Zii65 zSkAn16Oz?O<^?gSw#PhJuPZW;!F>crSVir;kNjv%fobM&sqj8*YcEMo{BbWOAR+Q? zJBaqJ)z{RC<&}2-s;_k?x=|?PZ(4@N|Db$EKw%fI=6lX;?+1M+LMlw&2^~B_ED-|p zx#oML18GRsJ;vhWHv1Enx?kVab_g=`)jhJUwTjYRZ;P!mmo%kukOX^7)pF;GTp>Y` zIM&Geev?#RG-9KxS7t|dS&l~@fR%DFO2jlH5S|&dYirN!{kC)+|eqB!PwbXfWB5Uq`!XRZfebk zn(jOmOnVk4_5M+~UUUw>^tI%o+4%|DiO$^C(s0g;T9G^($rN!&3S%2vvBm>R!|GqW zH~3O6(wZZb5l;JZ1`Q!?Nq4HO^B^<7D9XYuX~lT^f~~hn{y9&tIA80MZ}*OShCBGU zM52FQ^cGYdKMp>}A%J!tX7*aFu)#I=>nNK={d@|7j#V7H)LFP%7!6@_5xY z#J@XfeZHJ+%emeW3xfAiQh~n)dUIY1yy*-6PGmP6q&yF`J0VVNSTA zC-T#FTw9A+CnX7pp4I?iin7dY#p+Tt?EQ3F9&%QFK>C#NMWrHb2vW>j-R<1VrH( z(A4u!y`URT+cjOt=4$?2sw3DcrH?FS9bTZj2pG{q-7Yk`G*XPuS;&s6UvQZI>BM8r zGcG;FE)4>^=v}U~bx#Lb`;Z6|y-U)gerlZ8ja{x&_X4^g^c#A`7P~sSAS{Z{iwPFc zZcugK)>|L-Jia3zqIlXZZ%1EUt+dFP@XMUMO z$>ET%Wjx4N;IrmLU{EF-Omm+#CsYe9%Cq}SHV&5;d+E5^dfw?o69w-s(w$_ zu1=b)fwPho8FGL7DMI59f*z-)2jeUR&_izD@%Cr5P$X>5yMUI3M&~k-PL4YM9)m9F z2sz2UY&jpZgYm)@~4gud=YNzHcyx;)giM8Ce>R5qaN)~qUL-UODt>bFrTGc7IC_1 zJN@-m#^zjXnQaZco8K})MBq9G=B56Y(^ilpIaymD-kcAOsrge+U52NTWmX)pj=NoE zK1e~WGV5jKn=>29ObgNa-c+`Au+Nj5^Q|H3wu)_McjiDoez4jAeW z#(5i;$Eq2w=G)2Gn|)!d!Ul!LkizSmUF5px)2@@0Io5~i=mT$2&2n&h{d&UXPhCWe z)e@uh0CLI~$~+6|N`Wf!r&fQVj1jQo7o_FDYNY5fwaCJKc$@whFj?h@7zPuIcpa`L zy@C`>a+9NXqbA1{kb?>^mWLWZC9VgRPGsFM=H9+g1uf%47m=xJjR@vocNH74t!GBD z46|N#9P&%sda}vqlvPs=z7|6ut-7onT+K3bW>G7@C36Sdy2DAjka@#0m<~Gb$nf^T%H>CDy3+An?MQDL}SKhdn z{Lw{Rthe@LmQW}O`_`O*8~Qyd&DOvGj{2HaO~Ohi3$5u@-+={$%rN>h=5AiVm7(Nk z3-E<|5NVeXXXl75XcLqku#DhC)A&(XDWf7Yrr$9rP)J&+ru-|0Y!?LR} zA_m3`Z}wzQHg0r19PN5!XZv5A2|L&UPm)8+p~qd1v~#J4HkP?nyIpJOAdZF;YH^*E ziCrx@ldN!s;-+mv|25pc&LOr}(Tc>>v|jcKAHQG{>)prSuK(V_U;0g3r)HfngPxJ} zu!&8LTZP#4AE8mA9{aK^_jLG!QBqku8nczLnVikl10^+CHx~WBWZ62Odw2)E!23A- z4THCPv4_CXnJEYf*$5AT4D%Fn*L&*GIINxP&QYvJpm!PfWf0IOV`zvXlA zW9$$#ufugWmNr&P;yJGvFZk9ipO}pSPO39ED(vkDdtFcNlFhv|{%{S(W^JkGo~CyW zvHuV%v)^xeKIF~W<8{s411q$XkrrmQ2Zoua=v)&?&h%=hbS<4T1cCLLx8c@{oDTE; z-9&0l@_Hohp4q`>T_$d3&GJNEFkax@7*7=0_vgg%%{bTPXZ80^+riCnyhwqr0eaUK zs7NGl(^Fw@^lN#o^BmsR$^)qRX7%??3mXd~0Z3sgDH!LXk5;fYKH^OrIs~E|lqgfd z-4Pfc`AD2;5@!T)GJ4`z5xyj<#F-YU7?Bs)Q>MuzPPAp%O%uVErRrTW;Fhww$+_M2 zn|L7T^63~D`7610Nd-%>8(q!I_y#)I{+8JcbvD4;c$JC|#5H0jA|@2u zSeE7d+Fy#I+8YJI_c%c;!?`Cv$8j)=VMp13H0iX)4XwS?T>EcOjPt+oeu~P244v! zH+>beG96^=2l3e({R%za%3Xu+A#V^T)pT4H8E3rg*meGdw8L#V zn*tbF-h`3m(8ay+^BXy2)$~==T3W#Jly%V&Lg5RMrZ#;Q9XP^wnxr&tPbk$U)`8b@ z5mriHFekmp6ald{Klr$o@V(>Sc;4iQ8gh$>^OIlD7G&(rk~QPuQ|zM!28YwCn6olm zUA>%qb>fP1dX% z)47TKI9A*F)zMg2W_uRvLUvBkZHcmZcL=4WtOK|&`4n-v*8H9T!oRNOJ8;2H>vQ_@ z@EN*r6;n6pgR#c!ik5LOu;dY`CShc}M8&6<*VITAuPw@&7Md@7o_bhPf!K$T$y555 zZnjV49t|jMkydlQuGR>HP%Cnr_*wHMUGv`@!k)oK4WTR#qAlEb(NU?`C_R$ zEa;iUUL^Wf)|%we?DKF%xwg-vZO;rhA0~;(f942GYj-ZB*qH`PP5v`SVAsD5qB%2$ zT_pqWZXs&$gZ$tD+dj{5yuD5Djw-nPU2UL;W}NTVhG@o{7m^_9p4OeNAk|y(efCm~ zedljT6$1>GHB;C1ZA|^gnIo;(2MA*g)qP_pS+PSkNTO+PvNa<1eX!-?MqMNYV_jn4 zXP4Q)GziVWH1qd5AwAF9jI$-(GF{U|Oib6Dq`!mhHQmAb=6A~yi`GoiD@FQ~t@pzW z{8;Pb$@wjwbU&AO^%i_q?Q5irZ00`L=#>@o*S34^PRFOU*3q)`X4x9p!<)Zl>HWFQ z&v4Fq=|=Cv$)PyblCnSAE)nZORje66LDp znMH~Wjp*F?&tNK3>sL1g{@1IE36Jj0uE862;Uc8S>=h4)e%q<)I86$r( zsF*4~O#Su`#VoDk=V|UTrXHCpXdd9I5R%sElD?H_Qtw0qIsVcFv{tj2gC1^QIxpzk zs^sX+p>Wz|C+Okt8o1G%$)8|$=Q9wW8C*E+(D8cUD6rBom;SAEj??L|vNeN5Wd5`u zoOa%c#D6RBYqOL6z3nQA@`ZjblZJlY#^*et{!Is?12H(6<74xZ3zm-GBXbNv`bXWF z=>=3#x$(t+suB02cjH@YI1wr^=bdHEuchRj-1wN_d46_U*SBY_SjM9S37cbDIcQU-NW89;*>RF2pnE5gbW{jxm zY&v;{asevxua78C(f~-yXeS3*sxx!RKs@f(h0s`tHJV4Iy|4KskPN#NjcJ#|9v=+| zMJ03vw~cA%CJ-<C07aQ=@Ii>V zdZh%;*|&H=)3-5;vzxxfT4Xg|t|!;)ye!Ez__22!(;2r8yTi3c4zse!=?foXzC^L3)QxW>^p<4~Bjuc5+QNEc=?>pr@tY)QxN$~z!4L(mG0(I~LxU|wg{ zp{w~zM)L>}JB5iNSk_q~LOD4fFTMh5xUT*Nl%R;~n!jqa;Vw$|%N@FOuI4u_Pt6eP z#gk$Lvh{L{kVUZfJ}za1(Mq=x8Fq{D`NnNE&%WO-^CECTD=z1~m4CKp2c-#~b@%GB zT1~*y_}Gtk8`0m(sz=S|CNB^vK1f4fH5nu4(HY>P|cqBZ{dAO*Jng z4C@!PUJ}g)Flxz?#h)nRH*HxUmiv0nH?t13$y(6kSk{?@J;oB!g`y)OsmzmAqnG^* zrEVE}7Km;J`x)3+*t4<~3%;F0=xTl0 z6AiA0+ig6@s#hL1Sho4HvyAq~E~F03#fWB)F`b8RpJi3wtl-_A3$s7AMpkF?at^uH z+=j#3I)5s`%sKl6f3D~tz*_vpZ~U#Ya{7wDbwRW&Bz`OelN|!~*cuRQsJ7}U67of% zQ~(_uFNpLK2sQTRGi(XvqY7&o5&vu3F@oJGJ4dZ6qC!dF#xf&1Oyqjdk6a9=w9cJi z-pW9WCWVyt1UjN*mafPU+xMVbpe<;Mp2ZjRDO8Boh%u{wp-Yh8S{y4&z^CdG=t4F> zN30$-phwz|fz|*)i)4=@rTo?@apo5+dKQd(-xtizYmJ%Cy=H|AL5u3GD+tD9a`&)Y z8(B$mFx8RwjsEE}rZ-}}$2>PdYedM+%lk`YUc1l9)L0gH>aKbyG}3G(pM2!6M(||r zKolQyuOU|HB!SPRFl=lfWjtqopi9O=)`fbvu4f{^UW)J_y|30g?|sJ&=k-KG#Em`3 z$spz9zABErwo{L?MkwQZA%0PEtF3ttzS@g3+i$Q?;b%qf$L*jNPP=WSaF>`2X`K%) zJM@O<*Tbc**f!lBm}qW-hPDFMBRGS6xlme7wFs4%Y?eJFZhY{_rks-SK$yuT-Wb{+VH%a9ud<(_u&mBY92r;BrY>Q?kMg~T<&UMU0h$; zK;K~L*;zHA536&J_mDti_03XR09KK`qB-N(#k2XWrU7_cIRBbh7Wv>wev zjsoVX9&LjC**qvjYdc*-ITv=eD8OZ~46c$4au5;T6-= z>`Ix}=aMFS^}!J_U`@B224Devf*6+NBEvqDiI_HIBBv9Mc{=%}neT(Vzr|WLqlLSguO>pyTWEdKU&+Ki zpL>j3{V{p1MbR-U=A+CaHnmzuthiiQi4L;OYoDqqK%OaxPTlNXH`94{asXphd2Hge zM1|r!Yp42~;=>e~T_fykJM(0hqrF!SzG)vDle{^vcjtuF_II$Qxnc;bU3PSdsN-XO zWS{p*26ODvKwz26iXj`S0DA?D_gKemlTO?(FLg5L@j@5X%%OE?&AcL8e6qCOjtD#W z(xLc<(EMoi-vA{|DLg%vxd1MMvM7i>W5$qQsJ`jjsDNB!d0rv=VmTiN#)+^{$ZRc~ zb^xB!Z?aG?31hckyh+XUxyszu3eG5Z zQZ=qeVz1_#w1@>2Ei;|#Vwdp>bFZDr1u9&yt``RO3!$<^0LT{C6a+F(Nm$whuUs!p zaI>>@c^p~`(TwK-69q1zC?cU$g4s+d@>=5L({XZW++0~6DVDiGJEbZ`80tjO7J&_o zKg??)7I;}O7dd29)4{>6HR}l0)6Oh`B&U=LF(iDYIa_dnhS}cM=`m8xg@|Fun3M63 zM%_YteB^4rK)3+42S&dTX4iI@1MNcOwwA?2O7Vd|nD*EOB3$ie#qf@wNYWkbmfxlQ zwgrad1zjlUnbY8if|l<~!8&CHDL44hA7=QnCmCbcMR5nsw9UpS^MQYt*lCv&HMg}o z){$4bmAh7w*Ezh?wgukE4StbV`fO-|C;JMAk=3{?YFgmr?DL}o$9r4Ph~d6TfAmvk zot45#It8O&Y#s*Vqo2yoFrM;?&e0o~to23j^|9&c@lOpX<3x)hQ*|`GHc*LzllcWw zE(6LOsWJc5$$?jW(I3Fpy1LBQ%PjIO@NE`I&w*?UqYZ?nQs}Zu6l>jv=xo z+KIVIOs68`eRW&38(k5(po3!nK`@rfXcugo6;|7#5!g=WaE7Z{w7ql3QCA|r`J>Zr zUI2HLzA2sdU+&XX@<)H2FVvsy4Ze;l84QLry~{uDmAvR7=4fy_s!YAKSPEF6%%DCE zu@#jbDdj;)DzMQvl@{k(a~-txmtL4zXtfVg4ZdhT_wX^2Jf0*OIxf&Xnc!fa{?IXk zeszge>)Fy)PSi#%bc6xNim+26M1LKUn?OXm$L{#)VwU^+TvjQ6gGo*ErP(}(t*#L^ zOL6DnX^Xmj4J01lB+PcIyzmQs>5C^##SeXO(O93$8Ehnu8VwaD?jSvX539-@9B7U5Bzr@FNQ%Ymnnh-6QZ zh5?Wx`J-iumWIztDCwm;5 zcP#hMW*4{utrxykBq>GtZ*TX|#ZoG$DSxk^DUY0E4Dj+-GDiS(KX0DaRTq}#YRu*%uEaqBS z%+*XpR?okc~?^MR8q*fYUw9!hta6w^M+{!3;UT2;V4sL**W9+<}38x`KsO`zU& zd6X0U`fU0X;$a?*YF+0*j`B`x3+%^cWgbV@VzN^LpJ%7!yL{~kbTY~8{`Ima*0hhM zkJL=GMKYZQVp^K3CiBO26u4%-Se_poemt{AP7=P@Fu20I>TT6k(0Y^VqSv7d#W%pt zAaO;8M+{FU50Bhrkfkp$C@3~JO{JJDvs|=U`_m!+wMlQOC@kb?S#JY_j!Z0jg%BAf z_tFr0X+SnEg@~;=NQUOg@)v;8MKs(V&y>}%LnLEcN>>}549QVDkT zdCcf+jYA}+_y-FL&Bpelco*CA=ff)7I=X$o?%lhS*W{PocJqer4@c02wHIYB>He;Z zE%`sn8n`kqwmw7<^XTHTcKQ9LtNbD-mCnP9Y1Jls@$#;V88P}UUPcG!d4f-w547ph zcrMyZ%Kz(sZE|}Vzt?T}sSTZ}mj6&2PO_ojhQ&5qYQyz5++f4IZ1|uJx7l!y4d1un zK^ruk8fhHGtjqYZy=!^dp6&4#;ec*ut7GV1Zmvf)`aEVkj5HoVq`zp&v(8}6{- zn>IXX!+x^g#c!|;$J%hZ4fAcd(1!IkY_{R`HoV)0kJ)gW4PUb1yEgpFhVdCzzC&#| z)`rt;m~TVFhK)A7)`qv+P$U00{wy6T`;%BRnrp$kFR`Gr(t>@X?zq?Tzi`;mzemDX zlvGuhm${8v_od~AyL@St;V!K$D|c7a*Di9`)z_AmH#Cf=^Xds#T3=pbl=uGTKE6Tm zU;k#+2CB>4HMNpfd8vG{{Yz@Zv!be|%w4$5sI0Bg0Rl$J!s>E@N&hInF{A7B*YQNR z-nF-yWyPiEI9vyA6|IT#g`P9EG#W6ueh|b z>axqL7uD3(T~Xg)1Qst@y6nmyEx&5TO1=Foh}8#bjH*TD?(+Kj+IqKANp^)4<)1Tm zuH~z}=H{J!X0KP}JEy>#cXp4@obP2#o{|*rt#Oys)m2xOmKar3b!AC|dr=8&Rf4}^ zlrO3?gypJhOJKdqa`!BEB>(EFh4m%%%iL8prM30-<)udTvhneS)#W7(q40BIM@&CBn_ z`9@_`gS(`mp?uN8>SgY-Kz&usrS2M%S}bT#kgA$0qpGC3>Pnq_e368Qx23@4#B?tV zT*|w9S#6-cH?HH|d4`*yi)tGTcXid}<)kjfsV{E`R2%Nv3U_Hqb+u#$r39x_OKTU^ z=_WdMLTPpVN$!e3O{u1-ZlNVTNYykL^?_1@!t-B$^i@|ElvLH|vP-!qNx5~?tf>uL zTIp`6D=DR=6TG^XY!4$?Z+cDaL$B_#ms^!Lr^uqWQ3=wuHKpa_zdJp8=aVJ*%px_x zu_u!<2?PF-RvDG_?`6Ufm-mh% z=^mRtcBHZrqofBFolla*3cZ@E?hNY7uLzVk2y(*xbL`HCN;S&s7gf>FU`F8qX$FCs zK!XrS3r5PG+mF{9?EN|$=aGlen7q2q|B9md~|#~1EK_*=GL_#n^3Av<{FV7+lXywp>_XI zE;;PImG{WlC4qk2=bf_@hkd`c&pXx=4*Sj$;9>7S?epHRvGMB0RgDb5(N{NKy}B_q zHkJ{1&6+hJo|V;D*tk|X)z}lW3+Fd7zA^|G7On*?_t?g@jl@z6!b6bF04p#v&70|N4G8+Pfdg=x_aNR!9CjJp3xv^UtBa+rQo^tX4h$qS(Iu zF8?C&-T$lW-YWc&wOaW<%>j;8-Txfl@fWEsx>PZ`c0h zx}R?N_v>%C@n=83>E>I0aqDfry!}^q+anC z*dHH%;>ka?wQt(IW$U)>J9a+x^fS*sx2xm%7hZhn<=wCBdG)nFzy8LXZ|(id+wZ*l z-uoYzoqrAO`|zWWyFU5!v(LZSf8gMkUw!=zmP*xsbpmwk3C?$#0R6Me|Ig06TJZFrln$g7s2Zz$PD${Cwr5 z%n{4$tv994u3dcC`#H?W@lrO9gFd?>I)mbGq`jvboFGc#2wjxbQkEe$C%OovHM-gA*sJSIZpuUU`{LZMa zvRz6QRR-!Cy5E$VUtU&I-piv1FI@y>5e4vABF#L?LV4) zy0|UjIdpR}xsq1i#eF;59Lf5fNH6)7+LCv;|L}flIR2^lJIl^G{F^gMIg92TmTrc- zpBmtpt>U_3_eR%6WeGl6Z0x2Ck5$7Lrne2QODj&zQfluwQL9sGeTGu!4`t)`ZHo|& zjChqX#icUlq;(D2o6_NGOR7sOPAGKri&FjSqp}>SQ7ZL;YK4jEr{eN=}w9&>_0G0 z4J=Dn1E&m810AU<0a{8NP*+hWD>Z;e@VyVek8%G5cqM5Fbhs0hyDUYyi;|U_eBJfK zyR6ztt#c&zQ^`ggXLEs*65AZ;j`W`to8?G%s`N6RqBxb#xAaMbO?9eN{8I5t#V>VI za$Uwr32MlcGBw0;flBTgus5+IzRg(|SKP1As_Pvf*x#L`+*>k~+einGA>c4rxg7&l zM%R$NX&pVZesCHSC>|-tg&ZPr^p95k9gnLh>O<4r=&v%!KZE=;$UkFJTAL$19z1#A zyL9*tJT*NX@litWtQ09r}TkY1#I zBQ*Y@PpMz>+-HYB4)>EhZ`tpTG^a{4c*^2b8n~rRN@+_u(yt?u|F6za>K&egk@%Xn z@zAzEw1viVlIt8U_@^uZK8jbadiW?YN+mi{R7R%o!h`U_AK-=iH7^Js*DAIAED)&eDCBr!wz!@_wnfNR7Bzoicy26#Hm4(T)JIEf!FHu zmAaoN5@##!Z+IecELtTiSCLD(9)MOuoN5U84=DnY){seq>U15wlt4YjQ%BU*oRqz~ z-g}pIQrg}@9Vy*>GN4$gT|6so+#E3u6*Ci_wqc~)XD+0@@!Uo@fqlRK48L1=gtrBz z42cK7WN>q-A@zg0Quew!lG+khc{ouJ|HSmD}bxFmEg;u)#;ZLV>NxG4EbNbck{%}rIVT$et3B&gY?yoFX z>MuNDyKET~z42xI8$_A)mQDO6(Nye#3 zxuc9!@*hNf4OD|>4R|2F%el8-M@(Ck-Os_k%A!XK^nedvNT|!0m~`40BUz22zaK_= zLnaTbAJCP!H@?H!7U>_Q%~|o_Tf%7G9T24kOp4F?du4w32HFu%q|A=N@oF%*4hB?&M^fOCWO&2{%?GFv*I7K0qT5RnxD!X0VKw}t`)>LP`VhOX=f3MUHy?Ll8Ma91W z52eZ&$vheQrb1t20jnP`N`xNt<@NAIX8dV`C#P)ci;du``AGN>9!j5++SOBw@pgMl zA|2AYPTDavz5Q@GB%ZPI@A1vPZAy*Y-ivQW$E(p(GSui#hjyj!9o&)HHn1+GI5{HI z6sDv`tJK?*>s-Y>{m-sl^uIj!M`$2CF$ekQ=>1SvPe0Vd7mnB{6+4Ahv*G>KaOA*V zB`Hjx92sL65Bt_yp(V2|l{(Y3hQ>un&^l42UYA^#l_I@?^{bHm=&s1yk?>#o5*Dqp zY<-4*=}TDj_-E-$%ypbuUQ=GrhS4l*M{Jf+U!A*{y%^NF`DTb#z$|ubyEOyqW9FAs z8E4ei&t+Gpy4;$Hs_WG(t=C`&^D6aV^xSe{>TNbj)L&9lR?STQ3rV%0wk%Lxeg+$} zXS4r8=s&C68uqSc)w3vDtmBlS#E#{UUmfQ9Z9_36t;K zrRsAjil9t;R$wB4fTQGDC-J(TTH3DqWtWMo>5=U zy36g_?X70VQ(dIXQYa);MdJ3(DnxDzSX%QR210-;`^=0zo-Q1OO|06%B zf@8#(uhz!QuPQ5_RasJBR9hfB$upN3z5Ul*K17clcK89%WZzXw->!^)`VblHJ9t9nIg1XybYSeajDsXxDt}f%Nu5PAsGbqsUGAdV2r<;# zy+cuMkJa*o&eGP1H|ua8!gNah`C2K%YR+n(@Q36cVKa4)MZc;m!Oo{KE&FYhkxd58Oe z^&5g?FP=HCq`pd&HN0we?wqr8^I4xOt7d_-GI|aw29hrA$%<2UPKEV;g3!XQKxv~& zJuTR4Bn+5yVF3LaX!hUr+na0YV@1-7ydSnpk{tPZY$!6e9vlqEvsK7$Wg(q41uH9|xbL+k9GYfJgMgJPnqxnbtqz_;TUbk(* zA=-Aw0MmJ5d6Ibg@yG%CIG#ivrwzqV-UU7RmcSGFCh1CCfi50NU%DpoOW|P|K|kU@ znn(OkyDC-I{m)*^nLG}|G(b<5fn&1=FiH_eazoK z0-OK&G>@&EVc~LY<$(WrT>nuy9+L%Zsq&aC;QmKp^iNIq|8AU*2iKRk!Z_MqHj1jT+uf`1W7D_A9sb`G~)(4q09v8$R?M!+Y)U z4-KNxDkevJ4#jm;5C9hrf+N2}Hzqsekyqd-U zy8Rv@w$px3zsbtyzYEfw^*rDunfGzuA6YZbQR{l{PH4VWAB@Hq5r+6dR7UVX_UC4f`{Ji?lf*e55^&x2mE0 zug7lJ)iW(R{a4{i`xogi1P948f{XA+q>T#_jZDzwTh}L6KTtTgNWA~kze3-CE&g7c z9`4B&J^J=fecxqVkzWLgTiSdM&jmcvUT@%ei037q&v<0}GK=SIo<&l4evx>nMk$%g zF5$VJ=Ruwqc|PSyChP>B0v@rh`~So5?`fAu_4!5Hzew4$`&sprWy7&Hblb2uuSMeg zKMm(;3GWG;1>8Y&*Qic0v9nTPLFda&U~v27!WH5I27l&RTGck z&gW z&A@qdl2dRm0Jie@a9<02g-6oa13YPhQu+9w0{kscG46YTKcUkwaBl#vLZ|XZ+|59_ z`%dVy1=#rm#sK{H0k1fny6f*yj{{%l5qt!GW4i^;^jP`&fcNuAUIHh3iGzCz@KM|S zIM6rK;wyoxcoIp!88~GY`;oW>{*LE1I9>I#7NjTz&UwVxr%_hcsdCG4KVpEiw*)WmePdYLxGd#S!FcNV<(75 z%J>Y>JD)ltd@*nhkAz!u_-mf^l0I+?kL0xjczu~g+bzJ;E~Wp$zYw^F=XKoI0ypspK3jmX zl~!ErLnycH7WgwB!RKb+(^XdeJ_Eeza>`CRHv_L@(Kj6)*Z@4EhC0IS2X5f;h(GYm zTC4@(E(SL9EWo`5I2rjv+Q<~(G9Kw4mIF82?%S?_{~IU^;RSBtk?_v|R~uGcHv{Jf zEcXK7r#y9p{~UPLVv9c;f%|zj;C~Q!-U|2z_X1$cN@#+6J@D2>>M@D>1zxaKEB zOlYPoxD$bSE#QwkANV#;Bkp&BXRn7ZaTfw#<=Kck^IG)AuY-SZCj%GoNZScTE&^_~-IdpKT{n-^g$Oj?zmfJun%Tf0kJRIOVB^mf89FrVz%8^A zIQS;ZoeA8^lTMf&z_WfqedEptF6WW(0<&+m@)B5h8~%h5cny!_wHA2uFQGGTfl0qY zh6H~%a2JoX>ki=ZJCs^W7=h|eD}8}?@!W`i2XNo7p$~3>r{7Iq0}dYG*586B?&0^K z>wK@3eiksuBY3U{Zs+mg#(s&4{+-3cF~B={q_4Xh_~+l#XA$Ogf%h{;;}-bC{{t`L zE(4zT0Qlfu0G#v)^GDoMfKTv9J+=W~-e|e^0M|Ya&V&(ofJgZ4An>Cntg$ciNn}VK z!E-6_z*g|beGqurcFG8D)xgVkL2GdX&+mXga9;@ggh%+{b70^_%0;~|1tz}?&iD(w zi$|9cxOg}11plSLM|dRPjliZ?!5RN%VDX#q3~qs4Jd(b^H{P;vHi7s2#iDZ;@CR?h zPt=Q?%aF4Y>!rN_<;=rN;3H6U`^7C#^!CLq@MYWm7Etu>#b2Q4$BSE_=&y@g;2E}C z;3c-Z0w_A+5=P)pZMW!ux7%)kqMt3e2^4*22`^CekHuf0=u!2bmiE2a|w diff --git a/libs/common/setuptools/gui.exe b/libs/common/setuptools/gui.exe index f8d3509653ba8f80ca7f3aa7f95616142ba83a94..1eb430c6d614a5daea4139badc09c222a4b0e72a 100644 GIT binary patch literal 11776 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 z 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 14 and version > best_version: + best_version, best_dir = version, vc_dir + return best_version, best_dir + + +def _msvc14_find_vc2017(): + """Python 3.8 "distutils/_msvccompiler.py" backport + + Returns "15, path" based on the result of invoking vswhere.exe + If no install is found, returns "None, None" + + The version is returned to avoid unnecessarily changing the function + result. It may be ignored when the path is not None. + + If vswhere.exe is not available, by definition, VS 2017 is not + installed. + """ + root = environ.get("ProgramFiles(x86)") or environ.get("ProgramFiles") + if not root: + return None, None + + suitable_components = ( + "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", + "Microsoft.VisualStudio.Workload.WDExpress", + ) + + for component in suitable_components: + # Workaround for `-requiresAny` (only available on VS 2017 > 15.6) + with contextlib.suppress(CalledProcessError, OSError, UnicodeDecodeError): + path = ( + subprocess.check_output([ + join(root, "Microsoft Visual Studio", "Installer", "vswhere.exe"), + "-latest", + "-prerelease", + "-requires", + component, + "-property", + "installationPath", + "-products", + "*", + ]) + .decode(encoding="mbcs", errors="strict") + .strip() + ) + + path = join(path, "VC", "Auxiliary", "Build") + if isdir(path): + return 15, path + + return None, None # no suitable component found + + +PLAT_SPEC_TO_RUNTIME = { + 'x86': 'x86', + 'x86_amd64': 'x64', + 'x86_arm': 'arm', + 'x86_arm64': 'arm64', +} + + +def _msvc14_find_vcvarsall(plat_spec): + """Python 3.8 "distutils/_msvccompiler.py" backport""" + _, best_dir = _msvc14_find_vc2017() + vcruntime = None + + if plat_spec in PLAT_SPEC_TO_RUNTIME: + vcruntime_plat = PLAT_SPEC_TO_RUNTIME[plat_spec] + else: + vcruntime_plat = 'x64' if 'amd64' in plat_spec else 'x86' + + if best_dir: + vcredist = join( + best_dir, + "..", + "..", + "redist", + "MSVC", + "**", + vcruntime_plat, + "Microsoft.VC14*.CRT", + "vcruntime140.dll", + ) try: - # All-user installs on a 64-bit system register here - key = vc_base % ('Wow6432Node\\', version) - productdir = Reg.get_value(key, "installdir") - except KeyError: - productdir = None + import glob - if productdir: - vcvarsall = join(productdir, "vcvarsall.bat") - if isfile(vcvarsall): - return vcvarsall + vcruntime = glob.glob(vcredist, recursive=True)[-1] + except (ImportError, OSError, LookupError): + vcruntime = None - return get_unpatched(msvc9_find_vcvarsall)(version) + if not best_dir: + best_version, best_dir = _msvc14_find_vc2015() + if best_version: + vcruntime = join( + best_dir, + 'redist', + vcruntime_plat, + "Microsoft.VC140.CRT", + "vcruntime140.dll", + ) + + if not best_dir: + return None, None + + vcvarsall = join(best_dir, "vcvarsall.bat") + if not isfile(vcvarsall): + return None, None + + if not vcruntime or not isfile(vcruntime): + vcruntime = None + + return vcvarsall, vcruntime -def msvc9_query_vcvarsall(ver, arch='x86', *args, **kwargs): - """ - Patched "distutils.msvc9compiler.query_vcvarsall" for support extra - Microsoft Visual C++ 9.0 and 10.0 compilers. +def _msvc14_get_vc_env(plat_spec): + """Python 3.8 "distutils/_msvccompiler.py" backport""" + if "DISTUTILS_USE_SDK" in environ: + return {key.lower(): value for key, value in environ.items()} - Set environment without use of "vcvarsall.bat". + vcvarsall, vcruntime = _msvc14_find_vcvarsall(plat_spec) + if not vcvarsall: + raise distutils.errors.DistutilsPlatformError("Unable to find vcvarsall.bat") - Parameters - ---------- - ver: float - Required Microsoft Visual C++ version. - arch: str - Target architecture. - - Return - ------ - dict - environment - """ - # Try to get environment from vcvarsall.bat (Classical way) try: - orig = get_unpatched(msvc9_query_vcvarsall) - return orig(ver, arch, *args, **kwargs) - except distutils.errors.DistutilsPlatformError: - # Pass error if Vcvarsall.bat is missing - pass - except ValueError: - # Pass error if environment not set after executing vcvarsall.bat - pass + out = subprocess.check_output( + 'cmd /u /c "{}" {} && set'.format(vcvarsall, plat_spec), + stderr=subprocess.STDOUT, + ).decode('utf-16le', errors='replace') + except subprocess.CalledProcessError as exc: + raise distutils.errors.DistutilsPlatformError( + "Error executing {}".format(exc.cmd) + ) from exc - # If error, try to set environment directly - try: - return EnvironmentInfo(arch, ver).return_env() - except distutils.errors.DistutilsPlatformError as exc: - _augment_exception(exc, ver, arch) - raise + env = { + key.lower(): value + for key, _, value in (line.partition('=') for line in out.splitlines()) + if key and value + } + + if vcruntime: + env['py_vcruntime_redist'] = vcruntime + return env def msvc14_get_vc_env(plat_spec): @@ -159,34 +222,15 @@ def msvc14_get_vc_env(plat_spec): dict environment """ - # Try to get environment from vcvarsall.bat (Classical way) - try: - return get_unpatched(msvc14_get_vc_env)(plat_spec) - except distutils.errors.DistutilsPlatformError: - # Pass error Vcvarsall.bat is missing - pass - # If error, try to set environment directly + # Always use backport from CPython 3.8 try: - return EnvironmentInfo(plat_spec, vc_min_ver=14.0).return_env() + return _msvc14_get_vc_env(plat_spec) except distutils.errors.DistutilsPlatformError as exc: _augment_exception(exc, 14.0) raise -def msvc14_gen_lib_options(*args, **kwargs): - """ - Patched "distutils._msvccompiler.gen_lib_options" for fix - compatibility between "numpy.distutils" and "distutils._msvccompiler" - (for Numpy < 1.11.2) - """ - if "numpy.distutils" in sys.modules: - import numpy as np - if LegacyVersion(np.__version__) < LegacyVersion('1.11.2'): - return np.distutils.ccompiler.gen_lib_options(*args, **kwargs) - return get_unpatched(msvc14_gen_lib_options)(*args, **kwargs) - - def _augment_exception(exc, version, arch=''): """ Add details to the exception message to help guide the user @@ -197,7 +241,7 @@ def _augment_exception(exc, version, arch=''): if "vcvarsall" in message.lower() or "visual c" in message.lower(): # Special error message if MSVC++ not installed - tmpl = 'Microsoft Visual C++ {version:0.1f} is required.' + tmpl = 'Microsoft Visual C++ {version:0.1f} or greater is required.' message = tmpl.format(**locals()) msdownload = 'www.microsoft.com/download/details.aspx?id=%d' if version == 9.0: @@ -217,10 +261,13 @@ def _augment_exception(exc, version, arch=''): message += msdownload % 8279 elif version >= 14.0: # For VC++ 14.X Redirect user to latest Visual C++ Build Tools - message += (' Get it with "Build Tools for Visual Studio": ' - r'https://visualstudio.microsoft.com/downloads/') + message += ( + ' Get it with "Microsoft C++ Build Tools": ' + r'https://visualstudio.microsoft.com' + r'/visual-cpp-build-tools/' + ) - exc.args = (message, ) + exc.args = (message,) class PlatformInfo: @@ -232,6 +279,7 @@ class PlatformInfo: arch: str Target architecture. """ + current_cpu = environ.get('processor_architecture', '').lower() def __init__(self, arch): @@ -247,7 +295,7 @@ class PlatformInfo: str Target CPU """ - return self.arch[self.arch.find('_') + 1:] + return self.arch[self.arch.find('_') + 1 :] def target_is_x86(self): """ @@ -288,9 +336,11 @@ class PlatformInfo: subfolder: '\target', or '' (see hidex86 parameter) """ return ( - '' if (self.current_cpu == 'x86' and hidex86) else - r'\x64' if (self.current_cpu == 'amd64' and x64) else - r'\%s' % self.current_cpu + '' + if (self.current_cpu == 'x86' and hidex86) + else r'\x64' + if (self.current_cpu == 'amd64' and x64) + else r'\%s' % self.current_cpu ) def target_dir(self, hidex86=False, x64=False): @@ -310,9 +360,11 @@ class PlatformInfo: subfolder: '\current', or '' (see hidex86 parameter) """ return ( - '' if (self.target_cpu == 'x86' and hidex86) else - r'\x64' if (self.target_cpu == 'amd64' and x64) else - r'\%s' % self.target_cpu + '' + if (self.target_cpu == 'x86' and hidex86) + else r'\x64' + if (self.target_cpu == 'amd64' and x64) + else r'\%s' % self.target_cpu ) def cross_dir(self, forcex86=False): @@ -333,8 +385,9 @@ class PlatformInfo: """ current = 'x86' if forcex86 else self.current_cpu return ( - '' if self.target_cpu == current else - self.target_dir().replace('\\', '\\%s_' % current) + '' + if self.target_cpu == current + else self.target_dir().replace('\\', '\\%s_' % current) ) @@ -347,10 +400,13 @@ class RegistryInfo: platform_info: PlatformInfo "PlatformInfo" instance. """ - HKEYS = (winreg.HKEY_USERS, - winreg.HKEY_CURRENT_USER, - winreg.HKEY_LOCAL_MACHINE, - winreg.HKEY_CLASSES_ROOT) + + HKEYS = ( + winreg.HKEY_USERS, + winreg.HKEY_CURRENT_USER, + winreg.HKEY_LOCAL_MACHINE, + winreg.HKEY_CLASSES_ROOT, + ) def __init__(self, platform_info): self.pi = platform_info @@ -500,22 +556,28 @@ class RegistryInfo: """ key_read = winreg.KEY_READ openkey = winreg.OpenKey + closekey = winreg.CloseKey ms = self.microsoft for hkey in self.HKEYS: + bkey = None try: bkey = openkey(hkey, ms(key), 0, key_read) - except (OSError, IOError): + except OSError: if not self.pi.current_is_x86(): try: bkey = openkey(hkey, ms(key, True), 0, key_read) - except (OSError, IOError): + except OSError: continue else: continue try: return winreg.QueryValueEx(bkey, name)[0] - except (OSError, IOError): + except OSError: pass + finally: + if bkey: + closekey(bkey) + return None class SystemInfo: @@ -543,8 +605,7 @@ class SystemInfo: self.known_vs_paths = self.find_programdata_vs_vers() # Except for VS15+, VC version is aligned with VS version - self.vs_ver = self.vc_ver = ( - vc_ver or self._find_latest_available_vs_ver()) + self.vs_ver = self.vc_ver = vc_ver or self._find_latest_available_vs_ver() def _find_latest_available_vs_ver(self): """ @@ -559,7 +620,8 @@ class SystemInfo: if not (reg_vc_vers or self.known_vs_paths): raise distutils.errors.DistutilsPlatformError( - 'No Microsoft Visual C++ version found') + 'No Microsoft Visual C++ version found' + ) vc_vers = set(reg_vc_vers) vc_vers.update(self.known_vs_paths) @@ -577,27 +639,23 @@ class SystemInfo: ms = self.ri.microsoft vckeys = (self.ri.vc, self.ri.vc_for_python, self.ri.vs) vs_vers = [] - for hkey in self.ri.HKEYS: - for key in vckeys: - try: - bkey = winreg.OpenKey(hkey, ms(key), 0, winreg.KEY_READ) - except (OSError, IOError): - continue + for hkey, key in itertools.product(self.ri.HKEYS, vckeys): + try: + bkey = winreg.OpenKey(hkey, ms(key), 0, winreg.KEY_READ) + except OSError: + continue + with bkey: subkeys, values, _ = winreg.QueryInfoKey(bkey) for i in range(values): - try: + with contextlib.suppress(ValueError): ver = float(winreg.EnumValue(bkey, i)[0]) if ver not in vs_vers: vs_vers.append(ver) - except ValueError: - pass for i in range(subkeys): - try: + with contextlib.suppress(ValueError): ver = float(winreg.EnumKey(bkey, i)) if ver not in vs_vers: vs_vers.append(ver) - except ValueError: - pass return sorted(vs_vers) def find_programdata_vs_vers(self): @@ -611,13 +669,12 @@ class SystemInfo: float version as key, path as value. """ vs_versions = {} - instances_dir = \ - r'C:\ProgramData\Microsoft\VisualStudio\Packages\_Instances' + instances_dir = r'C:\ProgramData\Microsoft\VisualStudio\Packages\_Instances' try: hashed_names = listdir(instances_dir) - except (OSError, IOError): + except OSError: # Directory not exists with all Visual Studio versions return vs_versions @@ -633,10 +690,11 @@ class SystemInfo: listdir(join(vs_path, r'VC\Tools\MSVC')) # Store version and path - vs_versions[self._as_float_version( - state['installationVersion'])] = vs_path + vs_versions[self._as_float_version(state['installationVersion'])] = ( + vs_path + ) - except (OSError, IOError, KeyError): + except (OSError, KeyError): # Skip if "state.json" file is missing or bad format continue @@ -670,8 +728,9 @@ class SystemInfo: path """ # Default path - default = join(self.ProgramFilesx86, - 'Microsoft Visual Studio %0.1f' % self.vs_ver) + default = join( + self.ProgramFilesx86, 'Microsoft Visual Studio %0.1f' % self.vs_ver + ) # Try to get path from registry, if fail use default path return self.ri.lookup(self.ri.vs, '%0.1f' % self.vs_ver) or default @@ -721,7 +780,7 @@ class SystemInfo: vc_ver = listdir(guess_vc)[-1] self.vc_ver = self._as_float_version(vc_ver) return join(guess_vc, vc_ver) - except (OSError, IOError, IndexError): + except (OSError, IndexError): return '' def _guess_vc_legacy(self): @@ -733,8 +792,9 @@ class SystemInfo: str path """ - default = join(self.ProgramFilesx86, - r'Microsoft Visual Studio %0.1f\VC' % self.vs_ver) + default = join( + self.ProgramFilesx86, r'Microsoft Visual Studio %0.1f\VC' % self.vs_ver + ) # Try to get "VC++ for Python" path from registry as default path reg_path = join(self.ri.vc_for_python, '%0.1f' % self.vs_ver) @@ -764,6 +824,7 @@ class SystemInfo: return '8.1', '8.1a' elif self.vs_ver >= 14.0: return '10.0', '8.1' + return None @property def WindowsSdkLastVersion(self): @@ -777,8 +838,8 @@ class SystemInfo: """ return self._use_last_dir_name(join(self.WindowsSdkDir, 'lib')) - @property - def WindowsSdkDir(self): + @property # noqa: C901 + def WindowsSdkDir(self): # noqa: C901 # is too complex (12) # FIXME """ Microsoft Windows SDK directory. @@ -803,7 +864,7 @@ class SystemInfo: if not sdkdir or not isdir(sdkdir): # If fail, use default new path for ver in self.WindowsSdkVersion: - intver = ver[:ver.rfind('.')] + intver = ver[: ver.rfind('.')] path = r'Microsoft SDKs\Windows Kits\%s' % intver d = join(self.ProgramFiles, path) if isdir(d): @@ -855,6 +916,8 @@ class SystemInfo: if execpath: return execpath + return None + @property def FSharpInstallDir(self): """ @@ -883,11 +946,12 @@ class SystemInfo: # Find path of the more recent Kit for ver in vers: - sdkdir = self.ri.lookup(self.ri.windows_kits_roots, - 'kitsroot%s' % ver) + sdkdir = self.ri.lookup(self.ri.windows_kits_roots, 'kitsroot%s' % ver) if sdkdir: return sdkdir or '' + return None + @property def UniversalCRTSdkLastVersion(self): """ @@ -911,10 +975,11 @@ class SystemInfo: versions """ # Set FxSdk versions for specified VS version - return (('4.7.2', '4.7.1', '4.7', - '4.6.2', '4.6.1', '4.6', - '4.5.2', '4.5.1', '4.5') - if self.vs_ver >= 14.0 else ()) + return ( + ('4.7.2', '4.7.1', '4.7', '4.6.2', '4.6.1', '4.6', '4.5.2', '4.5.1', '4.5') + if self.vs_ver >= 14.0 + else () + ) @property def NetFxSdkDir(self): @@ -1039,8 +1104,7 @@ class SystemInfo: matching_dirs = ( dir_name for dir_name in reversed(listdir(path)) - if isdir(join(path, dir_name)) and - dir_name.startswith(prefix) + if isdir(join(path, dir_name)) and dir_name.startswith(prefix) ) return next(matching_dirs, None) or '' @@ -1132,8 +1196,10 @@ class EnvironmentInfo: list of str paths """ - return [join(self.si.VCInstallDir, 'Include'), - join(self.si.VCInstallDir, r'ATLMFC\Include')] + return [ + join(self.si.VCInstallDir, 'Include'), + join(self.si.VCInstallDir, r'ATLMFC\Include'), + ] @property def VCLibraries(self): @@ -1193,14 +1259,15 @@ class EnvironmentInfo: tools += [join(si.VCInstallDir, path)] elif self.vs_ver >= 15.0: - host_dir = (r'bin\HostX86%s' if self.pi.current_is_x86() else - r'bin\HostX64%s') - tools += [join( - si.VCInstallDir, host_dir % self.pi.target_dir(x64=True))] + host_dir = ( + r'bin\HostX86%s' if self.pi.current_is_x86() else r'bin\HostX64%s' + ) + tools += [join(si.VCInstallDir, host_dir % self.pi.target_dir(x64=True))] if self.pi.current_cpu != self.pi.target_cpu: - tools += [join( - si.VCInstallDir, host_dir % self.pi.current_dir(x64=True))] + tools += [ + join(si.VCInstallDir, host_dir % self.pi.current_dir(x64=True)) + ] else: tools += [join(si.VCInstallDir, 'Bin')] @@ -1225,7 +1292,7 @@ class EnvironmentInfo: arch_subdir = self.pi.target_dir(x64=True) lib = join(self.si.WindowsSdkDir, 'lib') libver = self._sdk_subdir - return [join(lib, '%sum%s' % (libver , arch_subdir))] + return [join(lib, '%sum%s' % (libver, arch_subdir))] @property def OSIncludes(self): @@ -1247,9 +1314,11 @@ class EnvironmentInfo: sdkver = self._sdk_subdir else: sdkver = '' - return [join(include, '%sshared' % sdkver), - join(include, '%sum' % sdkver), - join(include, '%swinrt' % sdkver)] + return [ + join(include, '%sshared' % sdkver), + join(include, '%sum' % sdkver), + join(include, '%swinrt' % sdkver), + ] @property def OSLibpath(self): @@ -1276,11 +1345,16 @@ class EnvironmentInfo: join(self.si.WindowsSdkDir, 'UnionMetadata'), join(ref, 'Windows.Foundation.UniversalApiContract', '1.0.0.0'), join(ref, 'Windows.Foundation.FoundationContract', '1.0.0.0'), - join(ref,'Windows.Networking.Connectivity.WwanContract', - '1.0.0.0'), - join(self.si.WindowsSdkDir, 'ExtensionSDKs', 'Microsoft.VCLibs', - '%0.1f' % self.vs_ver, 'References', 'CommonConfiguration', - 'neutral'), + join(ref, 'Windows.Networking.Connectivity.WwanContract', '1.0.0.0'), + join( + self.si.WindowsSdkDir, + 'ExtensionSDKs', + 'Microsoft.VCLibs', + '%0.1f' % self.vs_ver, + 'References', + 'CommonConfiguration', + 'neutral', + ), ] return libpath @@ -1381,11 +1455,9 @@ class EnvironmentInfo: tools = [] if include32: - tools += [join(si.FrameworkDir32, ver) - for ver in si.FrameworkVersion32] + tools += [join(si.FrameworkDir32, ver) for ver in si.FrameworkVersion32] if include64: - tools += [join(si.FrameworkDir64, ver) - for ver in si.FrameworkVersion64] + tools += [join(si.FrameworkDir64, ver) for ver in si.FrameworkVersion64] return tools @property @@ -1561,9 +1633,11 @@ class EnvironmentInfo: prefixes += [join(tools_path, 'redist')] # VS14 legacy path # CRT directory - crt_dirs = ('Microsoft.VC%d.CRT' % (self.vc_ver * 10), - # Sometime store in directory with VS version instead of VC - 'Microsoft.VC%d.CRT' % (int(self.vs_ver) * 10)) + crt_dirs = ( + 'Microsoft.VC%d.CRT' % (self.vc_ver * 10), + # Sometime store in directory with VS version instead of VC + 'Microsoft.VC%d.CRT' % (int(self.vs_ver) * 10), + ) # vcruntime path for prefix, crt_dir in itertools.product(prefixes, crt_dirs): @@ -1586,36 +1660,47 @@ class EnvironmentInfo: environment """ env = dict( - include=self._build_paths('include', - [self.VCIncludes, - self.OSIncludes, - self.UCRTIncludes, - self.NetFxSDKIncludes], - exists), - lib=self._build_paths('lib', - [self.VCLibraries, - self.OSLibraries, - self.FxTools, - self.UCRTLibraries, - self.NetFxSDKLibraries], - exists), - libpath=self._build_paths('libpath', - [self.VCLibraries, - self.FxTools, - self.VCStoreRefs, - self.OSLibpath], - exists), - path=self._build_paths('path', - [self.VCTools, - self.VSTools, - self.VsTDb, - self.SdkTools, - self.SdkSetup, - self.FxTools, - self.MSBuild, - self.HTMLHelpWorkshop, - self.FSharp], - exists), + include=self._build_paths( + 'include', + [ + self.VCIncludes, + self.OSIncludes, + self.UCRTIncludes, + self.NetFxSDKIncludes, + ], + exists, + ), + lib=self._build_paths( + 'lib', + [ + self.VCLibraries, + self.OSLibraries, + self.FxTools, + self.UCRTLibraries, + self.NetFxSDKLibraries, + ], + exists, + ), + libpath=self._build_paths( + 'libpath', + [self.VCLibraries, self.FxTools, self.VCStoreRefs, self.OSLibpath], + exists, + ), + path=self._build_paths( + 'path', + [ + self.VCTools, + self.VSTools, + self.VsTDb, + self.SdkTools, + self.SdkSetup, + self.FxTools, + self.MSBuild, + self.HTMLHelpWorkshop, + self.FSharp, + ], + exists, + ), ) if self.vs_ver >= 14 and isfile(self.VCRuntimeRedist): env['py_vcruntime_redist'] = self.VCRuntimeRedist @@ -1651,29 +1736,5 @@ class EnvironmentInfo: if not extant_paths: msg = "%s environment variable is empty" % name.upper() raise distutils.errors.DistutilsPlatformError(msg) - unique_paths = self._unique_everseen(extant_paths) + unique_paths = unique_everseen(extant_paths) return pathsep.join(unique_paths) - - # from Python docs - @staticmethod - def _unique_everseen(iterable, key=None): - """ - List unique elements, preserving order. - Remember all elements ever seen. - - _unique_everseen('AAAABBBCCDAABBB') --> A B C D - - _unique_everseen('ABBCcAD', str.lower) --> A B C D - """ - seen = set() - seen_add = seen.add - if key is None: - for element in filterfalse(seen.__contains__, iterable): - seen_add(element) - yield element - else: - for element in iterable: - k = key(element) - if k not in seen: - seen_add(k) - yield element diff --git a/libs/common/setuptools/namespaces.py b/libs/common/setuptools/namespaces.py index dc16106d..e8f2941d 100644 --- a/libs/common/setuptools/namespaces.py +++ b/libs/common/setuptools/namespaces.py @@ -2,22 +2,18 @@ import os from distutils import log import itertools -from setuptools.extern.six.moves import map - flatten = itertools.chain.from_iterable class Installer: - nspkg_ext = '-nspkg.pth' def install_namespaces(self): nsp = self._get_all_ns_packages() if not nsp: return - filename, ext = os.path.splitext(self._get_target()) - filename += self.nspkg_ext + filename = self._get_nspkg_file() self.outputs.append(filename) log.info("Installing %s", filename) lines = map(self._gen_nspkg_line, nsp) @@ -31,45 +27,44 @@ class Installer: f.writelines(lines) def uninstall_namespaces(self): - filename, ext = os.path.splitext(self._get_target()) - filename += self.nspkg_ext + filename = self._get_nspkg_file() if not os.path.exists(filename): return log.info("Removing %s", filename) os.remove(filename) + def _get_nspkg_file(self): + filename, _ = os.path.splitext(self._get_target()) + return filename + self.nspkg_ext + def _get_target(self): return self.target _nspkg_tmpl = ( "import sys, types, os", - "has_mfs = sys.version_info > (3, 5)", "p = os.path.join(%(root)s, *%(pth)r)", - "importlib = has_mfs and __import__('importlib.util')", - "has_mfs and __import__('importlib.machinery')", - "m = has_mfs and " + "importlib = __import__('importlib.util')", + "__import__('importlib.machinery')", + ( + "m = " "sys.modules.setdefault(%(pkg)r, " - "importlib.util.module_from_spec(" - "importlib.machinery.PathFinder.find_spec(%(pkg)r, " - "[os.path.dirname(p)])))", - "m = m or " - "sys.modules.setdefault(%(pkg)r, types.ModuleType(%(pkg)r))", + "importlib.util.module_from_spec(" + "importlib.machinery.PathFinder.find_spec(%(pkg)r, " + "[os.path.dirname(p)])))" + ), + ("m = m or " "sys.modules.setdefault(%(pkg)r, types.ModuleType(%(pkg)r))"), "mp = (m or []) and m.__dict__.setdefault('__path__',[])", "(p not in mp) and mp.append(p)", ) "lines for the namespace installer" - _nspkg_tmpl_multi = ( - 'm and setattr(sys.modules[%(parent)r], %(child)r, m)', - ) + _nspkg_tmpl_multi = ('m and setattr(sys.modules[%(parent)r], %(child)r, m)',) "additional line(s) when a parent package is indicated" def _get_root(self): return "sys._getframe(1).f_locals['sitedir']" def _gen_nspkg_line(self, pkg): - # ensure pkg is not a unicode string under Python 2.7 - pkg = str(pkg) pth = tuple(pkg.split('.')) root = self._get_root() tmpl_lines = self._nspkg_tmpl @@ -81,7 +76,7 @@ class Installer: def _get_all_ns_packages(self): """Return sorted list of all package namespaces""" pkgs = self.distribution.namespace_packages or [] - return sorted(flatten(map(self._pkg_names, pkgs))) + return sorted(set(flatten(map(self._pkg_names, pkgs)))) @staticmethod def _pkg_names(pkg): diff --git a/libs/common/setuptools/package_index.py b/libs/common/setuptools/package_index.py index f419d471..271aa97f 100644 --- a/libs/common/setuptools/package_index.py +++ b/libs/common/setuptools/package_index.py @@ -1,33 +1,45 @@ -"""PyPI and direct package downloading""" +"""PyPI and direct package downloading.""" + import sys import os import re +import io import shutil import socket import base64 import hashlib import itertools -import warnings +import configparser +import html +import http.client +import urllib.parse +import urllib.request +import urllib.error from functools import wraps -from setuptools.extern import six -from setuptools.extern.six.moves import urllib, http_client, configparser, map - import setuptools from pkg_resources import ( - CHECKOUT_DIST, Distribution, BINARY_DIST, normalize_path, SOURCE_DIST, - Environment, find_distributions, safe_name, safe_version, - to_filename, Requirement, DEVELOP_DIST, EGG_DIST, + CHECKOUT_DIST, + Distribution, + BINARY_DIST, + normalize_path, + SOURCE_DIST, + Environment, + find_distributions, + safe_name, + safe_version, + to_filename, + Requirement, + DEVELOP_DIST, + EGG_DIST, + parse_version, ) -from setuptools import ssl_support from distutils import log from distutils.errors import DistutilsError from fnmatch import translate -from setuptools.py27compat import get_all_headers -from setuptools.py33compat import unescape from setuptools.wheel import Wheel +from setuptools.extern.more_itertools import unique_everseen -__metaclass__ = type EGG_FRAGMENT = re.compile(r'^egg=([-A-Za-z0-9_.+!]+)$') HREF = re.compile(r"""href\s*=\s*['"]?([^'"> ]+)""", re.I) @@ -39,23 +51,27 @@ URL_SCHEME = re.compile('([-+.a-z0-9]{2,}):', re.I).match EXTENSIONS = ".tar.gz .tar.bz2 .tar .zip .tgz".split() __all__ = [ - 'PackageIndex', 'distros_for_url', 'parse_bdist_wininst', + 'PackageIndex', + 'distros_for_url', + 'parse_bdist_wininst', 'interpret_distro_name', ] _SOCKET_TIMEOUT = 15 _tmpl = "setuptools/{setuptools.__version__} Python-urllib/{py_major}" -user_agent = _tmpl.format(py_major='{}.{}'.format(*sys.version_info), setuptools=setuptools) +user_agent = _tmpl.format( + py_major='{}.{}'.format(*sys.version_info), setuptools=setuptools +) def parse_requirement_arg(spec): try: return Requirement.parse(spec) - except ValueError: + except ValueError as e: raise DistutilsError( "Not a URL, existing file, or requirement spec: %r" % (spec,) - ) + ) from e def parse_bdist_wininst(name): @@ -96,15 +112,13 @@ def egg_info_for_url(url): def distros_for_url(url, metadata=None): """Yield egg or source distribution objects that might be found at a URL""" base, fragment = egg_info_for_url(url) - for dist in distros_for_location(url, base, metadata): - yield dist + yield from distros_for_location(url, base, metadata) if fragment: match = EGG_FRAGMENT.match(fragment) if match: - for dist in interpret_distro_name( + yield from interpret_distro_name( url, match.group(1), metadata, precedence=CHECKOUT_DIST - ): - yield dist + ) def distros_for_location(location, basename, metadata=None): @@ -118,13 +132,15 @@ def distros_for_location(location, basename, metadata=None): wheel = Wheel(basename) if not wheel.is_compatible(): return [] - return [Distribution( - location=location, - project_name=wheel.project_name, - version=wheel.version, - # Increase priority over eggs. - precedence=EGG_DIST + 1, - )] + return [ + Distribution( + location=location, + project_name=wheel.project_name, + version=wheel.version, + # Increase priority over eggs. + precedence=EGG_DIST + 1, + ) + ] if basename.endswith('.exe'): win_base, py_ver, platform = parse_bdist_wininst(basename) if win_base is not None: @@ -135,7 +151,7 @@ def distros_for_location(location, basename, metadata=None): # for ext in EXTENSIONS: if basename.endswith(ext): - basename = basename[:-len(ext)] + basename = basename[: -len(ext)] return interpret_distro_name(location, basename, metadata) return [] # no extension matched @@ -148,57 +164,37 @@ def distros_for_filename(filename, metadata=None): def interpret_distro_name( - location, basename, metadata, py_version=None, precedence=SOURCE_DIST, - platform=None + location, basename, metadata, py_version=None, precedence=SOURCE_DIST, platform=None ): - """Generate alternative interpretations of a source distro name + """Generate the interpretation of a source distro name Note: if `location` is a filesystem filename, you should call ``pkg_resources.normalize_path()`` on it before passing it to this routine! """ - # Generate alternative interpretations of a source distro name - # Because some packages are ambiguous as to name/versions split - # e.g. "adns-python-1.1.0", "egenix-mx-commercial", etc. - # So, we generate each possible interepretation (e.g. "adns, python-1.1.0" - # "adns-python, 1.1.0", and "adns-python-1.1.0, no version"). In practice, - # the spurious interpretations should be ignored, because in the event - # there's also an "adns" package, the spurious "python-1.1.0" version will - # compare lower than any numeric version number, and is therefore unlikely - # to match a request for it. It's still a potential problem, though, and - # in the long run PyPI and the distutils should go for "safe" names and - # versions in distribution archive names (sdist and bdist). parts = basename.split('-') if not py_version and any(re.match(r'py\d\.\d$', p) for p in parts[2:]): # it is a bdist_dumb, not an sdist -- bail out return - for p in range(1, len(parts) + 1): - yield Distribution( - location, metadata, '-'.join(parts[:p]), '-'.join(parts[p:]), - py_version=py_version, precedence=precedence, - platform=platform - ) - - -# From Python 2.7 docs -def unique_everseen(iterable, key=None): - "List unique elements, preserving order. Remember all elements ever seen." - # unique_everseen('AAAABBBCCDAABBB') --> A B C D - # unique_everseen('ABBCcAD', str.lower) --> A B C D - seen = set() - seen_add = seen.add - if key is None: - for element in six.moves.filterfalse(seen.__contains__, iterable): - seen_add(element) - yield element + # find the pivot (p) that splits the name from the version. + # infer the version as the first item that has a digit. + for p in range(len(parts)): + if parts[p][:1].isdigit(): + break else: - for element in iterable: - k = key(element) - if k not in seen: - seen_add(k) - yield element + p = len(parts) + + yield Distribution( + location, + metadata, + '-'.join(parts[:p]), + '-'.join(parts[p:]), + py_version=py_version, + precedence=precedence, + platform=platform, + ) def unique_values(func): @@ -214,8 +210,10 @@ def unique_values(func): return wrapper -REL = re.compile(r"""<([^>]*\srel\s*=\s*['"]?([^'">]+)[^>]*)>""", re.I) -# this line is here to fix emacs' cruddy broken syntax highlighting +REL = re.compile(r"""<([^>]*\srel\s{0,10}=\s{0,10}['"]?([^'" >]+)[^>]*)>""", re.I) +""" +Regex for an HTML tag with 'rel="val"' attributes. +""" @unique_values @@ -299,27 +297,33 @@ class PackageIndex(Environment): """A distribution index that scans web pages for download URLs""" def __init__( - self, index_url="https://pypi.org/simple/", hosts=('*',), - ca_bundle=None, verify_ssl=True, *args, **kw + self, + index_url="https://pypi.org/simple/", + hosts=('*',), + ca_bundle=None, + verify_ssl=True, + *args, + **kw, ): - Environment.__init__(self, *args, **kw) - self.index_url = index_url + "/" [:not index_url.endswith('/')] + super().__init__(*args, **kw) + self.index_url = index_url + "/"[: not index_url.endswith('/')] self.scanned_urls = {} self.fetched_urls = {} self.package_pages = {} self.allows = re.compile('|'.join(map(translate, hosts))).match self.to_scan = [] - use_ssl = ( - verify_ssl - and ssl_support.is_available - and (ca_bundle or ssl_support.find_ca_bundle()) - ) - if use_ssl: - self.opener = ssl_support.opener_for(ca_bundle) - else: - self.opener = urllib.request.urlopen + self.opener = urllib.request.urlopen - def process_url(self, url, retrieve=False): + def add(self, dist): + # ignore invalid versions + try: + parse_version(dist.version) + except Exception: + return None + return super().add(dist) + + # FIXME: 'PackageIndex.process_url' is too complex (14) + def process_url(self, url, retrieve=False): # noqa: C901 """Evaluate a URL as a possible download, and maybe retrieve it""" if url in self.scanned_urls and not retrieve: return @@ -348,6 +352,8 @@ class PackageIndex(Environment): f = self.open_url(url, tmpl % url) if f is None: return + if isinstance(f, urllib.error.HTTPError) and f.code == 401: + self.info("Authentication error: %s" % f.msg) self.fetched_urls[f.url] = True if 'html' not in f.headers.get('content-type', '').lower(): f.close() # not html, we can't process it @@ -393,11 +399,14 @@ class PackageIndex(Environment): return True msg = ( "\nNote: Bypassing %s (disallowed host; see " - "http://bit.ly/2hrImnY for details).\n") + "https://setuptools.pypa.io/en/latest/deprecated/" + "easy_install.html#restricting-downloads-with-allow-hosts for details).\n" + ) if fatal: raise DistutilsError(msg % url) else: self.warn(msg, url) + return False def scan_egg_links(self, search_path): dirs = filter(os.path.isdir, search_path) @@ -425,62 +434,63 @@ class PackageIndex(Environment): dist.precedence = SOURCE_DIST self.add(dist) + def _scan(self, link): + # Process a URL to see if it's for a package page + NO_MATCH_SENTINEL = None, None + if not link.startswith(self.index_url): + return NO_MATCH_SENTINEL + + parts = list(map(urllib.parse.unquote, link[len(self.index_url) :].split('/'))) + if len(parts) != 2 or '#' in parts[1]: + return NO_MATCH_SENTINEL + + # it's a package page, sanitize and index it + pkg = safe_name(parts[0]) + ver = safe_version(parts[1]) + self.package_pages.setdefault(pkg.lower(), {})[link] = True + return to_filename(pkg), to_filename(ver) + def process_index(self, url, page): """Process the contents of a PyPI page""" - def scan(link): - # Process a URL to see if it's for a package page - if link.startswith(self.index_url): - parts = list(map( - urllib.parse.unquote, link[len(self.index_url):].split('/') - )) - if len(parts) == 2 and '#' not in parts[1]: - # it's a package page, sanitize and index it - pkg = safe_name(parts[0]) - ver = safe_version(parts[1]) - self.package_pages.setdefault(pkg.lower(), {})[link] = True - return to_filename(pkg), to_filename(ver) - return None, None - # process an index page into the package-page index for match in HREF.finditer(page): try: - scan(urllib.parse.urljoin(url, htmldecode(match.group(1)))) + self._scan(urllib.parse.urljoin(url, htmldecode(match.group(1)))) except ValueError: pass - pkg, ver = scan(url) # ensure this page is in the page index - if pkg: - # process individual package page - for new_url in find_external_links(url, page): - # Process the found URL - base, frag = egg_info_for_url(new_url) - if base.endswith('.py') and not frag: - if ver: - new_url += '#egg=%s-%s' % (pkg, ver) - else: - self.need_version_info(url) - self.scan_url(new_url) - - return PYPI_MD5.sub( - lambda m: '%s' % m.group(1, 3, 2), page - ) - else: + pkg, ver = self._scan(url) # ensure this page is in the page index + if not pkg: return "" # no sense double-scanning non-package pages + # process individual package page + for new_url in find_external_links(url, page): + # Process the found URL + base, frag = egg_info_for_url(new_url) + if base.endswith('.py') and not frag: + if ver: + new_url += '#egg=%s-%s' % (pkg, ver) + else: + self.need_version_info(url) + self.scan_url(new_url) + + return PYPI_MD5.sub( + lambda m: '%s' % m.group(1, 3, 2), page + ) + def need_version_info(self, url): self.scan_all( "Page at %s links to .py file(s) without version info; an index " - "scan is required.", url + "scan is required.", + url, ) def scan_all(self, msg=None, *args): if self.index_url not in self.fetched_urls: if msg: self.warn(msg, *args) - self.info( - "Scanning index of all packages (this may take a while)" - ) + self.info("Scanning index of all packages (this may take a while)") self.scan_url(self.index_url) def find_packages(self, requirement): @@ -505,15 +515,13 @@ class PackageIndex(Environment): if dist in requirement: return dist self.debug("%s does not match %s", requirement, dist) - return super(PackageIndex, self).obtain(requirement, installer) + return super().obtain(requirement, installer) def check_hash(self, checker, filename, tfp): """ checker is a ContentChecker """ - checker.report( - self.debug, - "Validating %%s checksum for %s" % filename) + checker.report(self.debug, "Validating %%s checksum for %s" % filename) if not checker.is_valid(): tfp.close() os.unlink(filename) @@ -550,7 +558,8 @@ class PackageIndex(Environment): else: # no distros seen for this name, might be misspelled meth, msg = ( self.warn, - "Couldn't find index page for %r (maybe misspelled?)") + "Couldn't find index page for %r (maybe misspelled?)", + ) meth(msg, requirement.unsafe_name) self.scan_all() @@ -588,9 +597,15 @@ class PackageIndex(Environment): spec = parse_requirement_arg(spec) return getattr(self.fetch_distribution(spec, tmpdir), 'location', None) - def fetch_distribution( - self, requirement, tmpdir, force_scan=False, source=False, - develop_ok=False, local_index=None): + def fetch_distribution( # noqa: C901 # is too complex (14) # FIXME + self, + requirement, + tmpdir, + force_scan=False, + source=False, + develop_ok=False, + local_index=None, + ): """Obtain a distribution suitable for fulfilling `requirement` `requirement` must be a ``pkg_resources.Requirement`` instance. @@ -618,25 +633,24 @@ class PackageIndex(Environment): # Find a matching distribution; may be called more than once for dist in env[req.key]: - if dist.precedence == DEVELOP_DIST and not develop_ok: if dist not in skipped: self.warn( - "Skipping development or system egg: %s", dist, + "Skipping development or system egg: %s", + dist, ) skipped[dist] = 1 continue - test = ( - dist in req - and (dist.precedence <= SOURCE_DIST or not source) - ) + test = dist in req and (dist.precedence <= SOURCE_DIST or not source) if test: loc = self.download(dist.location, tmpdir) dist.download_location = loc if os.path.exists(dist.download_location): return dist + return None + if force_scan: self.prescan() self.find_packages(requirement) @@ -660,6 +674,7 @@ class PackageIndex(Environment): (source and "a source distribution of " or ""), requirement, ) + return None else: self.info("Best match: %s", dist) return dist.clone(location=dist.download_location) @@ -679,10 +694,15 @@ class PackageIndex(Environment): def gen_setup(self, filename, fragment, tmpdir): match = EGG_FRAGMENT.match(fragment) - dists = match and [ - d for d in - interpret_distro_name(filename, match.group(1), None) if d.version - ] or [] + dists = ( + match + and [ + d + for d in interpret_distro_name(filename, match.group(1), None) + if d.version + ] + or [] + ) if len(dists) == 1: # unambiguous ``#egg`` fragment basename = os.path.basename(filename) @@ -690,8 +710,7 @@ class PackageIndex(Environment): # Make sure the file has been downloaded to the temp dir. if os.path.dirname(filename) != tmpdir: dst = os.path.join(tmpdir, basename) - from setuptools.command.easy_install import samefile - if not samefile(filename, dst): + if not (os.path.exists(dst) and os.path.samefile(filename, dst)): shutil.copy2(filename, dst) filename = dst @@ -700,8 +719,9 @@ class PackageIndex(Environment): "from setuptools import setup\n" "setup(name=%r, version=%r, py_modules=[%r])\n" % ( - dists[0].project_name, dists[0].version, - os.path.splitext(basename)[0] + dists[0].project_name, + dists[0].version, + os.path.splitext(basename)[0], ) ) return filename @@ -737,7 +757,7 @@ class PackageIndex(Environment): size = -1 if "content-length" in headers: # Some servers return multiple Content-Length headers :( - sizes = get_all_headers(headers, 'Content-Length') + sizes = headers.get_all('Content-Length') size = max(map(int, sizes)) self.reporthook(url, filename, blocknum, bs, size) with open(filename, 'wb') as tfp: @@ -759,40 +779,40 @@ class PackageIndex(Environment): def reporthook(self, url, filename, blocknum, blksize, size): pass # no-op - def open_url(self, url, warning=None): + # FIXME: + def open_url(self, url, warning=None): # noqa: C901 # is too complex (12) if url.startswith('file:'): return local_open(url) try: return open_with_auth(url, self.opener) - except (ValueError, http_client.InvalidURL) as v: + except (ValueError, http.client.InvalidURL) as v: msg = ' '.join([str(arg) for arg in v.args]) if warning: self.warn(warning, msg) else: - raise DistutilsError('%s %s' % (url, msg)) + raise DistutilsError('%s %s' % (url, msg)) from v except urllib.error.HTTPError as v: return v except urllib.error.URLError as v: if warning: self.warn(warning, v.reason) else: - raise DistutilsError("Download error for %s: %s" - % (url, v.reason)) - except http_client.BadStatusLine as v: + raise DistutilsError( + "Download error for %s: %s" % (url, v.reason) + ) from v + except http.client.BadStatusLine as v: if warning: self.warn(warning, v.line) else: raise DistutilsError( '%s returned a bad status line. The server might be ' - 'down, %s' % - (url, v.line) - ) - except (http_client.HTTPException, socket.error) as v: + 'down, %s' % (url, v.line) + ) from v + except (http.client.HTTPException, OSError) as v: if warning: self.warn(warning, v) else: - raise DistutilsError("Download error for %s: %s" - % (url, v)) + raise DistutilsError("Download error for %s: %s" % (url, v)) from v def _download_url(self, scheme, url, tmpdir): # Determine download filename @@ -829,46 +849,16 @@ class PackageIndex(Environment): def _attempt_download(self, url, filename): headers = self._download_to(url, filename) if 'html' in headers.get('content-type', '').lower(): - return self._download_html(url, headers, filename) + return self._invalid_download_html(url, headers, filename) else: return filename - def _download_html(self, url, headers, filename): - file = open(filename) - for line in file: - if line.strip(): - # Check for a subversion index page - if re.search(r'([^- ]+ - )?Revision \d+:', line): - # it's a subversion index page: - file.close() - os.unlink(filename) - return self._download_svn(url, filename) - break # not an index page - file.close() + def _invalid_download_html(self, url, headers, filename): os.unlink(filename) - raise DistutilsError("Unexpected HTML page found at " + url) + raise DistutilsError(f"Unexpected HTML page found at {url}") - def _download_svn(self, url, filename): - warnings.warn("SVN download support is deprecated", UserWarning) - url = url.split('#', 1)[0] # remove any fragment for svn's sake - creds = '' - if url.lower().startswith('svn:') and '@' in url: - scheme, netloc, path, p, q, f = urllib.parse.urlparse(url) - if not netloc and path.startswith('//') and '/' in path[2:]: - netloc, path = path[2:].split('/', 1) - auth, host = _splituser(netloc) - if auth: - if ':' in auth: - user, pw = auth.split(':', 1) - creds = " --username=%s --password=%s" % (user, pw) - else: - creds = " --username=" + auth - netloc = host - parts = scheme, netloc, url, p, q, f - url = urllib.parse.urlunparse(parts) - self.info("Doing subversion checkout from %s to %s", url, filename) - os.system("svn checkout%s -q %s %s" % (creds, url, filename)) - return filename + def _download_svn(self, url, _filename): + raise DistutilsError(f"Invalid config, SVN download is not supported: {url}") @staticmethod def _vcs_split_rev_from_url(url, pop_prefix=False): @@ -897,10 +887,13 @@ class PackageIndex(Environment): if rev is not None: self.info("Checking out %s", rev) - os.system("git -C %s checkout --quiet %s" % ( - filename, - rev, - )) + os.system( + "git -C %s checkout --quiet %s" + % ( + filename, + rev, + ) + ) return filename @@ -913,10 +906,13 @@ class PackageIndex(Environment): if rev is not None: self.info("Updating to %s", rev) - os.system("hg --cwd %s up -C -r %s -q" % ( - filename, - rev, - )) + os.system( + "hg --cwd %s up -C -r %s -q" + % ( + filename, + rev, + ) + ) return filename @@ -937,7 +933,7 @@ entity_sub = re.compile(r'&(#(\d+|x[\da-fA-F]+)|[\w.:-]+);?').sub def decode_entity(match): what = match.group(0) - return unescape(what) + return html.unescape(what) def htmldecode(text): @@ -969,8 +965,7 @@ def socket_timeout(timeout=15): def _encode_auth(auth): """ - A function compatible with Python 2.3-3.3 that will encode - auth from a URL suitable for an HTTP header. + Encode auth from a URL suitable for an HTTP header. >>> str(_encode_auth('username%3Apassword')) 'dXNlcm5hbWU6cGFzc3dvcmQ=' @@ -1012,7 +1007,7 @@ class PyPIConfig(configparser.RawConfigParser): Load from ~/.pypirc """ defaults = dict.fromkeys(['username', 'password', 'repository'], '') - configparser.RawConfigParser.__init__(self, defaults) + super().__init__(defaults) rc = os.path.join(os.path.expanduser('~'), '.pypirc') if os.path.exists(rc): @@ -1021,7 +1016,8 @@ class PyPIConfig(configparser.RawConfigParser): @property def creds_by_repository(self): sections_with_repositories = [ - section for section in self.sections() + section + for section in self.sections() if self.get(section, 'repository').strip() ] @@ -1042,6 +1038,7 @@ class PyPIConfig(configparser.RawConfigParser): for repository, cred in self.creds_by_repository.items(): if url.startswith(repository): return cred + return None def open_with_auth(url, opener=urllib.request.urlopen): @@ -1050,10 +1047,10 @@ def open_with_auth(url, opener=urllib.request.urlopen): parsed = urllib.parse.urlparse(url) scheme, netloc, path, params, query, frag = parsed - # Double scheme does not raise on Mac OS X as revealed by a + # Double scheme does not raise on macOS as revealed by a # failing test. We would expect "nonnumeric port". Refs #20. if netloc.endswith(':'): - raise http_client.InvalidURL("nonnumeric port: ''") + raise http.client.InvalidURL("nonnumeric port: ''") if scheme in ('http', 'https'): auth, address = _splituser(netloc) @@ -1092,7 +1089,8 @@ def open_with_auth(url, opener=urllib.request.urlopen): # copy of urllib.parse._splituser from Python 3.8 def _splituser(host): - """splituser('user[:passwd]@host[:port]') --> 'user[:passwd]', 'host[:port]'.""" + """splituser('user[:passwd]@host[:port]') + --> 'user[:passwd]', 'host[:port]'.""" user, delim, host = host.rpartition('@') return (user if delim else None), host @@ -1124,13 +1122,13 @@ def local_open(url): files.append('<a href="{name}">{name}</a>'.format(name=f)) else: tmpl = ( - "<html><head><title>{url}" - "{files}") + "{url}" "{files}" + ) body = tmpl.format(url=url, files='\n'.join(files)) status, message = 200, "OK" else: status, message, body = 404, "Path not found", "Not found" headers = {'content-type': 'text/html'} - body_stream = six.StringIO(body) + body_stream = io.StringIO(body) return urllib.error.HTTPError(url, status, message, headers, body_stream) diff --git a/libs/common/setuptools/py27compat.py b/libs/common/setuptools/py27compat.py deleted file mode 100644 index 1d57360f..00000000 --- a/libs/common/setuptools/py27compat.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -Compatibility Support for Python 2.7 and earlier -""" - -import sys -import platform - -from setuptools.extern import six - - -def get_all_headers(message, key): - """ - Given an HTTPMessage, return all headers matching a given key. - """ - return message.get_all(key) - - -if six.PY2: - def get_all_headers(message, key): - return message.getheaders(key) - - -linux_py2_ascii = ( - platform.system() == 'Linux' and - six.PY2 -) - -rmtree_safe = str if linux_py2_ascii else lambda x: x -"""Workaround for http://bugs.python.org/issue24672""" - - -try: - from ._imp import find_module, PY_COMPILED, PY_FROZEN, PY_SOURCE - from ._imp import get_frozen_object, get_module -except ImportError: - import imp - from imp import PY_COMPILED, PY_FROZEN, PY_SOURCE # noqa - - def find_module(module, paths=None): - """Just like 'imp.find_module()', but with package support""" - parts = module.split('.') - while parts: - part = parts.pop(0) - f, path, (suffix, mode, kind) = info = imp.find_module(part, paths) - - if kind == imp.PKG_DIRECTORY: - parts = parts or ['__init__'] - paths = [path] - - elif parts: - raise ImportError("Can't find %r in %s" % (parts, module)) - - return info - - def get_frozen_object(module, paths): - return imp.get_frozen_object(module) - - def get_module(module, paths, info): - imp.load_module(module, *info) - return sys.modules[module] diff --git a/libs/common/setuptools/py31compat.py b/libs/common/setuptools/py31compat.py deleted file mode 100644 index e1da7ee2..00000000 --- a/libs/common/setuptools/py31compat.py +++ /dev/null @@ -1,32 +0,0 @@ -__all__ = [] - -__metaclass__ = type - - -try: - # Python >=3.2 - from tempfile import TemporaryDirectory -except ImportError: - import shutil - import tempfile - - class TemporaryDirectory: - """ - Very simple temporary directory context manager. - Will try to delete afterward, but will also ignore OS and similar - errors on deletion. - """ - - def __init__(self, **kwargs): - self.name = None # Handle mkdtemp raising an exception - self.name = tempfile.mkdtemp(**kwargs) - - def __enter__(self): - return self.name - - def __exit__(self, exctype, excvalue, exctrace): - try: - shutil.rmtree(self.name, True) - except OSError: # removal errors are not the only possible - pass - self.name = None diff --git a/libs/common/setuptools/py33compat.py b/libs/common/setuptools/py33compat.py deleted file mode 100644 index cb694436..00000000 --- a/libs/common/setuptools/py33compat.py +++ /dev/null @@ -1,59 +0,0 @@ -import dis -import array -import collections - -try: - import html -except ImportError: - html = None - -from setuptools.extern import six -from setuptools.extern.six.moves import html_parser - -__metaclass__ = type - -OpArg = collections.namedtuple('OpArg', 'opcode arg') - - -class Bytecode_compat: - def __init__(self, code): - self.code = code - - def __iter__(self): - """Yield '(op,arg)' pair for each operation in code object 'code'""" - - bytes = array.array('b', self.code.co_code) - eof = len(self.code.co_code) - - ptr = 0 - extended_arg = 0 - - while ptr < eof: - - op = bytes[ptr] - - if op >= dis.HAVE_ARGUMENT: - - arg = bytes[ptr + 1] + bytes[ptr + 2] * 256 + extended_arg - ptr += 3 - - if op == dis.EXTENDED_ARG: - long_type = six.integer_types[-1] - extended_arg = arg * long_type(65536) - continue - - else: - arg = None - ptr += 1 - - yield OpArg(op, arg) - - -Bytecode = getattr(dis, 'Bytecode', Bytecode_compat) - - -unescape = getattr(html, 'unescape', None) -if unescape is None: - # HTMLParser.unescape is deprecated since Python 3.4, and will be removed - # from 3.9. - unescape = html_parser.HTMLParser().unescape diff --git a/libs/common/setuptools/py34compat.py b/libs/common/setuptools/py34compat.py deleted file mode 100644 index 3ad91722..00000000 --- a/libs/common/setuptools/py34compat.py +++ /dev/null @@ -1,13 +0,0 @@ -import importlib - -try: - import importlib.util -except ImportError: - pass - - -try: - module_from_spec = importlib.util.module_from_spec -except AttributeError: - def module_from_spec(spec): - return spec.loader.load_module(spec.name) diff --git a/libs/common/setuptools/sandbox.py b/libs/common/setuptools/sandbox.py index 685f3f72..7634b132 100644 --- a/libs/common/setuptools/sandbox.py +++ b/libs/common/setuptools/sandbox.py @@ -8,11 +8,11 @@ import re import contextlib import pickle import textwrap +import builtins -from setuptools.extern import six -from setuptools.extern.six.moves import builtins, map - -import pkg_resources.py31compat +import pkg_resources +from distutils.errors import DistutilsError +from pkg_resources import working_set if sys.platform.startswith('java'): import org.python.modules.posix.PosixModule as _os @@ -23,12 +23,13 @@ try: except NameError: _file = None _open = open -from distutils.errors import DistutilsError -from pkg_resources import working_set __all__ = [ - "AbstractSandbox", "DirectorySandbox", "SandboxViolation", "run_setup", + "AbstractSandbox", + "DirectorySandbox", + "SandboxViolation", + "run_setup", ] @@ -70,7 +71,7 @@ def override_temp(replacement): """ Monkey-patch tempfile.tempdir with replacement, ensuring it exists """ - pkg_resources.py31compat.makedirs(replacement, exist_ok=True) + os.makedirs(replacement, exist_ok=True) saved = tempfile.tempdir @@ -108,12 +109,13 @@ class UnpickleableException(Exception): except Exception: # get UnpickleableException inside the sandbox from setuptools.sandbox import UnpickleableException as cls + return cls.dump(cls, cls(repr(exc))) class ExceptionSaver: """ - A Context Manager that will save an exception, serialized, and restore it + A Context Manager that will save an exception, serialize, and restore it later. """ @@ -122,7 +124,7 @@ class ExceptionSaver: def __exit__(self, type, exc, tb): if not exc: - return + return False # dump the exception self._saved = UnpickleableException.dump(type, exc) @@ -138,7 +140,7 @@ class ExceptionSaver: return type, exc = map(pickle.loads, self._saved) - six.reraise(type, exc, self._tb) + raise exc.with_traceback(self._tb) @contextlib.contextmanager @@ -156,7 +158,8 @@ def save_modules(): sys.modules.update(saved) # remove any modules imported since del_modules = ( - mod_name for mod_name in sys.modules + mod_name + for mod_name in sys.modules if mod_name not in saved # exclude any encodings modules. See #285 and not mod_name.startswith('encodings.') @@ -185,8 +188,8 @@ def setup_context(setup_dir): temp_dir = os.path.join(setup_dir, 'temp') with save_pkg_resources_state(): with save_modules(): - hide_setuptools() with save_path(): + hide_setuptools() with save_argv(): with override_temp(temp_dir): with pushd(setup_dir): @@ -195,6 +198,15 @@ def setup_context(setup_dir): yield +_MODULES_TO_HIDE = { + 'setuptools', + 'distutils', + 'pkg_resources', + 'Cython', + '_distutils_hack', +} + + def _needs_hiding(mod_name): """ >>> _needs_hiding('setuptools') @@ -212,8 +224,8 @@ def _needs_hiding(mod_name): >>> _needs_hiding('Cython') True """ - pattern = re.compile(r'(setuptools|pkg_resources|distutils|Cython)(\.|$)') - return bool(pattern.match(mod_name)) + base_module = mod_name.split('.', 1)[0] + return base_module in _MODULES_TO_HIDE def hide_setuptools(): @@ -223,6 +235,10 @@ def hide_setuptools(): necessary to avoid issues such as #315 where setuptools upgrading itself would fail to find a function declared in the metadata. """ + _distutils_hack = sys.modules.get('_distutils_hack', None) + if _distutils_hack is not None: + _distutils_hack._remove_shim() + modules = filter(_needs_hiding, sys.modules) _clear_modules(modules) @@ -238,15 +254,8 @@ def run_setup(setup_script, args): working_set.__init__() working_set.callbacks.append(lambda dist: dist.activate()) - # __file__ should be a byte string on Python 2 (#712) - dunder_file = ( - setup_script - if isinstance(setup_script, str) else - setup_script.encode(sys.getfilesystemencoding()) - ) - with DirectorySandbox(setup_dir): - ns = dict(__file__=dunder_file, __name__='__main__') + ns = dict(__file__=setup_script, __name__='__main__') _execfile(setup_script, ns) except SystemExit as v: if v.args and v.args[0]: @@ -261,7 +270,8 @@ class AbstractSandbox: def __init__(self): self._attrs = [ - name for name in dir(_os) + name + for name in dir(_os) if not name.startswith('_') and hasattr(self, name) ] @@ -316,9 +326,25 @@ class AbstractSandbox: _file = _mk_single_path_wrapper('file', _file) _open = _mk_single_path_wrapper('open', _open) for name in [ - "stat", "listdir", "chdir", "open", "chmod", "chown", "mkdir", - "remove", "unlink", "rmdir", "utime", "lchown", "chroot", "lstat", - "startfile", "mkfifo", "mknod", "pathconf", "access" + "stat", + "listdir", + "chdir", + "open", + "chmod", + "chown", + "mkdir", + "remove", + "unlink", + "rmdir", + "utime", + "lchown", + "chroot", + "lstat", + "startfile", + "mkfifo", + "mknod", + "pathconf", + "access", ]: if hasattr(_os, name): locals()[name] = _mk_single_path_wrapper(name) @@ -369,12 +395,12 @@ class AbstractSandbox: """Called for path pairs like rename, link, and symlink operations""" return ( self._remap_input(operation + '-from', src, *args, **kw), - self._remap_input(operation + '-to', dst, *args, **kw) + self._remap_input(operation + '-to', dst, *args, **kw), ) if hasattr(os, 'devnull'): - _EXCEPTIONS = [os.devnull,] + _EXCEPTIONS = [os.devnull] else: _EXCEPTIONS = [] @@ -383,27 +409,35 @@ class DirectorySandbox(AbstractSandbox): """Restrict operations to a single subdirectory - pseudo-chroot""" write_ops = dict.fromkeys([ - "open", "chmod", "chown", "mkdir", "remove", "unlink", "rmdir", - "utime", "lchown", "chroot", "mkfifo", "mknod", "tempnam", + "open", + "chmod", + "chown", + "mkdir", + "remove", + "unlink", + "rmdir", + "utime", + "lchown", + "chroot", + "mkfifo", + "mknod", + "tempnam", ]) - _exception_patterns = [ - # Allow lib2to3 to attempt to save a pickled grammar object (#121) - r'.*lib2to3.*\.pickle$', - ] + _exception_patterns = [] "exempt writing to paths that match the pattern" def __init__(self, sandbox, exceptions=_EXCEPTIONS): self._sandbox = os.path.normcase(os.path.realpath(sandbox)) self._prefix = os.path.join(self._sandbox, '') self._exceptions = [ - os.path.normcase(os.path.realpath(path)) - for path in exceptions + os.path.normcase(os.path.realpath(path)) for path in exceptions ] AbstractSandbox.__init__(self) def _violation(self, operation, *args, **kw): from setuptools.sandbox import SandboxViolation + raise SandboxViolation(operation, args, kw) if _file: @@ -436,12 +470,10 @@ class DirectorySandbox(AbstractSandbox): def _exempted(self, filepath): start_matches = ( - filepath.startswith(exception) - for exception in self._exceptions + filepath.startswith(exception) for exception in self._exceptions ) pattern_matches = ( - re.match(pattern, filepath) - for pattern in self._exception_patterns + re.match(pattern, filepath) for pattern in self._exception_patterns ) candidates = itertools.chain(start_matches, pattern_matches) return any(candidates) @@ -466,15 +498,19 @@ class DirectorySandbox(AbstractSandbox): WRITE_FLAGS = functools.reduce( - operator.or_, [getattr(_os, a, 0) for a in - "O_WRONLY O_RDWR O_APPEND O_CREAT O_TRUNC O_TEMPORARY".split()] + operator.or_, + [ + getattr(_os, a, 0) + for a in "O_WRONLY O_RDWR O_APPEND O_CREAT O_TRUNC O_TEMPORARY".split() + ], ) class SandboxViolation(DistutilsError): """A setup script attempted to modify the filesystem outside the sandbox""" - tmpl = textwrap.dedent(""" + tmpl = textwrap.dedent( + """ SandboxViolation: {cmd}{args!r} {kwargs} The package setup script has attempted to modify files on your system @@ -484,7 +520,8 @@ class SandboxViolation(DistutilsError): support alternate installation locations even if you run its setup script by hand. Please inform the package's author and the EasyInstall maintainers to find out if a fix or workaround is available. - """).lstrip() + """ + ).lstrip() def __str__(self): cmd, args, kwargs = self.args diff --git a/libs/common/setuptools/site-patch.py b/libs/common/setuptools/site-patch.py deleted file mode 100644 index 40b00de0..00000000 --- a/libs/common/setuptools/site-patch.py +++ /dev/null @@ -1,74 +0,0 @@ -def __boot(): - import sys - import os - PYTHONPATH = os.environ.get('PYTHONPATH') - if PYTHONPATH is None or (sys.platform == 'win32' and not PYTHONPATH): - PYTHONPATH = [] - else: - PYTHONPATH = PYTHONPATH.split(os.pathsep) - - pic = getattr(sys, 'path_importer_cache', {}) - stdpath = sys.path[len(PYTHONPATH):] - mydir = os.path.dirname(__file__) - - for item in stdpath: - if item == mydir or not item: - continue # skip if current dir. on Windows, or my own directory - importer = pic.get(item) - if importer is not None: - loader = importer.find_module('site') - if loader is not None: - # This should actually reload the current module - loader.load_module('site') - break - else: - try: - import imp # Avoid import loop in Python 3 - stream, path, descr = imp.find_module('site', [item]) - except ImportError: - continue - if stream is None: - continue - try: - # This should actually reload the current module - imp.load_module('site', stream, path, descr) - finally: - stream.close() - break - else: - raise ImportError("Couldn't find the real 'site' module") - - known_paths = dict([(makepath(item)[1], 1) for item in sys.path]) # 2.2 comp - - oldpos = getattr(sys, '__egginsert', 0) # save old insertion position - sys.__egginsert = 0 # and reset the current one - - for item in PYTHONPATH: - addsitedir(item) - - sys.__egginsert += oldpos # restore effective old position - - d, nd = makepath(stdpath[0]) - insert_at = None - new_path = [] - - for item in sys.path: - p, np = makepath(item) - - if np == nd and insert_at is None: - # We've hit the first 'system' path entry, so added entries go here - insert_at = len(new_path) - - if np in known_paths or insert_at is None: - new_path.append(item) - else: - # new path after the insert point, back-insert it - new_path.insert(insert_at, item) - insert_at += 1 - - sys.path[:] = new_path - - -if __name__ == 'site': - __boot() - del __boot diff --git a/libs/common/setuptools/ssl_support.py b/libs/common/setuptools/ssl_support.py deleted file mode 100644 index 226db694..00000000 --- a/libs/common/setuptools/ssl_support.py +++ /dev/null @@ -1,260 +0,0 @@ -import os -import socket -import atexit -import re -import functools - -from setuptools.extern.six.moves import urllib, http_client, map, filter - -from pkg_resources import ResolutionError, ExtractionError - -try: - import ssl -except ImportError: - ssl = None - -__all__ = [ - 'VerifyingHTTPSHandler', 'find_ca_bundle', 'is_available', 'cert_paths', - 'opener_for' -] - -cert_paths = """ -/etc/pki/tls/certs/ca-bundle.crt -/etc/ssl/certs/ca-certificates.crt -/usr/share/ssl/certs/ca-bundle.crt -/usr/local/share/certs/ca-root.crt -/etc/ssl/cert.pem -/System/Library/OpenSSL/certs/cert.pem -/usr/local/share/certs/ca-root-nss.crt -/etc/ssl/ca-bundle.pem -""".strip().split() - -try: - HTTPSHandler = urllib.request.HTTPSHandler - HTTPSConnection = http_client.HTTPSConnection -except AttributeError: - HTTPSHandler = HTTPSConnection = object - -is_available = ssl is not None and object not in (HTTPSHandler, HTTPSConnection) - - -try: - from ssl import CertificateError, match_hostname -except ImportError: - try: - from backports.ssl_match_hostname import CertificateError - from backports.ssl_match_hostname import match_hostname - except ImportError: - CertificateError = None - match_hostname = None - -if not CertificateError: - - class CertificateError(ValueError): - pass - - -if not match_hostname: - - def _dnsname_match(dn, hostname, max_wildcards=1): - """Matching according to RFC 6125, section 6.4.3 - - https://tools.ietf.org/html/rfc6125#section-6.4.3 - """ - pats = [] - if not dn: - return False - - # Ported from python3-syntax: - # leftmost, *remainder = dn.split(r'.') - parts = dn.split(r'.') - leftmost = parts[0] - remainder = parts[1:] - - wildcards = leftmost.count('*') - if wildcards > max_wildcards: - # Issue #17980: avoid denials of service by refusing more - # than one wildcard per fragment. A survey of established - # policy among SSL implementations showed it to be a - # reasonable choice. - raise CertificateError( - "too many wildcards in certificate DNS name: " + repr(dn)) - - # speed up common case w/o wildcards - if not wildcards: - return dn.lower() == hostname.lower() - - # RFC 6125, section 6.4.3, subitem 1. - # The client SHOULD NOT attempt to match a presented identifier in which - # the wildcard character comprises a label other than the left-most label. - if leftmost == '*': - # When '*' is a fragment by itself, it matches a non-empty dotless - # fragment. - pats.append('[^.]+') - elif leftmost.startswith('xn--') or hostname.startswith('xn--'): - # RFC 6125, section 6.4.3, subitem 3. - # The client SHOULD NOT attempt to match a presented identifier - # where the wildcard character is embedded within an A-label or - # U-label of an internationalized domain name. - pats.append(re.escape(leftmost)) - else: - # Otherwise, '*' matches any dotless string, e.g. www* - pats.append(re.escape(leftmost).replace(r'\*', '[^.]*')) - - # add the remaining fragments, ignore any wildcards - for frag in remainder: - pats.append(re.escape(frag)) - - pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE) - return pat.match(hostname) - - def match_hostname(cert, hostname): - """Verify that *cert* (in decoded format as returned by - SSLSocket.getpeercert()) matches the *hostname*. RFC 2818 and RFC 6125 - rules are followed, but IP addresses are not accepted for *hostname*. - - CertificateError is raised on failure. On success, the function - returns nothing. - """ - if not cert: - raise ValueError("empty or no certificate") - dnsnames = [] - san = cert.get('subjectAltName', ()) - for key, value in san: - if key == 'DNS': - if _dnsname_match(value, hostname): - return - dnsnames.append(value) - if not dnsnames: - # The subject is only checked when there is no dNSName entry - # in subjectAltName - for sub in cert.get('subject', ()): - for key, value in sub: - # XXX according to RFC 2818, the most specific Common Name - # must be used. - if key == 'commonName': - if _dnsname_match(value, hostname): - return - dnsnames.append(value) - if len(dnsnames) > 1: - raise CertificateError("hostname %r " - "doesn't match either of %s" - % (hostname, ', '.join(map(repr, dnsnames)))) - elif len(dnsnames) == 1: - raise CertificateError("hostname %r " - "doesn't match %r" - % (hostname, dnsnames[0])) - else: - raise CertificateError("no appropriate commonName or " - "subjectAltName fields were found") - - -class VerifyingHTTPSHandler(HTTPSHandler): - """Simple verifying handler: no auth, subclasses, timeouts, etc.""" - - def __init__(self, ca_bundle): - self.ca_bundle = ca_bundle - HTTPSHandler.__init__(self) - - def https_open(self, req): - return self.do_open( - lambda host, **kw: VerifyingHTTPSConn(host, self.ca_bundle, **kw), req - ) - - -class VerifyingHTTPSConn(HTTPSConnection): - """Simple verifying connection: no auth, subclasses, timeouts, etc.""" - - def __init__(self, host, ca_bundle, **kw): - HTTPSConnection.__init__(self, host, **kw) - self.ca_bundle = ca_bundle - - def connect(self): - sock = socket.create_connection( - (self.host, self.port), getattr(self, 'source_address', None) - ) - - # Handle the socket if a (proxy) tunnel is present - if hasattr(self, '_tunnel') and getattr(self, '_tunnel_host', None): - self.sock = sock - self._tunnel() - # http://bugs.python.org/issue7776: Python>=3.4.1 and >=2.7.7 - # change self.host to mean the proxy server host when tunneling is - # being used. Adapt, since we are interested in the destination - # host for the match_hostname() comparison. - actual_host = self._tunnel_host - else: - actual_host = self.host - - if hasattr(ssl, 'create_default_context'): - ctx = ssl.create_default_context(cafile=self.ca_bundle) - self.sock = ctx.wrap_socket(sock, server_hostname=actual_host) - else: - # This is for python < 2.7.9 and < 3.4? - self.sock = ssl.wrap_socket( - sock, cert_reqs=ssl.CERT_REQUIRED, ca_certs=self.ca_bundle - ) - try: - match_hostname(self.sock.getpeercert(), actual_host) - except CertificateError: - self.sock.shutdown(socket.SHUT_RDWR) - self.sock.close() - raise - - -def opener_for(ca_bundle=None): - """Get a urlopen() replacement that uses ca_bundle for verification""" - return urllib.request.build_opener( - VerifyingHTTPSHandler(ca_bundle or find_ca_bundle()) - ).open - - -# from jaraco.functools -def once(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - if not hasattr(func, 'always_returns'): - func.always_returns = func(*args, **kwargs) - return func.always_returns - return wrapper - - -@once -def get_win_certfile(): - try: - import wincertstore - except ImportError: - return None - - class CertFile(wincertstore.CertFile): - def __init__(self): - super(CertFile, self).__init__() - atexit.register(self.close) - - def close(self): - try: - super(CertFile, self).close() - except OSError: - pass - - _wincerts = CertFile() - _wincerts.addstore('CA') - _wincerts.addstore('ROOT') - return _wincerts.name - - -def find_ca_bundle(): - """Return an existing CA bundle path, or None""" - extant_cert_paths = filter(os.path.isfile, cert_paths) - return ( - get_win_certfile() - or next(extant_cert_paths, None) - or _certifi_where() - ) - - -def _certifi_where(): - try: - return __import__('certifi').where() - except (ImportError, ResolutionError, ExtractionError): - pass diff --git a/libs/common/setuptools/unicode_utils.py b/libs/common/setuptools/unicode_utils.py index 7c63efd2..d43dcc11 100644 --- a/libs/common/setuptools/unicode_utils.py +++ b/libs/common/setuptools/unicode_utils.py @@ -1,12 +1,10 @@ import unicodedata import sys -from setuptools.extern import six - # HFS Plus uses decomposed UTF-8 def decompose(path): - if isinstance(path, six.text_type): + if isinstance(path, str): return unicodedata.normalize('NFD', path) try: path = path.decode('utf-8') @@ -20,10 +18,10 @@ def decompose(path): def filesys_decode(path): """ Ensure that the given path is decoded, - NONE when no expected encoding works + ``None`` when no expected encoding works """ - if isinstance(path, six.text_type): + if isinstance(path, str): return path fs_enc = sys.getfilesystemencoding() or 'utf-8' @@ -35,6 +33,8 @@ def filesys_decode(path): except UnicodeDecodeError: continue + return None + def try_encode(string, enc): "turn unicode encoding into a functional routine" diff --git a/libs/common/setuptools/version.py b/libs/common/setuptools/version.py index 95e18696..ec253c41 100644 --- a/libs/common/setuptools/version.py +++ b/libs/common/setuptools/version.py @@ -1,6 +1,6 @@ -import pkg_resources +from ._importlib import metadata try: - __version__ = pkg_resources.get_distribution('setuptools').version + __version__ = metadata.version('setuptools') or '0.dev0+unknown' except Exception: - __version__ = 'unknown' + __version__ = '0.dev0+unknown' diff --git a/libs/common/setuptools/wheel.py b/libs/common/setuptools/wheel.py index 025aaa82..9861b5cf 100644 --- a/libs/common/setuptools/wheel.py +++ b/libs/common/setuptools/wheel.py @@ -1,42 +1,44 @@ """Wheels support.""" -from distutils.util import get_platform -from distutils import log import email import itertools +import functools import os import posixpath import re import zipfile +import contextlib + +from distutils.util import get_platform -import pkg_resources import setuptools -from pkg_resources import parse_version +from setuptools.extern.packaging.version import Version as parse_version from setuptools.extern.packaging.tags import sys_tags from setuptools.extern.packaging.utils import canonicalize_name -from setuptools.extern.six import PY3 -from setuptools.command.egg_info import write_requirements - - -__metaclass__ = type +from setuptools.command.egg_info import write_requirements, _egg_basename +from setuptools.archive_util import _unpack_zipfile_obj WHEEL_NAME = re.compile( r"""^(?P.+?)-(?P\d.*?) ((-(?P\d.*?))?-(?P.+?)-(?P.+?)-(?P.+?) )\.whl$""", - re.VERBOSE).match + re.VERBOSE, +).match -NAMESPACE_PACKAGE_INIT = '''\ -try: - __import__('pkg_resources').declare_namespace(__name__) -except ImportError: - __path__ = __import__('pkgutil').extend_path(__path__, __name__) -''' +NAMESPACE_PACKAGE_INIT = "__import__('pkg_resources').declare_namespace(__name__)\n" + + +@functools.lru_cache(maxsize=None) +def _get_supported_tags(): + # We calculate the supported tags only once, otherwise calling + # this method on thousands of wheels takes seconds instead of + # milliseconds. + return {(t.interpreter, t.abi, t.platform) for t in sys_tags()} def unpack(src_dir, dst_dir): - '''Move everything under `src_dir` to `dst_dir`, and delete the former.''' + """Move everything under `src_dir` to `dst_dir`, and delete the former.""" for dirpath, dirnames, filenames in os.walk(src_dir): subdir = os.path.relpath(dirpath, src_dir) for f in filenames: @@ -57,8 +59,21 @@ def unpack(src_dir, dst_dir): os.rmdir(dirpath) -class Wheel: +@contextlib.contextmanager +def disable_info_traces(): + """ + Temporarily disable info traces. + """ + from distutils import log + saved = log.set_threshold(log.WARN) + try: + yield + finally: + log.set_threshold(saved) + + +class Wheel: def __init__(self, filename): match = WHEEL_NAME(os.path.basename(filename)) if match is None: @@ -68,7 +83,7 @@ class Wheel: setattr(self, k, v) def tags(self): - '''List tags (py_version, abi, platform) supported by this wheel.''' + """List tags (py_version, abi, platform) supported by this wheel.""" return itertools.product( self.py_version.split('.'), self.abi.split('.'), @@ -76,28 +91,31 @@ class Wheel: ) def is_compatible(self): - '''Is the wheel is compatible with the current platform?''' - supported_tags = set((t.interpreter, t.abi, t.platform) for t in sys_tags()) - return next((True for t in self.tags() if t in supported_tags), False) + """Is the wheel compatible with the current platform?""" + return next((True for t in self.tags() if t in _get_supported_tags()), False) def egg_name(self): - return pkg_resources.Distribution( - project_name=self.project_name, version=self.version, - platform=(None if self.platform == 'any' else get_platform()), - ).egg_name() + '.egg' + return ( + _egg_basename( + self.project_name, + self.version, + platform=(None if self.platform == 'any' else get_platform()), + ) + + ".egg" + ) def get_dist_info(self, zf): # find the correct name of the .dist-info dir in the wheel file for member in zf.namelist(): dirname = posixpath.dirname(member) - if (dirname.endswith('.dist-info') and - canonicalize_name(dirname).startswith( - canonicalize_name(self.project_name))): + if dirname.endswith('.dist-info') and canonicalize_name(dirname).startswith( + canonicalize_name(self.project_name) + ): return dirname raise ValueError("unsupported wheel format. .dist-info not found") def install_as_egg(self, destination_eggdir): - '''Install wheel as an egg directory.''' + """Install wheel as an egg directory.""" with zipfile.ZipFile(self.filename) as zf: self._install_as_egg(destination_eggdir, zf) @@ -113,27 +131,26 @@ class Wheel: @staticmethod def _convert_metadata(zf, destination_eggdir, dist_info, egg_info): + import pkg_resources + def get_metadata(name): with zf.open(posixpath.join(dist_info, name)) as fp: - value = fp.read().decode('utf-8') if PY3 else fp.read() + value = fp.read().decode('utf-8') return email.parser.Parser().parsestr(value) wheel_metadata = get_metadata('WHEEL') # Check wheel format version is supported. wheel_version = parse_version(wheel_metadata.get('Wheel-Version')) - wheel_v1 = ( - parse_version('1.0') <= wheel_version < parse_version('2.0dev0') - ) + wheel_v1 = parse_version('1.0') <= wheel_version < parse_version('2.0dev0') if not wheel_v1: - raise ValueError( - 'unsupported wheel format version: %s' % wheel_version) + raise ValueError('unsupported wheel format version: %s' % wheel_version) # Extract to target directory. - os.mkdir(destination_eggdir) - zf.extractall(destination_eggdir) + _unpack_zipfile_obj(zf, destination_eggdir) # Convert metadata. dist_info = os.path.join(destination_eggdir, dist_info) dist = pkg_resources.Distribution.from_location( - destination_eggdir, dist_info, + destination_eggdir, + dist_info, metadata=pkg_resources.PathMetadata(destination_eggdir, dist_info), ) @@ -143,13 +160,14 @@ class Wheel: def raw_req(req): req.marker = None return str(req) - install_requires = list(sorted(map(raw_req, dist.requires()))) + + install_requires = list(map(raw_req, dist.requires())) extras_require = { - extra: sorted( + extra: [ req for req in map(raw_req, dist.requires((extra,))) if req not in install_requires - ) + ] for extra in dist.extras } os.rename(dist_info, egg_info) @@ -163,17 +181,12 @@ class Wheel: extras_require=extras_require, ), ) - # Temporarily disable info traces. - log_threshold = log._global_log.threshold - log.set_threshold(log.WARN) - try: + with disable_info_traces(): write_requirements( setup_dist.get_command_obj('egg_info'), None, os.path.join(egg_info, 'requires.txt'), ) - finally: - log.set_threshold(log_threshold) @staticmethod def _move_data_entries(destination_eggdir, dist_data): @@ -181,8 +194,7 @@ class Wheel: dist_data = os.path.join(destination_eggdir, dist_data) dist_data_scripts = os.path.join(dist_data, 'scripts') if os.path.exists(dist_data_scripts): - egg_info_scripts = os.path.join( - destination_eggdir, 'EGG-INFO', 'scripts') + egg_info_scripts = os.path.join(destination_eggdir, 'EGG-INFO', 'scripts') os.mkdir(egg_info_scripts) for entry in os.listdir(dist_data_scripts): # Remove bytecode, as it's not properly handled @@ -195,18 +207,20 @@ class Wheel: os.path.join(egg_info_scripts, entry), ) os.rmdir(dist_data_scripts) - for subdir in filter(os.path.exists, ( - os.path.join(dist_data, d) - for d in ('data', 'headers', 'purelib', 'platlib') - )): + for subdir in filter( + os.path.exists, + ( + os.path.join(dist_data, d) + for d in ('data', 'headers', 'purelib', 'platlib') + ), + ): unpack(subdir, destination_eggdir) if os.path.exists(dist_data): os.rmdir(dist_data) @staticmethod def _fix_namespace_packages(egg_info, destination_eggdir): - namespace_packages = os.path.join( - egg_info, 'namespace_packages.txt') + namespace_packages = os.path.join(egg_info, 'namespace_packages.txt') if os.path.exists(namespace_packages): with open(namespace_packages) as fp: namespace_packages = fp.read().split() diff --git a/libs/common/setuptools/windows_support.py b/libs/common/setuptools/windows_support.py index cb977cff..8299ac1c 100644 --- a/libs/common/setuptools/windows_support.py +++ b/libs/common/setuptools/windows_support.py @@ -1,5 +1,4 @@ import platform -import ctypes def windows_only(func): @@ -13,10 +12,12 @@ def hide_file(path): """ Set the hidden attribute on a file or directory. - From http://stackoverflow.com/questions/19622133/ + From https://stackoverflow.com/questions/19622133/ `path` must be text. """ + import ctypes + __import__('ctypes.wintypes') SetFileAttributes = ctypes.windll.kernel32.SetFileAttributesW SetFileAttributes.argtypes = ctypes.wintypes.LPWSTR, ctypes.wintypes.DWORD diff --git a/libs/common/six.py b/libs/common/six.py index 4e15675d..84871a9c 100644 --- a/libs/common/six.py +++ b/libs/common/six.py @@ -263,7 +263,7 @@ _moved_attributes = [ MovedAttribute("reduce", "__builtin__", "functools"), MovedAttribute("shlex_quote", "pipes", "shlex", "quote"), MovedAttribute("StringIO", "StringIO", "io"), - MovedAttribute("UserDict", "UserDict", "collections"), + MovedAttribute("UserDict", "UserDict", "collections", "IterableUserDict", "UserDict"), MovedAttribute("UserList", "UserList", "collections"), MovedAttribute("UserString", "UserString", "collections"), MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), @@ -363,6 +363,8 @@ _urllib_parse_moved_attributes = [ MovedAttribute("splittag", "urllib", "urllib.parse"), MovedAttribute("splituser", "urllib", "urllib.parse"), MovedAttribute("splitvalue", "urllib", "urllib.parse"), + MovedAttribute("splittype", "urllib", "urllib.parse"), + MovedAttribute("splithost", "urllib", "urllib.parse"), MovedAttribute("uses_fragment", "urlparse", "urllib.parse"), MovedAttribute("uses_netloc", "urlparse", "urllib.parse"), MovedAttribute("uses_params", "urlparse", "urllib.parse"), @@ -531,6 +533,7 @@ if PY3: _func_code = "__code__" _func_defaults = "__defaults__" _func_globals = "__globals__" + _func_name = "__name__" else: _meth_func = "im_func" _meth_self = "im_self" @@ -539,6 +542,7 @@ else: _func_code = "func_code" _func_defaults = "func_defaults" _func_globals = "func_globals" + _func_name = "func_name" try: @@ -592,6 +596,7 @@ get_function_closure = operator.attrgetter(_func_closure) get_function_code = operator.attrgetter(_func_code) get_function_defaults = operator.attrgetter(_func_defaults) get_function_globals = operator.attrgetter(_func_globals) +get_function_name = operator.attrgetter(_func_name) if PY3: diff --git a/setup.py b/setup.py index dd93f90c..0a984763 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ def read(*names, **kwargs): setup( name='nzbToMedia', - version='12.1.12', + version='12.1.13', license='GPLv3', description='Efficient on demand post processing', long_description=""" @@ -73,17 +73,12 @@ setup( 'Operating System :: POSIX', 'Operating System :: Unix', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', 'Topic :: Games/Entertainment', 'Topic :: Multimedia', 'Topic :: Multimedia :: Sound/Audio', diff --git a/tox.ini b/tox.ini index 88d8e83e..e5fd86e7 100644 --- a/tox.ini +++ b/tox.ini @@ -4,19 +4,17 @@ envlist = clean, check, - {py27, py35, py36, py37, py38, py39, py310, py311}, + {py38, py39, py310, py311, py312, py313}, report [testenv] basepython = - py27: {env:TOXPYTHON:python2.7} - py35: {env:TOXPYTHON:python3.5} - py36: {env:TOXPYTHON:python3.6} - py37: {env:TOXPYTHON:python3.7} py38: {env:TOXPYTHON:python3.8} py39: {env:TOXPYTHON:python3.9} py310: {env:TOXPYTHON:python3.10} py311: {env:TOXPYTHON:python3.11} + py312: {env:TOXPYTHON:python3.12} + py313: {env:TOXPYTHON:python3.13} {clean,check,report,codecov}: {env:TOXPYTHON:python3} setenv = PYTHONPATH={toxinidir}/tests