diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..b56320fd --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,126 @@ +name: Automated Tests + +on: [push, pull_request] + +jobs: + test: + strategy: + matrix: + python: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9] + platform: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.platform }} + steps: + - uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - name: Install tox + run: | + python -m pip install tox + - name: Run tests + run: tox + env: + TOXENV: python + + qa: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.9 + - name: Install tox + run: | + python -m pip install tox + - name: Run checks + run: tox + env: + TOXENV: qa + + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.9 + - name: Install tox + run: | + python -m pip install tox + - name: Evaluate coverage + run: tox + env: + TOXENV: cov + + benchmark: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.9 + - name: Install tox + run: | + python -m pip install tox + - name: Run benchmarks + run: tox + env: + TOXENV: perf + + diffcov: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.9 + - name: Install tox + run: | + python -m pip install tox + - name: Evaluate coverage + run: tox + env: + TOXENV: diffcov + + docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.9 + - name: Install tox + run: | + python -m pip install tox + - name: Build docs + run: tox + env: + TOXENV: docs + + release: + needs: test + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.9 + - name: Install tox + run: | + python -m pip install tox + - name: Release + run: tox -e release + env: + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index a8d1bd28..00000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,50 +0,0 @@ -image: quay.io/python-devs/ci-image - -stages: - - test - - qa - - docs - - codecov - - deploy - -qa: - script: - - tox -e qa - -tests: - script: - - tox -e py27,py35,py36,py37,py38 - -coverage: - script: - - tox -e py27-cov,py35-cov,py36-cov,py37-cov,py38-cov - artifacts: - paths: - - coverage.xml - -benchmark: - script: - - tox -e perf - -diffcov: - script: - - tox -e py27-diffcov,py35-diffcov,py36-diffcov,py37-diffcov,py38-diffcov - -docs: - script: - - tox -e docs - -codecov: - stage: codecov - dependencies: - - coverage - script: - - codecov - when: on_success - -release: - stage: deploy - only: - - /^v\d+\.\d+(\.\d+)?([abc]\d*)?$/ - script: - - tox -e release diff --git a/README.rst b/README.rst index 2bdd4b8a..5655d9ab 100644 --- a/README.rst +++ b/README.rst @@ -2,8 +2,14 @@ ``importlib_metadata`` ========================= -``importlib_metadata`` is a library to access the metadata for a Python -package. It is intended to be ported to Python 3.8. +``importlib_metadata`` is a library to access the metadata for a +Python package. + +As of Python 3.8, this functionality has been added to the +`Python standard library +`_. +This package supplies backports of that functionality including +improvements added to subsequent Python versions. Usage @@ -30,7 +36,7 @@ tools (or other conforming packages). It does not support: Project details =============== - * Project home: https://gitlab.com/python-devs/importlib_metadata - * Report bugs at: https://gitlab.com/python-devs/importlib_metadata/issues - * Code hosting: https://gitlab.com/python-devs/importlib_metadata.git - * Documentation: http://importlib_metadata.readthedocs.io/ + * Project home: https://github.com/python/importlib_metadata + * Report bugs at: https://github.com/python/importlib_metadata/issues + * Code hosting: https://github.com/python/importlib_metadata + * Documentation: https://importlib_metadata.readthedocs.io/ diff --git a/importlib_metadata/docs/__init__.py b/docs/__init__.py similarity index 100% rename from importlib_metadata/docs/__init__.py rename to docs/__init__.py diff --git a/importlib_metadata/docs/changelog.rst b/docs/changelog.rst similarity index 96% rename from importlib_metadata/docs/changelog.rst rename to docs/changelog.rst index b7e93b5d..532ec971 100644 --- a/importlib_metadata/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,20 @@ importlib_metadata NEWS ========================= +v2.1.1 +====== + +* #261: Restored compatibility for package discovery for + metadata without version in the name and for legacy + eggs. + +v2.1.0 +====== + +* #253: When querying for package metadata, the lookup + now honors + `package normalization rules `_. + v2.0.0 ====== diff --git a/importlib_metadata/docs/conf.py b/docs/conf.py similarity index 100% rename from importlib_metadata/docs/conf.py rename to docs/conf.py diff --git a/importlib_metadata/docs/index.rst b/docs/index.rst similarity index 74% rename from importlib_metadata/docs/index.rst rename to docs/index.rst index 530197cf..57332f5e 100644 --- a/importlib_metadata/docs/index.rst +++ b/docs/index.rst @@ -10,9 +10,11 @@ Python 3.7 and newer (backported as :doc:`importlib_resources ` module for Python 2.7, and 3.4 through 3.7. Users of -Python 3.8 and beyond are encouraged to use the standard library module. +``importlib_metadata`` supplies a backport of +:doc:`importlib.metadata ` as found in +Python 3.8 and later for earlier Python releases. Users of +Python 3.8 and beyond are encouraged to use the standard library module +when possible and fall back to ``importlib_metadata`` when necessary. When imported on Python 3.8 and later, ``importlib_metadata`` replaces the DistributionFinder behavior from the stdlib, but leaves the API in tact. Developers looking for detailed API descriptions should refer to the Python @@ -32,10 +34,10 @@ The documentation here includes a general :ref:`usage ` guide. Project details =============== - * Project home: https://gitlab.com/python-devs/importlib_metadata - * Report bugs at: https://gitlab.com/python-devs/importlib_metadata/issues - * Code hosting: https://gitlab.com/python-devs/importlib_metadata.git - * Documentation: http://importlib_metadata.readthedocs.io/ + * Project home: https://github.com/python/importlib_metadata + * Report bugs at: https://github.com/python/importlib_metadata/issues + * Code hosting: https://github.com/python/importlib_metadata + * Documentation: https://importlib_metadata.readthedocs.io/ Indices and tables diff --git a/importlib_metadata/docs/using.rst b/docs/using.rst similarity index 100% rename from importlib_metadata/docs/using.rst rename to docs/using.rst diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 7031323d..e296a2c7 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -462,43 +462,67 @@ def zip_children(self): for child in names ) - def is_egg(self, search): - base = self.base - return ( - base == search.versionless_egg_name - or base.startswith(search.prefix) - and base.endswith('.egg')) - def search(self, name): - for child in self.children(): - n_low = child.lower() - if (n_low in name.exact_matches - or n_low.startswith(name.prefix) - and n_low.endswith(name.suffixes) - # legacy case: - or self.is_egg(name) and n_low == 'egg-info'): - yield self.joinpath(child) + return ( + self.joinpath(child) + for child in self.children() + if name.matches(child, self.base) + ) class Prepared: """ A prepared search for metadata on a possibly-named package. """ - normalized = '' - prefix = '' + normalized = None suffixes = '.dist-info', '.egg-info' exact_matches = [''][:0] - versionless_egg_name = '' def __init__(self, name): self.name = name if name is None: return - self.normalized = name.lower().replace('-', '_') - self.prefix = self.normalized + '-' + self.normalized = self.normalize(name) self.exact_matches = [ self.normalized + suffix for suffix in self.suffixes] - self.versionless_egg_name = self.normalized + '.egg' + + @staticmethod + def normalize(name): + """ + PEP 503 normalization plus dashes as underscores. + """ + return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_') + + @staticmethod + def legacy_normalize(name): + """ + Normalize the package name as found in the convention in + older packaging tools versions and specs. + """ + return name.lower().replace('-', '_') + + def matches(self, cand, base): + low = cand.lower() + pre, ext = os.path.splitext(low) + name, sep, rest = pre.partition('-') + return ( + low in self.exact_matches + or ext in self.suffixes and ( + not self.normalized or + name.replace('.', '_') == self.normalized + ) + # legacy case: + or self.is_egg(base) and low == 'egg-info' + ) + + def is_egg(self, base): + normalized = self.legacy_normalize(self.name or '') + prefix = normalized + '-' if normalized else '' + versionless_egg_name = normalized + '.egg' if self.name else '' + return ( + base == versionless_egg_name + or base.startswith(prefix) + and base.endswith('.egg')) @install diff --git a/setup.cfg b/setup.cfg index eee6caf2..fa10c8d3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,16 +22,7 @@ install_requires = pathlib2; python_version < '3' contextlib2; python_version < '3' configparser>=3.5; python_version < '3' -packages = find: - -[options.package_data] -* = *.zip, *.file, *.txt, *.toml -importlib_metadata = - docs/* - docs/_static/* -importlib_metadata.tests.data = - *.egg - *.whl +packages = importlib_metadata [mypy] ignore_missing_imports = True @@ -42,12 +33,6 @@ ignore_missing_imports = True # that package.__spec__ is not None. strict_optional = False -[mypy-importlib_metadata.docs.*] -ignore_errors: True - -[mypy-importlib_metadata.tests.*] -ignore_errors: True - [wheel] universal=1 @@ -56,6 +41,7 @@ testing = importlib_resources>=1.3; python_version < "3.9" packaging pep517 + unittest2; python_version < "3" docs = sphinx rst.linker diff --git a/importlib_metadata/tests/__init__.py b/tests/__init__.py similarity index 100% rename from importlib_metadata/tests/__init__.py rename to tests/__init__.py diff --git a/importlib_metadata/tests/data/__init__.py b/tests/data/__init__.py similarity index 100% rename from importlib_metadata/tests/data/__init__.py rename to tests/data/__init__.py diff --git a/importlib_metadata/tests/data/example-21.12-py3-none-any.whl b/tests/data/example-21.12-py3-none-any.whl similarity index 100% rename from importlib_metadata/tests/data/example-21.12-py3-none-any.whl rename to tests/data/example-21.12-py3-none-any.whl diff --git a/importlib_metadata/tests/data/example-21.12-py3.6.egg b/tests/data/example-21.12-py3.6.egg similarity index 100% rename from importlib_metadata/tests/data/example-21.12-py3.6.egg rename to tests/data/example-21.12-py3.6.egg diff --git a/importlib_metadata/tests/fixtures.py b/tests/fixtures.py similarity index 84% rename from importlib_metadata/tests/fixtures.py rename to tests/fixtures.py index 20982fa1..71e17275 100644 --- a/importlib_metadata/tests/fixtures.py +++ b/tests/fixtures.py @@ -5,9 +5,10 @@ import shutil import tempfile import textwrap -import test.support -from .._compat import pathlib, contextlib +from .py39compat import FS_NONASCII + +from importlib_metadata._compat import pathlib, contextlib __metaclass__ = type @@ -103,6 +104,42 @@ def setUp(self): build_files(DistInfoPkg.files, self.site_dir) +class DistInfoPkgWithDot(OnSysPath, SiteDir): + files = { + "pkg_dot-1.0.0.dist-info": { + "METADATA": """ + Name: pkg.dot + Version: 1.0.0 + """, + }, + } + + def setUp(self): + super(DistInfoPkgWithDot, self).setUp() + build_files(DistInfoPkgWithDot.files, self.site_dir) + + +class DistInfoPkgWithDotLegacy(OnSysPath, SiteDir): + files = { + "pkg.dot-1.0.0.dist-info": { + "METADATA": """ + Name: pkg.dot + Version: 1.0.0 + """, + }, + "pkg.lot.egg-info": { + "METADATA": """ + Name: pkg.lot + Version: 1.0.0 + """, + }, + } + + def setUp(self): + super(DistInfoPkgWithDotLegacy, self).setUp() + build_files(DistInfoPkgWithDotLegacy.files, self.site_dir) + + class DistInfoPkgOffPath(SiteDir): def setUp(self): super(DistInfoPkgOffPath, self).setUp() @@ -218,7 +255,7 @@ def build_files(file_defs, prefix=pathlib.Path()): class FileBuilder: def unicode_filename(self): - return test.support.FS_NONASCII or \ + return FS_NONASCII or \ self.skip("File system does not support non-ascii.") diff --git a/tests/py39compat.py b/tests/py39compat.py new file mode 100644 index 00000000..a175d4c3 --- /dev/null +++ b/tests/py39compat.py @@ -0,0 +1,4 @@ +try: + from test.support.os_helpers import FS_NONASCII +except ImportError: + from test.support import FS_NONASCII # noqa diff --git a/importlib_metadata/tests/test_api.py b/tests/test_api.py similarity index 81% rename from importlib_metadata/tests/test_api.py rename to tests/test_api.py index eb0ff53b..f36da1c5 100644 --- a/importlib_metadata/tests/test_api.py +++ b/tests/test_api.py @@ -1,9 +1,13 @@ import re import textwrap -import unittest + +try: + import unittest2 as unittest +except ImportError: + import unittest from . import fixtures -from .. import ( +from importlib_metadata import ( Distribution, PackageNotFoundError, distribution, entry_points, files, metadata, requires, version, ) @@ -22,6 +26,7 @@ class APITests( fixtures.EggInfoPkg, fixtures.DistInfoPkg, + fixtures.DistInfoPkgWithDot, fixtures.EggInfoFile, unittest.TestCase): @@ -41,6 +46,19 @@ def test_for_name_does_not_exist(self): with self.assertRaises(PackageNotFoundError): distribution('does-not-exist') + def test_name_normalization(self): + names = 'pkg.dot', 'pkg_dot', 'pkg-dot', 'pkg..dot', 'Pkg.Dot' + for name in names: + with self.subTest(name): + assert distribution(name).metadata['Name'] == 'pkg.dot' + + def test_prefix_not_matched(self): + prefixes = 'p', 'pkg', 'pkg.' + for prefix in prefixes: + with self.subTest(prefix): + with self.assertRaises(PackageNotFoundError): + distribution(prefix) + def test_for_top_level(self): self.assertEqual( distribution('egginfo-pkg').read_text('top_level.txt').strip(), @@ -156,6 +174,20 @@ def test_more_complex_deps_requires_text(self): assert deps == expected +class LegacyDots(fixtures.DistInfoPkgWithDotLegacy, unittest.TestCase): + def test_name_normalization(self): + names = 'pkg.dot', 'pkg_dot', 'pkg-dot', 'pkg..dot', 'Pkg.Dot' + for name in names: + with self.subTest(name): + assert distribution(name).metadata['Name'] == 'pkg.dot' + + def test_name_normalization_versionless_egg_info(self): + names = 'pkg.lot', 'pkg_lot', 'pkg-lot', 'pkg..lot', 'Pkg.Lot' + for name in names: + with self.subTest(name): + assert distribution(name).metadata['Name'] == 'pkg.lot' + + class OffSysPathTests(fixtures.DistInfoPkgOffPath, unittest.TestCase): def test_find_distributions_specified_path(self): dists = Distribution.discover(path=[str(self.site_dir)]) diff --git a/importlib_metadata/tests/test_integration.py b/tests/test_integration.py similarity index 97% rename from importlib_metadata/tests/test_integration.py rename to tests/test_integration.py index cbb940bd..377574c4 100644 --- a/importlib_metadata/tests/test_integration.py +++ b/tests/test_integration.py @@ -7,7 +7,7 @@ import packaging.version from . import fixtures -from .. import ( +from importlib_metadata import ( Distribution, _compat, version, diff --git a/importlib_metadata/tests/test_main.py b/tests/test_main.py similarity index 99% rename from importlib_metadata/tests/test_main.py rename to tests/test_main.py index 4ffdd5d6..847750bc 100644 --- a/importlib_metadata/tests/test_main.py +++ b/tests/test_main.py @@ -11,7 +11,7 @@ import pyfakefs.fake_filesystem_unittest as ffs from . import fixtures -from .. import ( +from importlib_metadata import ( Distribution, EntryPoint, MetadataPathFinder, PackageNotFoundError, distributions, entry_points, metadata, version, diff --git a/importlib_metadata/tests/test_zip.py b/tests/test_zip.py similarity index 97% rename from importlib_metadata/tests/test_zip.py rename to tests/test_zip.py index 4aae933d..5cebcd02 100644 --- a/importlib_metadata/tests/test_zip.py +++ b/tests/test_zip.py @@ -1,7 +1,7 @@ import sys import unittest -from .. import ( +from importlib_metadata import ( distribution, entry_points, files, PackageNotFoundError, version, distributions, ) @@ -20,7 +20,7 @@ class TestZip(unittest.TestCase): - root = 'importlib_metadata.tests.data' + root = 'tests.data' def _fixture_on_path(self, filename): pkg_file = resources.files(self.root).joinpath(filename) diff --git a/tox.ini b/tox.ini index b2775cd1..1f0e9757 100644 --- a/tox.ini +++ b/tox.ini @@ -55,7 +55,7 @@ extras = [testenv:docs] basepython = python3 commands = - sphinx-build importlib_metadata/docs build/sphinx/html + sphinx-build docs build/sphinx/html extras = docs