From b2412262dc1dd5d3d697e551d86acee4d5519bb6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 30 Sep 2022 13:30:09 -0400 Subject: [PATCH 001/131] Indicate to use latest Python version (workaround for readthedocs/readthedocs.org/#9623). Requires also specifying the OS version (workaround for readthedocs/readthedocs.org#9635). --- .readthedocs.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.readthedocs.yml b/.readthedocs.yml index cc698548..6bef3493 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -4,3 +4,10 @@ python: - path: . extra_requirements: - docs + +# workaround for readthedocs/readthedocs.org#9623 +build: + # workaround for readthedocs/readthedocs.org#9635 + os: ubuntu-22.04 + tools: + python: "3" From ecb363c061d4b24f8fb84621ebb5529dd8685f45 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 3 Oct 2022 09:30:48 -0400 Subject: [PATCH 002/131] Simply wrap .matches instead of replacing EntryPoint. --- importlib_metadata/__init__.py | 3 +-- importlib_metadata/_py39compat.py | 21 ++++----------------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index eab5d4c1..33c632a2 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -382,8 +382,7 @@ def select(self, **params): Select entry points from self that match the given parameters (typically group and/or name). """ - candidates = (_py39compat.ep_matches(ep, **params) for ep in self) - return EntryPoints(ep for ep, predicate in candidates if predicate) + return EntryPoints(ep for ep in self if _py39compat.ep_matches(ep, **params)) @property def names(self): diff --git a/importlib_metadata/_py39compat.py b/importlib_metadata/_py39compat.py index cf9cc124..cde4558f 100644 --- a/importlib_metadata/_py39compat.py +++ b/importlib_metadata/_py39compat.py @@ -1,7 +1,7 @@ """ Compatibility layer with Python 3.8/3.9 """ -from typing import TYPE_CHECKING, Any, Optional, Tuple +from typing import TYPE_CHECKING, Any, Optional if TYPE_CHECKING: # pragma: no cover # Prevent circular imports on runtime. @@ -22,27 +22,14 @@ def normalized_name(dist: Distribution) -> Optional[str]: return Prepared.normalize(getattr(dist, "name", None) or dist.metadata['Name']) -def ep_matches(ep: EntryPoint, **params) -> Tuple[EntryPoint, bool]: +def ep_matches(ep: EntryPoint, **params) -> bool: """ Workaround for ``EntryPoint`` objects without the ``matches`` method. - For the sake of convenience, a tuple is returned containing not only the - boolean value corresponding to the predicate evalutation, but also a compatible - ``EntryPoint`` object that can be safely used at a later stage. - - For example, the following sequences of expressions should be compatible: - - # Sequence 1: using the compatibility layer - candidates = (_py39compat.ep_matches(ep, **params) for ep in entry_points) - [ep for ep, predicate in candidates if predicate] - - # Sequence 2: using Python 3.9+ - [ep for ep in entry_points if ep.matches(**params)] """ try: - return ep, ep.matches(**params) + return ep.matches(**params) except AttributeError: from . import EntryPoint # -> delay to prevent circular imports. # Reconstruct the EntryPoint object to make sure it is compatible. - _ep = EntryPoint(ep.name, ep.value, ep.group) - return _ep, _ep.matches(**params) + return EntryPoint(ep.name, ep.value, ep.group).matches(**params) From 13438768ba4f2fea8e4c9407bc66674bafd598f0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 4 Oct 2022 11:50:53 -0400 Subject: [PATCH 003/131] Add minimum retention of DeprecatedTuple. Ref #409, Ref #348. --- importlib_metadata/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index f5967e41..26a1388c 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -139,6 +139,7 @@ class DeprecatedTuple: 1 """ + # Do not remove prior to 2023-05-01 or Python 3.13 _warn = functools.partial( warnings.warn, "EntryPoint tuple interface is deprecated. Access members by name.", From 9674092a543bd3e046eece53a26e0cb59e814fde Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 5 Oct 2022 15:08:07 -0400 Subject: [PATCH 004/131] Fix warning in plural of Import Package. --- docs/using.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index bbb3824f..7707078e 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -7,7 +7,7 @@ ``importlib_metadata`` is a library that provides access to the metadata of an installed :term:`packaging:Distribution Package`, such as its entry points -or its top-level names (:term:`packaging:Import Package`s, modules, if any). +or its top-level names (:term:`packaging:Import Package`\ s, modules, if any). Built in part on Python's import system, this library intends to replace similar functionality in the `entry point API`_ and `metadata API`_ of ``pkg_resources``. Along with From f75acec52c2eb8ded78ee753a95fd214b9f877a2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Oct 2022 13:46:44 -0400 Subject: [PATCH 005/131] Correct syntax is without the space. --- docs/using.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index 7707078e..37428fd0 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -7,7 +7,7 @@ ``importlib_metadata`` is a library that provides access to the metadata of an installed :term:`packaging:Distribution Package`, such as its entry points -or its top-level names (:term:`packaging:Import Package`\ s, modules, if any). +or its top-level names (:term:`packaging:Import Package`\s, modules, if any). Built in part on Python's import system, this library intends to replace similar functionality in the `entry point API`_ and `metadata API`_ of ``pkg_resources``. Along with From e95c54fe607aaa980a064b6490312483381ba0ab Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 9 Oct 2022 11:35:13 -0400 Subject: [PATCH 006/131] GHA pretty env (#67) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🎨 Make the GHA log is clean and colorized This patch sets up root-level environment variables shared by all the workflow jobs. They include: * Disabling undesired `pip`'s warnings/suggestions * Requesting the executed apps color their output unconditionally * Letting `tox` pass those requests to underlying/wrapped programs * Reformat without end of line comments. Group into sections. * Avoid numerics for booleans where possible. Choose arbitrary numeric where any numeric is accepted. Co-authored-by: Sviatoslav Sydorenko --- .github/workflows/main.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 46e1ec9c..102e0e2b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,6 +2,36 @@ name: tests on: [push, pull_request] +env: + # Environment variables to support color support (jaraco/skeleton#66): + # Request colored output from CLI tools supporting it. Different tools + # interpret the value differently. For some, just being set is sufficient. + # For others, it must be a non-zero integer. For yet others, being set + # to a non-empty value is sufficient. + FORCE_COLOR: -106 + # MyPy's color enforcement (must be a non-zero number) + MYPY_FORCE_COLOR: -42 + # Recognized by the `py` package, dependency of `pytest` (must be "1") + PY_COLORS: 1 + # Make tox-wrapped tools see color requests + TOX_TESTENV_PASSENV: >- + FORCE_COLOR + MYPY_FORCE_COLOR + NO_COLOR + PY_COLORS + PYTEST_THEME + PYTEST_THEME_MODE + + # Suppress noisy pip warnings + PIP_DISABLE_PIP_VERSION_CHECK: 'true' + PIP_NO_PYTHON_VERSION_WARNING: 'true' + PIP_NO_WARN_SCRIPT_LOCATION: 'true' + + # Disable the spinner, noise in GHA; TODO(webknjaz): Fix this upstream + # Must be "1". + TOX_PARALLEL_NO_SPINNER: 1 + + jobs: test: strategy: From 54675240d4b4d2452a3777c5156f688e42a6c985 Mon Sep 17 00:00:00 2001 From: Zach Burnett Date: Thu, 13 Oct 2022 15:00:05 -0400 Subject: [PATCH 007/131] rename `.readthedocs.yml` to `.readthedocs.yaml` (RTD docs indicate that `.readthedocs.yml` will be deprecated) (#68) --- .readthedocs.yml => .readthedocs.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .readthedocs.yml => .readthedocs.yaml (100%) diff --git a/.readthedocs.yml b/.readthedocs.yaml similarity index 100% rename from .readthedocs.yml rename to .readthedocs.yaml From a0d28e5e792b20e81bc00bb07c9ff14b52d10924 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 17 Oct 2022 16:30:28 -0400 Subject: [PATCH 008/131] Add row for Python 3.12 to compatibility map. --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 47fa0cb2..08e06ea8 100644 --- a/README.rst +++ b/README.rst @@ -42,6 +42,8 @@ were contributed to different versions in the standard library: * - importlib_metadata - stdlib + * - 5.0 + - 3.12 * - 4.8 - 3.11 * - 4.4 From 88ad39d6b27f8bce591e1c8acb47094278534ce7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 30 Oct 2022 12:36:44 -0400 Subject: [PATCH 009/131] Update compatibility matrix to reflect 4.13 in 3.11 (python/cpython#98875). --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 08e06ea8..c85645ee 100644 --- a/README.rst +++ b/README.rst @@ -44,7 +44,7 @@ were contributed to different versions in the standard library: - stdlib * - 5.0 - 3.12 - * - 4.8 + * - 4.13 - 3.11 * - 4.4 - 3.10 From 151032887aea82292b42b2f4b74263d79b62e167 Mon Sep 17 00:00:00 2001 From: Julien Palard Date: Tue, 18 Oct 2022 15:28:19 +0200 Subject: [PATCH 010/131] Doc: missing underscore in hyperlink. (GH-98391) --- docs/using.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index 40616d86..03b30576 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -200,7 +200,7 @@ all the metadata in a JSON-compatible form per PEP 566:: The actual type of the object returned by ``metadata()`` is an implementation detail and should be accessed only through the interface described by the - `PackageMetadata protocol `. + `PackageMetadata protocol `_. .. _version: From 92498a6e6908368804565601de97d3432d1a0818 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 5 Nov 2022 11:49:03 -0400 Subject: [PATCH 011/131] Python 3.10 is synced mainly through 4.6 (plus bugfixes). --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index c85645ee..e2cee859 100644 --- a/README.rst +++ b/README.rst @@ -46,7 +46,7 @@ were contributed to different versions in the standard library: - 3.12 * - 4.13 - 3.11 - * - 4.4 + * - 4.6 - 3.10 * - 1.4 - 3.8 From da84e5c7dabacf379165a0829b2f1741060ee2c6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 8 Nov 2022 05:25:30 -0500 Subject: [PATCH 012/131] Pin mypy to '<0.990' due to realpython/pytest-mypy#141 --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.cfg b/setup.cfg index a0d86eba..503cbfda 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,6 +40,8 @@ testing = pytest-mypy >= 0.9.1; \ # workaround for jaraco/skeleton#22 python_implementation != "PyPy" + # workaround for realpython/pytest-mypy#141 + mypy < 0.990 pytest-enabler >= 1.3 # local From f999a531587170b577da64d4bfb67a68b9aec106 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 21 Oct 2022 11:14:42 -0400 Subject: [PATCH 013/131] Remove the hyperlink for the Python versions badge. The PyPI badge is a better anchor for the hyperlink. --- README.rst | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.rst b/README.rst index c82c6429..39459a4a 100644 --- a/README.rst +++ b/README.rst @@ -1,10 +1,7 @@ .. image:: https://img.shields.io/pypi/v/skeleton.svg - :target: `PyPI link`_ + :target: https://pypi.org/project/skeleton .. image:: https://img.shields.io/pypi/pyversions/skeleton.svg - :target: `PyPI link`_ - -.. _PyPI link: https://pypi.org/project/skeleton .. image:: https://github.com/jaraco/skeleton/workflows/tests/badge.svg :target: https://github.com/jaraco/skeleton/actions?query=workflow%3A%22tests%22 From 401287d8d0f9fb0365149983f5ca42618f00a6d8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 9 Nov 2022 19:32:49 -0500 Subject: [PATCH 014/131] Apply explicit_package_bases for mypy and unpin the version. Ref python/mypy#14057. --- mypy.ini | 3 +++ setup.cfg | 2 -- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mypy.ini b/mypy.ini index 976ba029..b6f97276 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,2 +1,5 @@ [mypy] ignore_missing_imports = True +# required to support namespace packages +# https://github.com/python/mypy/issues/14057 +explicit_package_bases = True diff --git a/setup.cfg b/setup.cfg index 503cbfda..a0d86eba 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,8 +40,6 @@ testing = pytest-mypy >= 0.9.1; \ # workaround for jaraco/skeleton#22 python_implementation != "PyPy" - # workaround for realpython/pytest-mypy#141 - mypy < 0.990 pytest-enabler >= 1.3 # local From e16916fedcbeb41ba3e326b9b4fb0b66e660f3bd Mon Sep 17 00:00:00 2001 From: layday Date: Wed, 16 Nov 2022 22:09:56 +0200 Subject: [PATCH 015/131] Fix `SimplePath` protocol This makes `pathlib.Path`s and `zipfile.Path`s assignable to the protocol. --- importlib_metadata/_meta.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/importlib_metadata/_meta.py b/importlib_metadata/_meta.py index 37ee43e6..259b15ba 100644 --- a/importlib_metadata/_meta.py +++ b/importlib_metadata/_meta.py @@ -30,18 +30,19 @@ def json(self) -> Dict[str, Union[str, List[str]]]: """ -class SimplePath(Protocol): +class SimplePath(Protocol[_T]): """ A minimal subset of pathlib.Path required by PathDistribution. """ - def joinpath(self) -> 'SimplePath': + def joinpath(self) -> _T: ... # pragma: no cover - def __truediv__(self) -> 'SimplePath': + def __truediv__(self, other: Union[str, _T]) -> _T: ... # pragma: no cover - def parent(self) -> 'SimplePath': + @property + def parent(self) -> _T: ... # pragma: no cover def read_text(self) -> str: From 56b6f1d1d7a975b27f96c4e15a20077914b4c554 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 18 Nov 2022 22:32:51 -0500 Subject: [PATCH 016/131] Add Python 3.12 to matrix. Only test 3.8-3.10 on Linux. --- .github/workflows/main.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 102e0e2b..3a28be36 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -38,8 +38,8 @@ jobs: matrix: python: - "3.7" - - "3.10" - "3.11" + - "3.12" # Workaround for actions/setup-python#508 dev: - -dev @@ -48,6 +48,12 @@ jobs: - macos-latest - windows-latest include: + - python: "3.8" + platform: ubuntu-latest + - python: "3.9" + platform: ubuntu-latest + - python: "3.10" + platform: ubuntu-latest - python: pypy3.9 platform: ubuntu-latest runs-on: ${{ matrix.platform }} From 9e13598ce4b81c2c964dd555fa407bb3ba4cc607 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 19 Nov 2022 09:36:01 -0500 Subject: [PATCH 017/131] Disable flake8 on Python 3.12. Workaround for tholo/pytest-flake8#87. --- setup.cfg | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index a0d86eba..a8f80ced 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,7 +30,9 @@ testing = # upstream pytest >= 6 pytest-checkdocs >= 2.4 - pytest-flake8 + pytest-flake8; \ + # workaround for tholo/pytest-flake8#87 + python_version < "3.12" # workaround for tholo/pytest-flake8#87 flake8 < 5 pytest-black >= 0.3.7; \ From c7d639e7da133d8ed8027f4c40bf477b8a447459 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 24 Nov 2022 08:52:56 -0500 Subject: [PATCH 018/131] Update changelog. --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 5aa5776f..cf7dcf16 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +v5.1.0 +====== + +* #415: Instrument ``SimplePath`` with generic support. + v5.0.0 ====== From b74765da2d794941012b145bf228eeabc42ba73b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 24 Nov 2022 09:02:28 -0500 Subject: [PATCH 019/131] Add note to docs about limitation of packages_distributions. Fixes #402. --- docs/using.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/using.rst b/docs/using.rst index f3f63176..831ad62b 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -289,6 +289,10 @@ Python module or :term:`packaging:Import Package`:: >>> packages_distributions() {'importlib_metadata': ['importlib-metadata'], 'yaml': ['PyYAML'], 'jaraco': ['jaraco.classes', 'jaraco.functools'], ...} +Some editable installs, `do not supply top-level names +`_, and thus this +function is not reliable with such installs. + .. _distributions: Distributions From 9708c37ef0d286c4e907adc59f46cc92262e3bf1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 6 Dec 2022 09:10:17 -0500 Subject: [PATCH 020/131] Honor ResourceWarnings. Fixes jaraco/skeleton#73. --- pytest.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pytest.ini b/pytest.ini index 80e98cc9..2c2817b8 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,6 +3,9 @@ norecursedirs=dist build .tox .eggs addopts=--doctest-modules doctest_optionflags=ALLOW_UNICODE ELLIPSIS filterwarnings= + # Ensure ResourceWarnings are emitted + default::ResourceWarning + # Suppress deprecation warning in flake8 ignore:SelectableGroups dict interface is deprecated::flake8 From 86a55c8320e2706d0f92e3248c29351bff83da4b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 8 Dec 2022 19:18:02 -0500 Subject: [PATCH 021/131] tox 4 requires a boolean value, so use '1' to FORCE_COLOR. Fixes jaraco/skeleton#74. --- .github/workflows/main.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3a28be36..e1e7bf19 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,8 +7,10 @@ env: # Request colored output from CLI tools supporting it. Different tools # interpret the value differently. For some, just being set is sufficient. # For others, it must be a non-zero integer. For yet others, being set - # to a non-empty value is sufficient. - FORCE_COLOR: -106 + # to a non-empty value is sufficient. For tox, it must be one of + # , 0, 1, false, no, off, on, true, yes. The only enabling value + # in common is "1". + FORCE_COLOR: 1 # MyPy's color enforcement (must be a non-zero number) MYPY_FORCE_COLOR: -42 # Recognized by the `py` package, dependency of `pytest` (must be "1") From ef521390cb51a12eab5c4155900f45dc2c89d507 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 10 Dec 2022 23:17:14 -0500 Subject: [PATCH 022/131] Remove unnecessary shebang and encoding header in docs conf. --- docs/conf.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index fa741a85..c2043393 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,3 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- - extensions = [ 'sphinx.ext.autodoc', 'jaraco.packaging.sphinx', From c68ac3b7a3001502f681722dc55dff70a3169276 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 11 Dec 2022 21:04:34 -0500 Subject: [PATCH 023/131] Prevent Python 3.12 from blocking checks. --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e1e7bf19..9d02856b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -59,6 +59,7 @@ jobs: - python: pypy3.9 platform: ubuntu-latest runs-on: ${{ matrix.platform }} + continue-on-error: ${{ matrix.python == '3.12' }} steps: - uses: actions/checkout@v3 - name: Setup Python From 82465b907d5131a57862a7242d64d610c3a05039 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 17 Dec 2022 20:46:15 -0500 Subject: [PATCH 024/131] Build docs in CI, including sphinx-lint. --- .github/workflows/main.yml | 17 +++++++++++++++++ setup.cfg | 1 + tox.ini | 1 + 3 files changed, 19 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9d02856b..9629a26a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -72,11 +72,28 @@ jobs: - name: Run tests run: tox + docs: + runs-on: ubuntu-latest + env: + TOXENV: docs + steps: + - uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }}${{ matrix.dev }} + - name: Install tox + run: | + python -m pip install tox + - name: Run tests + run: tox + check: # This job does nothing and is only used for the branch protection if: always() needs: - test + - docs runs-on: ubuntu-latest diff --git a/setup.cfg b/setup.cfg index a8f80ced..c062c7b9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -52,6 +52,7 @@ docs = jaraco.packaging >= 9 rst.linker >= 1.9 furo + sphinx-lint # local diff --git a/tox.ini b/tox.ini index 3ca2af38..42ae6852 100644 --- a/tox.ini +++ b/tox.ini @@ -20,6 +20,7 @@ extras = changedir = docs commands = python -m sphinx -W --keep-going . {toxinidir}/build/html + python -m sphinxlint [testenv:release] skip_install = True From 880a6219a16911817214827020f272b4b03b54b4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 18 Dec 2022 16:37:30 -0500 Subject: [PATCH 025/131] Mark `PackageMetadata.__getitem__` as deprecated for missing values. Ref #371. --- CHANGES.rst | 7 +++++++ importlib_metadata/_adapters.py | 22 ++++++++++++++++++++++ tests/test_api.py | 8 ++++++++ 3 files changed, 37 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index cf7dcf16..a2df91a3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,10 @@ +v5.2.0 +====== + +* #371: Deprecated expectation that ``PackageMetadata.__getitem__`` + will return ``None`` for missing keys. In the future, it will raise a + ``KeyError``. + v5.1.0 ====== diff --git a/importlib_metadata/_adapters.py b/importlib_metadata/_adapters.py index aa460d3e..e33cba5e 100644 --- a/importlib_metadata/_adapters.py +++ b/importlib_metadata/_adapters.py @@ -1,8 +1,20 @@ +import functools +import warnings import re import textwrap import email.message from ._text import FoldedCase +from ._compat import pypy_partial + + +# Do not remove prior to 2024-01-01 or Python 3.14 +_warn = functools.partial( + warnings.warn, + "Implicit None on return values is deprecated and will raise KeyErrors.", + DeprecationWarning, + stacklevel=pypy_partial(2), +) class Message(email.message.Message): @@ -39,6 +51,16 @@ def __init__(self, *args, **kwargs): def __iter__(self): return super().__iter__() + def __getitem__(self, item): + """ + Warn users that a ``KeyError`` can be expected when a + mising key is supplied. Ref python/importlib_metadata#371. + """ + res = super().__getitem__(item) + if res is None: + _warn() + return res + def _repair_headers(self): def redent(value): "Correct for RFC822 indentation" diff --git a/tests/test_api.py b/tests/test_api.py index f65287a5..504d0553 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -141,6 +141,14 @@ def test_importlib_metadata_version(self): resolved = version('importlib-metadata') assert re.match(self.version_pattern, resolved) + def test_missing_key_legacy(self): + """ + Requesting a missing key will still return None, but warn. + """ + md = metadata('distinfo-pkg') + with suppress_known_deprecation(): + assert md['does-not-exist'] is None + @staticmethod def _test_files(files): root = files[0].root From a6c6660d71fcd9f55d4ddbb4cd411ab34cc38ec9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 23 Dec 2022 19:55:26 -0500 Subject: [PATCH 026/131] Put tidelift docs dependency in its own section to limit merge conflicts. --- setup.cfg | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 798b1033..cdb0caa9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,5 @@ [options.extras_require] docs = - # upstream + + # tidelift jaraco.tidelift >= 1.4 From cd68fe5f36f79da1c9710211958308afd1c8bc69 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 1 Jan 2023 12:40:44 -0500 Subject: [PATCH 027/131] Remove test for 'new style classes', no longer relevant in a world where all classes are new style. --- tests/test_main.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index 514bfadf..b5545e54 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -9,7 +9,6 @@ from importlib_metadata import ( Distribution, EntryPoint, - MetadataPathFinder, PackageNotFoundError, _unique, distributions, @@ -44,10 +43,6 @@ def test_package_not_found_mentions_metadata(self): assert "metadata" in str(ctx.exception) - def test_new_style_classes(self): - self.assertIsInstance(Distribution, type) - self.assertIsInstance(MetadataPathFinder, type) - @fixtures.parameterize( dict(name=None), dict(name=''), From 4a4f062a5122d637cf0358cf05642655ccbafba6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 1 Jan 2023 12:41:23 -0500 Subject: [PATCH 028/131] Correct typo --- tests/test_main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_main.py b/tests/test_main.py index b5545e54..7b8d797f 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -34,7 +34,7 @@ def test_for_name_does_not_exist(self): def test_package_not_found_mentions_metadata(self): """ When a package is not found, that could indicate that the - packgae is not installed or that it is installed without + package is not installed or that it is installed without metadata. Ensure the exception mentions metadata to help guide users toward the cause. See #124. """ From 1bf8a7aec366ede912b9f9c989006cc358cf2cdc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 1 Jan 2023 12:10:08 -0500 Subject: [PATCH 029/131] Add xfail test capturing desired expectation. --- tests/test_main.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/test_main.py b/tests/test_main.py index 7b8d797f..cec82121 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -43,6 +43,11 @@ def test_package_not_found_mentions_metadata(self): assert "metadata" in str(ctx.exception) + def test_abc_enforced(self): + with self.assertRaises(AssertionError): # xfail + with self.assertRaises(TypeError): + type('DistributionSubclass', (Distribution,), {})() + @fixtures.parameterize( dict(name=None), dict(name=''), From 8a9d1699aa47d71da6fb385b8510bf95fed6e3e2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 1 Jan 2023 12:35:46 -0500 Subject: [PATCH 030/131] Add ABCMeta to Distribution. Fixes #419. --- importlib_metadata/__init__.py | 2 +- tests/test_main.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 26a1388c..9a36a8e6 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -346,7 +346,7 @@ def __repr__(self): return f'' -class Distribution: +class Distribution(metaclass=abc.ABCMeta): """A Python distribution package.""" @abc.abstractmethod diff --git a/tests/test_main.py b/tests/test_main.py index cec82121..f0f84983 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -44,9 +44,8 @@ def test_package_not_found_mentions_metadata(self): assert "metadata" in str(ctx.exception) def test_abc_enforced(self): - with self.assertRaises(AssertionError): # xfail - with self.assertRaises(TypeError): - type('DistributionSubclass', (Distribution,), {})() + with self.assertRaises(TypeError): + type('DistributionSubclass', (Distribution,), {})() @fixtures.parameterize( dict(name=None), From 2d52ecd7a581e97012830e7d18932408729d0e9a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 1 Jan 2023 12:39:00 -0500 Subject: [PATCH 031/131] Update changelog. Ref #419. --- CHANGES.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index a2df91a3..4dd9d5df 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,15 @@ +v6.0.0 +====== + +* #419: Declared ``Distribution`` as an abstract class, enforcing + definition of abstract methods in instantiated subclasses. It's no + longer possible to instantiate a ``Distribution`` or any subclasses + unless they define the abstract methods. + + Please comment in the issue if this change breaks any projects. + This change will likely be rolled back if it causes significant + disruption. + v5.2.0 ====== From eb2bdc83a7d3cfd1c2bc3aeae39a900d654a6839 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 2 Jan 2023 03:17:24 -0500 Subject: [PATCH 032/131] Update badge for 2023 --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 39459a4a..af0efb05 100644 --- a/README.rst +++ b/README.rst @@ -14,5 +14,5 @@ .. .. image:: https://readthedocs.org/projects/skeleton/badge/?version=latest .. :target: https://skeleton.readthedocs.io/en/latest/?badge=latest -.. image:: https://img.shields.io/badge/skeleton-2022-informational +.. image:: https://img.shields.io/badge/skeleton-2023-informational :target: https://blog.jaraco.com/skeleton From f9e01d2197d18b2b21976bae6e5b7f90b683bc4f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 18 Jan 2023 21:33:18 -0500 Subject: [PATCH 033/131] ALLOW_UNICODE no longer needed on Python 3. As a result, ELLIPSES is also now enabled by default. --- pytest.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/pytest.ini b/pytest.ini index 2c2817b8..1e6adf08 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,7 +1,6 @@ [pytest] norecursedirs=dist build .tox .eggs addopts=--doctest-modules -doctest_optionflags=ALLOW_UNICODE ELLIPSIS filterwarnings= # Ensure ResourceWarnings are emitted default::ResourceWarning From 284359e5123eb6a9f975092d1fb17dfa814d1594 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 27 Jan 2023 17:56:30 -0500 Subject: [PATCH 034/131] Enable default encoding warning where available. See PEP 597. --- tox.ini | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 42ae6852..5a678211 100644 --- a/tox.ini +++ b/tox.ini @@ -8,10 +8,13 @@ toxworkdir={env:TOX_WORK_DIR:.tox} [testenv] deps = +setenv = + PYTHONWARNDEFAULTENCODING = 1 commands = pytest {posargs} usedevelop = True -extras = testing +extras = + testing [testenv:docs] extras = From f18255faba76a6a86bf3fa6f73da9d974262aebd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 27 Jan 2023 18:19:23 -0500 Subject: [PATCH 035/131] Suppress EncodingWarning in pytest_black. Workaround for shopkeep/pytest-black#67. --- pytest.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pytest.ini b/pytest.ini index 1e6adf08..bd7d0b52 100644 --- a/pytest.ini +++ b/pytest.ini @@ -17,3 +17,6 @@ filterwarnings= ignore: is not using a cooperative constructor:pytest.PytestDeprecationWarning ignore:The \(fspath. py.path.local\) argument to Flake8Item is deprecated.:pytest.PytestDeprecationWarning ignore:Flake8Item is an Item subclass and should not be a collector:pytest.PytestWarning + + # shopkeep/pytest-black#67 + ignore:'encoding' argument not specified::pytest_black From 0d9c6f0f5b6182cdac448270dbc0529f91b50bd9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 27 Jan 2023 18:50:22 -0500 Subject: [PATCH 036/131] Exempt warning. Workaround for realpython/pytest-mypy#152 --- pytest.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pytest.ini b/pytest.ini index bd7d0b52..69d95b26 100644 --- a/pytest.ini +++ b/pytest.ini @@ -20,3 +20,6 @@ filterwarnings= # shopkeep/pytest-black#67 ignore:'encoding' argument not specified::pytest_black + + # realpython/pytest-mypy#152 + ignore:'encoding' argument not specified::pytest_mypy From 5f095d18d76f7ae36e57fa3241da341b0f9cd365 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 7 Feb 2023 09:54:15 -0500 Subject: [PATCH 037/131] Add #upstream markers for filtered warnings. Add filter for platform module (ref python/cpython#100750). --- pytest.ini | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pytest.ini b/pytest.ini index 69d95b26..5b6ddc45 100644 --- a/pytest.ini +++ b/pytest.ini @@ -2,6 +2,8 @@ norecursedirs=dist build .tox .eggs addopts=--doctest-modules filterwarnings= + ## upstream + # Ensure ResourceWarnings are emitted default::ResourceWarning @@ -23,3 +25,8 @@ filterwarnings= # realpython/pytest-mypy#152 ignore:'encoding' argument not specified::pytest_mypy + + # python/cpython#100750 + ignore::EncodingWarning:platform + + ## end upstream From 6f7ac885c61eb74df8c2db435cdbec412da06fe6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 9 Feb 2023 03:52:03 -0500 Subject: [PATCH 038/131] Remove reference to EncodingWarning as it doesn't exist on some Pythons. --- pytest.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest.ini b/pytest.ini index 5b6ddc45..99a25199 100644 --- a/pytest.ini +++ b/pytest.ini @@ -27,6 +27,6 @@ filterwarnings= ignore:'encoding' argument not specified::pytest_mypy # python/cpython#100750 - ignore::EncodingWarning:platform + ignore:'encoding' argument not specified::platform ## end upstream From 9650fc184fc120a21623d8f92d03ee4ccbaa89d8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 16 Feb 2023 16:43:22 -0500 Subject: [PATCH 039/131] Revert "exclude build env from cov reporting (jaraco/skeleton#60)" This reverts commit e719f86c138a750f0c4599cd01cb8067b1ca95c8. The issue seems to have been addressed somehow. Ref pytest-dev/pytest-cov#538. --- .coveragerc | 1 - 1 file changed, 1 deletion(-) diff --git a/.coveragerc b/.coveragerc index 01164f62..6a34e662 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,7 +2,6 @@ omit = # leading `*/` for pytest-dev/pytest-cov#456 */.tox/* - */pep517-build-env-* [report] show_missing = True From 56cdf46aa19450d58b4a56af6553a0225762ae4b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 16 Feb 2023 21:12:36 -0500 Subject: [PATCH 040/131] Disable couldnt-parse warnings. Prescribed workaround for nedbat/coveragepy#1392. Fixes python/importlib_resources#279 and fixes jaraco/skeleton#56. --- .coveragerc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.coveragerc b/.coveragerc index 6a34e662..02879483 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,6 +2,8 @@ omit = # leading `*/` for pytest-dev/pytest-cov#456 */.tox/* +disable_warnings = + couldnt-parse [report] show_missing = True From 56aad0ffe7f2e72500cc45f7e4ba6bee014364bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Fri, 10 Mar 2023 02:56:16 +0000 Subject: [PATCH 041/131] Sync PackageMetadata with email.message.Message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- importlib_metadata/_meta.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/importlib_metadata/_meta.py b/importlib_metadata/_meta.py index 259b15ba..6123e746 100644 --- a/importlib_metadata/_meta.py +++ b/importlib_metadata/_meta.py @@ -1,5 +1,5 @@ from ._compat import Protocol -from typing import Any, Dict, Iterator, List, TypeVar, Union +from typing import Any, Dict, Iterator, List, Optional, TypeVar, Union, overload _T = TypeVar("_T") @@ -18,7 +18,12 @@ def __getitem__(self, key: str) -> str: def __iter__(self) -> Iterator[str]: ... # pragma: no cover - def get_all(self, name: str, failobj: _T = ...) -> Union[List[Any], _T]: + @overload + def get_all(self, name: str, failobj: None = None) -> Optional[List[Any]]: + ... + + @overload + def get_all(self, name: str, failobj: _T) -> Union[List[Any], _T]: """ Return all values associated with a possibly multi-valued key. """ From 5475a6e3d96e1f31441c90386e8d70514ab0e954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Fri, 10 Mar 2023 02:36:44 +0000 Subject: [PATCH 042/131] Fix formatting in test_api MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- tests/test_api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_api.py b/tests/test_api.py index 504d0553..2932c2d2 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -32,7 +32,6 @@ class APITests( fixtures.EggInfoFile, unittest.TestCase, ): - version_pattern = r'\d+\.\d+(\.\d)?' def test_retrieves_version_of_self(self): From fed3a41fa037ccf0f601b89a899ce6f22ff266af Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Fri, 10 Mar 2023 14:26:46 +0100 Subject: [PATCH 043/131] tests/fixtures: Fix FilesDef type to include bytes values The build_files() helper which handles these FilesDef nested dicts already has code to handle values of type 'bytes' (they get written verbatim instead of first being processed by DALS()). Changing the FilesDef type to reflect this prevents Mypy from failing when encountering legitimate bytes values in these dicts. --- tests/fixtures.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index 6d9a9d2b..080add19 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -83,8 +83,10 @@ def setUp(self): # Except for python/mypy#731, prefer to define -# FilesDef = Dict[str, Union['FilesDef', str]] -FilesDef = Dict[str, Union[Dict[str, Union[Dict[str, str], str]], str]] +# FilesDef = Dict[str, Union['FilesDef', str, bytes]] +FilesDef = Dict[ + str, Union[Dict[str, Union[Dict[str, Union[str, bytes]], str, bytes]], str, bytes] +] class DistInfoPkg(OnSysPath, SiteDir): From 578322a37d6b26acf8cede13e327dce105f3577d Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Fri, 10 Mar 2023 14:45:19 +0100 Subject: [PATCH 044/131] Add tests for egg-info package with no installed modules This corresponds to the qiskit[1] meta-package which: - does not contain any (runtime) Python code itself, but serves as a mechanism to install its transitive dependencies (which populate the qiskit package namespace). - is distributed as a source archive. - includes a top_level.txt which is empty (contains a single newline), arguably correct given that it does not directly install any importable packages/modules. - when installed as an egg, provides a SOURCES.txt which is incorrect from a runtime POV: it references 3 .py files, a setup.py and two files under test/, none of which are actually installed. - when installed (as an egg) by pip, provides an installed-files.txt file which is _more_ accurate than SOURCES.txt, since it reflects the files that are actually available after installation. importlib_metadata reports incorrect .files for this package, because we end up using SOURCES.txt. It is better to use installed-files.txt when it is available. Furthermore, as a result of this, packages_distributions() also incorrectly reports that this packages provides imports names that do not actually exist ("setup" and "test", in qiskit's case). This commit adds EggInfoPkgPipInstalledNoModules, a test project that mimics the egg installation of qiskit, and adds it to existing test cases, as well as adding a new test cases specifically for verifying packages_distributions() with egg-info packages. The following tests fail in this commit, but will be fixed in the next commit: - PackagesDistributionsTest.test_packages_distributions_on_eggs - APITests.test_files_egg_info See the python/importlib_metadata#115 issue for more details. [1]: qiskit is found at https://pypi.org/project/qiskit/0.41.1/#files --- tests/fixtures.py | 30 ++++++++++++++++++++++++++++++ tests/test_api.py | 30 ++++++++++++++++++++++-------- tests/test_main.py | 35 +++++++++++++++++++++++++++++++++-- 3 files changed, 85 insertions(+), 10 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index 080add19..aa6ffac9 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -213,6 +213,36 @@ def setUp(self): build_files(EggInfoPkg.files, prefix=self.site_dir) +class EggInfoPkgPipInstalledNoModules(OnSysPath, SiteDir): + files: FilesDef = { + "empty_egg_pkg.egg-info": { + "PKG-INFO": "Name: empty_egg-pkg", + # SOURCES.txt is made from the source archive, and contains files + # (setup.py) that are not present after installation. + "SOURCES.txt": """ + setup.py + empty_egg_pkg.egg-info/PKG-INFO + empty_egg_pkg.egg-info/SOURCES.txt + empty_egg_pkg.egg-info/top_level.txt + """, + # installed-files.txt is written by pip, and is a strictly more + # accurate source than SOURCES.txt as to the installed contents of + # the package. + "installed-files.txt": """ + PKG-INFO + SOURCES.txt + top_level.txt + """, + # top_level.txt correctly reflects that no modules are installed + "top_level.txt": b"\n", + }, + } + + def setUp(self): + super().setUp() + build_files(EggInfoPkgPipInstalledNoModules.files, prefix=self.site_dir) + + class EggInfoFile(OnSysPath, SiteDir): files: FilesDef = { "egginfo_file.egg-info": """ diff --git a/tests/test_api.py b/tests/test_api.py index 504d0553..3bf4a41f 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -27,12 +27,12 @@ def suppress_known_deprecation(): class APITests( fixtures.EggInfoPkg, + fixtures.EggInfoPkgPipInstalledNoModules, fixtures.DistInfoPkg, fixtures.DistInfoPkgWithDot, fixtures.EggInfoFile, unittest.TestCase, ): - version_pattern = r'\d+\.\d+(\.\d)?' def test_retrieves_version_of_self(self): @@ -63,15 +63,28 @@ def test_prefix_not_matched(self): distribution(prefix) def test_for_top_level(self): - self.assertEqual( - distribution('egginfo-pkg').read_text('top_level.txt').strip(), 'mod' - ) + tests = [ + ('egginfo-pkg', 'mod'), + ('empty_egg-pkg', ''), + ] + for pkg_name, expect_content in tests: + with self.subTest(pkg_name): + self.assertEqual( + distribution(pkg_name).read_text('top_level.txt').strip(), + expect_content, + ) def test_read_text(self): - top_level = [ - path for path in files('egginfo-pkg') if path.name == 'top_level.txt' - ][0] - self.assertEqual(top_level.read_text(), 'mod\n') + tests = [ + ('egginfo-pkg', 'mod\n'), + ('empty_egg-pkg', '\n'), + ] + for pkg_name, expect_content in tests: + with self.subTest(pkg_name): + top_level = [ + path for path in files(pkg_name) if path.name == 'top_level.txt' + ][0] + self.assertEqual(top_level.read_text(), expect_content) def test_entry_points(self): eps = entry_points() @@ -171,6 +184,7 @@ def test_files_dist_info(self): def test_files_egg_info(self): self._test_files(files('egginfo-pkg')) + self._test_files(files('empty_egg-pkg')) def test_version_egg_info_file(self): self.assertEqual(version('egginfo-file'), '0.1') diff --git a/tests/test_main.py b/tests/test_main.py index f0f84983..4d28fa26 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -170,11 +170,17 @@ def test_metadata_loads_egg_info(self): assert meta['Description'] == 'pôrˈtend' -class DiscoveryTests(fixtures.EggInfoPkg, fixtures.DistInfoPkg, unittest.TestCase): +class DiscoveryTests( + fixtures.EggInfoPkg, + fixtures.EggInfoPkgPipInstalledNoModules, + fixtures.DistInfoPkg, + unittest.TestCase, +): def test_package_discovery(self): dists = list(distributions()) assert all(isinstance(dist, Distribution) for dist in dists) assert any(dist.metadata['Name'] == 'egginfo-pkg' for dist in dists) + assert any(dist.metadata['Name'] == 'empty_egg-pkg' for dist in dists) assert any(dist.metadata['Name'] == 'distinfo-pkg' for dist in dists) def test_invalid_usage(self): @@ -304,7 +310,11 @@ def test_packages_distributions_example2(self): class PackagesDistributionsTest( - fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase + fixtures.EggInfoPkg, + fixtures.EggInfoPkgPipInstalledNoModules, + fixtures.OnSysPath, + fixtures.SiteDir, + unittest.TestCase, ): def test_packages_distributions_neither_toplevel_nor_files(self): """ @@ -322,3 +332,24 @@ def test_packages_distributions_neither_toplevel_nor_files(self): prefix=self.site_dir, ) packages_distributions() + + def test_packages_distributions_on_eggs(self): + """ + Test old-style egg packages with a variation of 'top_level.txt', + 'SOURCES.txt', and 'installed-files.txt', available. + """ + distributions = packages_distributions() + + def import_names_from_package(package_name): + return { + import_name + for import_name, package_names in distributions.items() + if package_name in package_names + } + + # egginfo-pkg declares one import ('mod') via top_level.txt + assert import_names_from_package('egginfo-pkg') == {'mod'} + + # empty_egg-pkg should not be associated with any import names + # (top_level.txt is empty, and installed-files.txt has no .py files) + assert import_names_from_package('empty_egg-pkg') == set() From 61b0f297960d678d260f31319e7d53584d901e36 Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Fri, 10 Mar 2023 14:45:19 +0100 Subject: [PATCH 045/131] Distribution.files: Prefer *.egg-info/installed-files.txt to SOURCES.txt When listing the files in a *.egg-info distribution, prefer using *.egg-info/installed-files.txt instead of *.egg-info/SOURCES.txt. installed-files.txt is written by pip[1] when installing a package, whereas the SOURCES.txt is written by setuptools when creating a source archive[2]. installed-files.txt is only present when the package has been installed by pip, so we cannot depend on it always being available. However, when it _is_ available, it is an accurate record of what files are installed. SOURCES.txt, on the other hand, is always avaiable, but is not always accurate: Since it is generated from the source archive, it will often include files (like 'setup.py') that are no longer available after the package has been installed. Fixes #115 for the cases where a installed-files.txt file is available. [1]: https://pip.pypa.io/en/stable/news/#v0-3 [2]: https://setuptools.pypa.io/en/latest/deprecated/python_eggs.html#sources-txt-source-files-manifest --- importlib_metadata/__init__.py | 37 ++++++++++++++++++++++++++++++---- 1 file changed, 33 insertions(+), 4 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 9a36a8e6..3b0d8247 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -460,8 +460,8 @@ def files(self): :return: List of PackagePath for this distribution or None Result is `None` if the metadata file that enumerates files - (i.e. RECORD for dist-info or SOURCES.txt for egg-info) is - missing. + (i.e. RECORD for dist-info, or installed-files.txt or + SOURCES.txt for egg-info) is missing. Result may be empty if the metadata exists but is empty. """ @@ -476,7 +476,11 @@ def make_file(name, hash=None, size_str=None): def make_files(lines): return list(starmap(make_file, csv.reader(lines))) - return make_files(self._read_files_distinfo() or self._read_files_egginfo()) + return make_files( + self._read_files_distinfo() + or self._read_files_egginfo_installed() + or self._read_files_egginfo_sources() + ) def _read_files_distinfo(self): """ @@ -485,10 +489,35 @@ def _read_files_distinfo(self): text = self.read_text('RECORD') return text and text.splitlines() - def _read_files_egginfo(self): + def _read_files_egginfo_installed(self): + """ + installed-files.txt might contain literal commas, so wrap + each line in quotes. Also, the entries in installed-files.txt + are relative to the .egg-info/ subdir (not relative to the + parent site-packages directory that make_file() expects). + + This file is written when the package is installed by pip, + but it might not be written for other installation methods. + Hence, even if we can assume that this file is accurate + when it exists, we cannot assume that it always exists. + """ + text = self.read_text('installed-files.txt') + # We need to prepend the .egg-info/ subdir to the lines in this file. + # But this subdir is only available in the PathDistribution's self._path + # which is not easily accessible from this base class... + subdir = getattr(self, '_path', None) + return text and subdir and [f'"{subdir}/{line}"' for line in text.splitlines()] + + def _read_files_egginfo_sources(self): """ SOURCES.txt might contain literal commas, so wrap each line in quotes. + + Note that SOURCES.txt is not a reliable source for what + files are installed by a package. This file is generated + for a source archive, and the files that are present + there (e.g. setup.py) may not correctly reflect the files + that are present after the package has been installed. """ text = self.read_text('SOURCES.txt') return text and map('"{}"'.format, text.splitlines()) From 8026db2b63274af61d32007c3d7e8fc8b57bdd26 Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Fri, 10 Mar 2023 14:45:19 +0100 Subject: [PATCH 046/131] Add tests for egg-info package with .files from inaccurate SOURCES.txt As established in previous commits, the SOURCES.txt file is not always an accurate source of files that are present after a package has been installed. One situation where this inaccuracy is problematic is when top_level.txt is also missing, and packages_distributions() is forced to infer the provided import names based on Distribution.files. In this situation we end up with incorrect mappings between import packages and distribution packages, including import packages that clearly do not exist at all. For example, a SOURCES.txt that lists setup.py (which is used _when_ installing, but is not available after installation), will see that setup.py returned from .files, which then will cause packages_distributions() to claim a mapping from the non-existent 'setup' import name to this distribution. This commit adds EggInfoPkgSourcesFallback which demostrates such a scenario, and adds this new class to a couple of relevant tests. A couple of these tests are currently failing, to demonstrate the issue at hand. These test failures will be fixed in the next commit. See the python/importlib_metadata#115 issue for more details. --- tests/fixtures.py | 26 ++++++++++++++++++++++++++ tests/test_api.py | 2 ++ tests/test_main.py | 7 +++++++ 3 files changed, 35 insertions(+) diff --git a/tests/fixtures.py b/tests/fixtures.py index aa6ffac9..bbd9854b 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -243,6 +243,32 @@ def setUp(self): build_files(EggInfoPkgPipInstalledNoModules.files, prefix=self.site_dir) +class EggInfoPkgSourcesFallback(OnSysPath, SiteDir): + files: FilesDef = { + "starved_egg_pkg.egg-info": { + "PKG-INFO": "Name: starved_egg-pkg", + # SOURCES.txt is made from the source archive, and contains files + # (setup.py) that are not present after installation. + "SOURCES.txt": """ + starved_egg_pkg.py + setup.py + starved_egg_pkg.egg-info/PKG-INFO + starved_egg_pkg.egg-info/SOURCES.txt + """, + # missing installed-files.txt (i.e. not installed by pip) + # missing top_level.txt + }, + "starved_egg_pkg.py": """ + def main(): + print("hello world") + """, + } + + def setUp(self): + super().setUp() + build_files(EggInfoPkgSourcesFallback.files, prefix=self.site_dir) + + class EggInfoFile(OnSysPath, SiteDir): files: FilesDef = { "egginfo_file.egg-info": """ diff --git a/tests/test_api.py b/tests/test_api.py index 3bf4a41f..1f0f79ab 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -28,6 +28,7 @@ def suppress_known_deprecation(): class APITests( fixtures.EggInfoPkg, fixtures.EggInfoPkgPipInstalledNoModules, + fixtures.EggInfoPkgSourcesFallback, fixtures.DistInfoPkg, fixtures.DistInfoPkgWithDot, fixtures.EggInfoFile, @@ -185,6 +186,7 @@ def test_files_dist_info(self): def test_files_egg_info(self): self._test_files(files('egginfo-pkg')) self._test_files(files('empty_egg-pkg')) + self._test_files(files('starved_egg-pkg')) def test_version_egg_info_file(self): self.assertEqual(version('egginfo-file'), '0.1') diff --git a/tests/test_main.py b/tests/test_main.py index 4d28fa26..e08a7609 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -173,6 +173,7 @@ def test_metadata_loads_egg_info(self): class DiscoveryTests( fixtures.EggInfoPkg, fixtures.EggInfoPkgPipInstalledNoModules, + fixtures.EggInfoPkgSourcesFallback, fixtures.DistInfoPkg, unittest.TestCase, ): @@ -181,6 +182,7 @@ def test_package_discovery(self): assert all(isinstance(dist, Distribution) for dist in dists) assert any(dist.metadata['Name'] == 'egginfo-pkg' for dist in dists) assert any(dist.metadata['Name'] == 'empty_egg-pkg' for dist in dists) + assert any(dist.metadata['Name'] == 'starved_egg-pkg' for dist in dists) assert any(dist.metadata['Name'] == 'distinfo-pkg' for dist in dists) def test_invalid_usage(self): @@ -312,6 +314,7 @@ def test_packages_distributions_example2(self): class PackagesDistributionsTest( fixtures.EggInfoPkg, fixtures.EggInfoPkgPipInstalledNoModules, + fixtures.EggInfoPkgSourcesFallback, fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase, @@ -353,3 +356,7 @@ def import_names_from_package(package_name): # empty_egg-pkg should not be associated with any import names # (top_level.txt is empty, and installed-files.txt has no .py files) assert import_names_from_package('empty_egg-pkg') == set() + + # starved_egg-pkg has one import ('starved_egg_pkg') inferred + # from SOURCES.txt (top_level.txt is missing) + assert import_names_from_package('starved_egg-pkg') == {'starved_egg_pkg'} From 22d9ea5e307412c0e46c6cb90db54ec082d1d536 Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Fri, 10 Mar 2023 14:45:19 +0100 Subject: [PATCH 047/131] Distribution.files: Only return files that actually exist Add an extra filter on the paths returned from Distribution.files, to prevent paths that don't exist on the filesystem from being returned. This attempts to solve the issue of .files returning incorrect information based on the inaccuracies of SOURCES.txt. As the code currently is organized, it is more complicated to write this such that it only applies to the information read from SOURCES.txt specifically, hence we apply it to _all_ of .files instead. This fixes #115, also in the case where there is no installed-files.txt file available. [1]: https://pip.pypa.io/en/stable/news/#v0-3 [2]: https://setuptools.pypa.io/en/latest/deprecated/python_eggs.html#sources-txt-source-files-manifest --- importlib_metadata/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 3b0d8247..773b8fc1 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -474,7 +474,12 @@ def make_file(name, hash=None, size_str=None): @pass_none def make_files(lines): - return list(starmap(make_file, csv.reader(lines))) + return list( + filter( + lambda package_path: package_path.locate().exists(), + list(starmap(make_file, csv.reader(lines))), + ) + ) return make_files( self._read_files_distinfo() From fb364de47af2f12b93f4ab6c156a268aceded81e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 18 Mar 2023 11:49:06 -0400 Subject: [PATCH 048/131] Add comment to link rationale for existence. --- importlib_metadata/_meta.py | 1 + 1 file changed, 1 insertion(+) diff --git a/importlib_metadata/_meta.py b/importlib_metadata/_meta.py index 6123e746..d6ba1c30 100644 --- a/importlib_metadata/_meta.py +++ b/importlib_metadata/_meta.py @@ -18,6 +18,7 @@ def __getitem__(self, key: str) -> str: def __iter__(self) -> Iterator[str]: ... # pragma: no cover + # overload per python/importlib_metadata#435 @overload def get_all(self, name: str, failobj: None = None) -> Optional[List[Any]]: ... From 51b89b07d828cf0e2137743a61672a1b57f88c73 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 18 Mar 2023 11:49:50 -0400 Subject: [PATCH 049/131] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins?= =?UTF-8?q?=20(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- importlib_metadata/_meta.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/importlib_metadata/_meta.py b/importlib_metadata/_meta.py index d6ba1c30..8621c680 100644 --- a/importlib_metadata/_meta.py +++ b/importlib_metadata/_meta.py @@ -21,7 +21,7 @@ def __iter__(self) -> Iterator[str]: # overload per python/importlib_metadata#435 @overload def get_all(self, name: str, failobj: None = None) -> Optional[List[Any]]: - ... + ... # pragma: no cover @overload def get_all(self, name: str, failobj: _T) -> Union[List[Any], _T]: From ac0949adceae61c7cf31c009635e02952cccc4b0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 18 Mar 2023 11:51:35 -0400 Subject: [PATCH 050/131] Update changelog. --- CHANGES.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 4dd9d5df..8eca4dfb 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,10 @@ +v6.0.1 +====== + +* #434: Expand protocol for ``PackageMetadata.get_all`` to match + the upstream implementation of ``email.message.Message.get_all`` + in python/typeshed#9620. + v6.0.0 ====== From a046b04cd628eff4b21a0e6e77d3f9a2672e311b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 18 Mar 2023 12:07:51 -0400 Subject: [PATCH 051/131] Extend the workaround to satisfy docstring of typing._overload_dummy. --- docs/conf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index ebfdb74d..8e7762d0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -61,9 +61,11 @@ ), ) -# Workaround for #316 nitpick_ignore = [ + # Workaround for #316 ('py:class', 'importlib_metadata.EntryPoints'), ('py:class', 'importlib_metadata.SelectableGroups'), ('py:class', 'importlib_metadata._meta._T'), + # Workaround for #435 + ('py:class', '_T'), ] From 109f8c09ddb4904dc3f83307473520b2250ccb30 Mon Sep 17 00:00:00 2001 From: Joyce Date: Sat, 18 Mar 2023 13:25:16 -0300 Subject: [PATCH 052/131] Feat: initial permissions to main.yml (jaraco/skeleton#76) Signed-off-by: Joyce --- .github/workflows/main.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9629a26a..3fa1c81e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,6 +2,9 @@ name: tests on: [push, pull_request] +permissions: + contents: read + env: # Environment variables to support color support (jaraco/skeleton#66): # Request colored output from CLI tools supporting it. Different tools @@ -104,6 +107,8 @@ jobs: jobs: ${{ toJSON(needs) }} release: + permissions: + contents: write needs: - check if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') From 56aee0332f3a5a637b9de97a68f5b173f4ee8549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= Date: Fri, 10 Mar 2023 00:57:43 +0000 Subject: [PATCH 053/131] Add missing modules to packages_distributions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Filipe Laíns --- importlib_metadata/__init__.py | 14 +++++++++----- tests/test_main.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 9a36a8e6..9428e7d0 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -13,6 +13,7 @@ import itertools import posixpath import collections +import inspect from . import _adapters, _meta, _py39compat from ._collections import FreezableDefaultDict, Pair @@ -897,8 +898,11 @@ def _top_level_declared(dist): def _top_level_inferred(dist): - return { - f.parts[0] if len(f.parts) > 1 else f.with_suffix('').name - for f in always_iterable(dist.files) - if f.suffix == ".py" - } + return filter( + None, + { + # this logic relies on the assumption that dist.files only contains files (not directories) + inspect.getmodulename(f) if len(f.parts) == 1 else f.parts[0] + for f in always_iterable(dist.files) + }, + ) diff --git a/tests/test_main.py b/tests/test_main.py index f0f84983..16367793 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -322,3 +322,34 @@ def test_packages_distributions_neither_toplevel_nor_files(self): prefix=self.site_dir, ) packages_distributions() + + def test_packages_distributions_all_module_types(self): + """ + Test top-level modules detected on a package without 'top-level.txt'. + """ + suffixes = importlib.machinery.all_suffixes() + fixtures.build_files( + { + 'all_distributions-1.0.0.dist-info': { + 'METADATA': """ + Name: all_distributions + Version: 1.0.0 + """, + 'RECORD': ''.join( + f'{i}-top-level{suffix},,\n' + f'{i}-in-namespace/mod{suffix},,\n' + f'{i}-in-package/__init__.py,,\n' + f'{i}-in-package/mod{suffix},,\n' + for i, suffix in enumerate(suffixes) + ), + }, + }, + prefix=self.site_dir, + ) + + distributions = packages_distributions() + + for i in range(len(suffixes)): + assert distributions[f'{i}-top-level'] == ['all_distributions'] + assert distributions[f'{i}-in-namespace'] == ['all_distributions'] + assert distributions[f'{i}-in-package'] == ['all_distributions'] From a3c066ddd3c0780c6e64364f1917b06b9795b330 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 18 Mar 2023 12:49:27 -0400 Subject: [PATCH 054/131] Remove long-line comment. --- importlib_metadata/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 9428e7d0..1d300ddb 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -901,7 +901,6 @@ def _top_level_inferred(dist): return filter( None, { - # this logic relies on the assumption that dist.files only contains files (not directories) inspect.getmodulename(f) if len(f.parts) == 1 else f.parts[0] for f in always_iterable(dist.files) }, From 6610368e5952f2780f347b9c8af060f9984e0846 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 18 Mar 2023 12:52:20 -0400 Subject: [PATCH 055/131] Extract variable for optional names. --- importlib_metadata/__init__.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 1d300ddb..656a79dc 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -898,10 +898,8 @@ def _top_level_declared(dist): def _top_level_inferred(dist): - return filter( - None, - { - inspect.getmodulename(f) if len(f.parts) == 1 else f.parts[0] - for f in always_iterable(dist.files) - }, - ) + opt_names = { + inspect.getmodulename(f) if len(f.parts) == 1 else f.parts[0] + for f in always_iterable(dist.files) + } + return filter(None, opt_names) From df7824b2ca587b073b08d25e306a68b5d61a960c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 18 Mar 2023 12:54:34 -0400 Subject: [PATCH 056/131] Restore logic for parts. --- CHANGES.rst | 7 +++++++ importlib_metadata/__init__.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 8eca4dfb..eccdd5ba 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,10 @@ +v6.1.0 +====== + +* #428: ``packages_distributions`` now honors packages and modules + with Python modules that not ``.py`` sources (e.g. ``.pyc``, + ``.so``). + v6.0.1 ====== diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 656a79dc..8d9f0016 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -899,7 +899,7 @@ def _top_level_declared(dist): def _top_level_inferred(dist): opt_names = { - inspect.getmodulename(f) if len(f.parts) == 1 else f.parts[0] + f.parts[0] if len(f.parts) > 1 else inspect.getmodulename(f) for f in always_iterable(dist.files) } return filter(None, opt_names) From b391f77d078b461da972226dbf6f0649e78eb5db Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Sun, 19 Mar 2023 02:06:58 +0100 Subject: [PATCH 057/131] squash! Add tests for egg-info package with no installed modules Move test_packages_distributions_on_eggs() method into a new class, PackagesDistributionsEggTest, to prevent applying unnecessary fixtures to existing tests. --- tests/test_main.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index d796808c..4c4b7b76 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -312,12 +312,7 @@ def test_packages_distributions_example2(self): class PackagesDistributionsTest( - fixtures.EggInfoPkg, - fixtures.EggInfoPkgPipInstalledNoModules, - fixtures.EggInfoPkgSourcesFallback, - fixtures.OnSysPath, - fixtures.SiteDir, - unittest.TestCase, + fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase ): def test_packages_distributions_neither_toplevel_nor_files(self): """ @@ -367,6 +362,13 @@ def test_packages_distributions_all_module_types(self): assert distributions[f'{i}-in-namespace'] == ['all_distributions'] assert distributions[f'{i}-in-package'] == ['all_distributions'] + +class PackagesDistributionsEggTest( + fixtures.EggInfoPkg, + fixtures.EggInfoPkgPipInstalledNoModules, + fixtures.EggInfoPkgSourcesFallback, + unittest.TestCase, +): def test_packages_distributions_on_eggs(self): """ Test old-style egg packages with a variation of 'top_level.txt', From 110f00d24304fdf446a640513f6bf7aef18ba9c3 Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Sun, 19 Mar 2023 02:49:43 +0100 Subject: [PATCH 058/131] Add test case demonstrating inferring module names from installed-files.txt --- tests/fixtures.py | 35 +++++++++++++++++++++++++++++++++++ tests/test_api.py | 2 ++ tests/test_main.py | 7 +++++++ 3 files changed, 44 insertions(+) diff --git a/tests/fixtures.py b/tests/fixtures.py index bbd9854b..0423c29d 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -213,6 +213,41 @@ def setUp(self): build_files(EggInfoPkg.files, prefix=self.site_dir) +class EggInfoPkgPipInstalledNoToplevel(OnSysPath, SiteDir): + files: FilesDef = { + "egg_with_module_pkg.egg-info": { + "PKG-INFO": "Name: egg_with_module-pkg", + # SOURCES.txt is made from the source archive, and contains files + # (setup.py) that are not present after installation. + "SOURCES.txt": """ + egg_with_module.py + setup.py + egg_with_module_pkg.egg-info/PKG-INFO + egg_with_module_pkg.egg-info/SOURCES.txt + egg_with_module_pkg.egg-info/top_level.txt + """, + # installed-files.txt is written by pip, and is a strictly more + # accurate source than SOURCES.txt as to the installed contents of + # the package. + "installed-files.txt": """ + ../egg_with_module.py + PKG-INFO + SOURCES.txt + top_level.txt + """, + # missing top_level.txt (to trigger fallback to installed-files.txt) + }, + "egg_with_module.py": """ + def main(): + print("hello world") + """ + } + + def setUp(self): + super().setUp() + build_files(EggInfoPkgPipInstalledNoToplevel.files, prefix=self.site_dir) + + class EggInfoPkgPipInstalledNoModules(OnSysPath, SiteDir): files: FilesDef = { "empty_egg_pkg.egg-info": { diff --git a/tests/test_api.py b/tests/test_api.py index 1f0f79ab..984c7707 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -27,6 +27,7 @@ def suppress_known_deprecation(): class APITests( fixtures.EggInfoPkg, + fixtures.EggInfoPkgPipInstalledNoToplevel, fixtures.EggInfoPkgPipInstalledNoModules, fixtures.EggInfoPkgSourcesFallback, fixtures.DistInfoPkg, @@ -185,6 +186,7 @@ def test_files_dist_info(self): def test_files_egg_info(self): self._test_files(files('egginfo-pkg')) + self._test_files(files('egg_with_module-pkg')) self._test_files(files('empty_egg-pkg')) self._test_files(files('starved_egg-pkg')) diff --git a/tests/test_main.py b/tests/test_main.py index 4c4b7b76..c67df8be 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -172,6 +172,7 @@ def test_metadata_loads_egg_info(self): class DiscoveryTests( fixtures.EggInfoPkg, + fixtures.EggInfoPkgPipInstalledNoToplevel, fixtures.EggInfoPkgPipInstalledNoModules, fixtures.EggInfoPkgSourcesFallback, fixtures.DistInfoPkg, @@ -181,6 +182,7 @@ def test_package_discovery(self): dists = list(distributions()) assert all(isinstance(dist, Distribution) for dist in dists) assert any(dist.metadata['Name'] == 'egginfo-pkg' for dist in dists) + assert any(dist.metadata['Name'] == 'egg_with_module-pkg' for dist in dists) assert any(dist.metadata['Name'] == 'empty_egg-pkg' for dist in dists) assert any(dist.metadata['Name'] == 'starved_egg-pkg' for dist in dists) assert any(dist.metadata['Name'] == 'distinfo-pkg' for dist in dists) @@ -365,6 +367,7 @@ def test_packages_distributions_all_module_types(self): class PackagesDistributionsEggTest( fixtures.EggInfoPkg, + fixtures.EggInfoPkgPipInstalledNoToplevel, fixtures.EggInfoPkgPipInstalledNoModules, fixtures.EggInfoPkgSourcesFallback, unittest.TestCase, @@ -386,6 +389,10 @@ def import_names_from_package(package_name): # egginfo-pkg declares one import ('mod') via top_level.txt assert import_names_from_package('egginfo-pkg') == {'mod'} + # egg_with_module-pkg has one import ('egg_with_module') inferred from + # installed-files.txt (top_level.txt is missing) + assert import_names_from_package('egg_with_module-pkg') == {'egg_with_module'} + # empty_egg-pkg should not be associated with any import names # (top_level.txt is empty, and installed-files.txt has no .py files) assert import_names_from_package('empty_egg-pkg') == set() From eeb2ed1593e25e4be3c7da8db073260a4ae8ec1e Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Sun, 19 Mar 2023 02:50:04 +0100 Subject: [PATCH 059/131] Fix issues with inferring module names from installed-files.txt --- importlib_metadata/__init__.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index c60a3582..dd7839db 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -512,7 +512,16 @@ def _read_files_egginfo_installed(self): # But this subdir is only available in the PathDistribution's self._path # which is not easily accessible from this base class... subdir = getattr(self, '_path', None) - return text and subdir and [f'"{subdir}/{line}"' for line in text.splitlines()] + try: + if text and subdir: + ret = [ + str((subdir / line).resolve().relative_to(self.locate_file(''))) + for line in text.splitlines() + ] + return map('"{}"'.format, ret) + except Exception: + pass + return None def _read_files_egginfo_sources(self): """ From a2dc88a299d6cb83052d91928b12cde0a6e5a2de Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Sun, 19 Mar 2023 02:51:27 +0100 Subject: [PATCH 060/131] squash! Add tests for egg-info package with .files from inaccurate SOURCES.txt Rename starved_egg to sources_fallback. --- tests/fixtures.py | 16 ++++++++-------- tests/test_api.py | 2 +- tests/test_main.py | 8 ++++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index 0423c29d..36c756f0 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -280,20 +280,20 @@ def setUp(self): class EggInfoPkgSourcesFallback(OnSysPath, SiteDir): files: FilesDef = { - "starved_egg_pkg.egg-info": { - "PKG-INFO": "Name: starved_egg-pkg", + "sources_fallback_pkg.egg-info": { + "PKG-INFO": "Name: sources_fallback-pkg", # SOURCES.txt is made from the source archive, and contains files # (setup.py) that are not present after installation. "SOURCES.txt": """ - starved_egg_pkg.py + sources_fallback.py setup.py - starved_egg_pkg.egg-info/PKG-INFO - starved_egg_pkg.egg-info/SOURCES.txt + sources_fallback_pkg.egg-info/PKG-INFO + sources_fallback_pkg.egg-info/SOURCES.txt """, - # missing installed-files.txt (i.e. not installed by pip) - # missing top_level.txt + # missing installed-files.txt (i.e. not installed by pip) and + # missing top_level.txt (to trigger fallback to SOURCES.txt) }, - "starved_egg_pkg.py": """ + "sources_fallback.py": """ def main(): print("hello world") """, diff --git a/tests/test_api.py b/tests/test_api.py index 984c7707..0c56dda9 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -188,7 +188,7 @@ def test_files_egg_info(self): self._test_files(files('egginfo-pkg')) self._test_files(files('egg_with_module-pkg')) self._test_files(files('empty_egg-pkg')) - self._test_files(files('starved_egg-pkg')) + self._test_files(files('sources_fallback-pkg')) def test_version_egg_info_file(self): self.assertEqual(version('egginfo-file'), '0.1') diff --git a/tests/test_main.py b/tests/test_main.py index c67df8be..a0f80f02 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -184,7 +184,7 @@ def test_package_discovery(self): assert any(dist.metadata['Name'] == 'egginfo-pkg' for dist in dists) assert any(dist.metadata['Name'] == 'egg_with_module-pkg' for dist in dists) assert any(dist.metadata['Name'] == 'empty_egg-pkg' for dist in dists) - assert any(dist.metadata['Name'] == 'starved_egg-pkg' for dist in dists) + assert any(dist.metadata['Name'] == 'sources_fallback-pkg' for dist in dists) assert any(dist.metadata['Name'] == 'distinfo-pkg' for dist in dists) def test_invalid_usage(self): @@ -397,6 +397,6 @@ def import_names_from_package(package_name): # (top_level.txt is empty, and installed-files.txt has no .py files) assert import_names_from_package('empty_egg-pkg') == set() - # starved_egg-pkg has one import ('starved_egg_pkg') inferred - # from SOURCES.txt (top_level.txt is missing) - assert import_names_from_package('starved_egg-pkg') == {'starved_egg_pkg'} + # sources_fallback-pkg has one import ('sources_fallback') inferred from + # SOURCES.txt (top_level.txt and installed-files.txt is missing) + assert import_names_from_package('sources_fallback-pkg') == {'sources_fallback'} From f62bf95d921fd8aca553813533f110d9010c2412 Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Sun, 19 Mar 2023 02:59:23 +0100 Subject: [PATCH 061/131] squash! Add tests for egg-info package with no installed modules Rename empty_egg to egg_with_no_modules --- tests/fixtures.py | 10 +++++----- tests/test_api.py | 6 +++--- tests/test_main.py | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index 36c756f0..6c589b40 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -250,15 +250,15 @@ def setUp(self): class EggInfoPkgPipInstalledNoModules(OnSysPath, SiteDir): files: FilesDef = { - "empty_egg_pkg.egg-info": { - "PKG-INFO": "Name: empty_egg-pkg", + "egg_with_no_modules_pkg.egg-info": { + "PKG-INFO": "Name: egg_with_no_modules-pkg", # SOURCES.txt is made from the source archive, and contains files # (setup.py) that are not present after installation. "SOURCES.txt": """ setup.py - empty_egg_pkg.egg-info/PKG-INFO - empty_egg_pkg.egg-info/SOURCES.txt - empty_egg_pkg.egg-info/top_level.txt + egg_with_no_modules_pkg.egg-info/PKG-INFO + egg_with_no_modules_pkg.egg-info/SOURCES.txt + egg_with_no_modules_pkg.egg-info/top_level.txt """, # installed-files.txt is written by pip, and is a strictly more # accurate source than SOURCES.txt as to the installed contents of diff --git a/tests/test_api.py b/tests/test_api.py index 0c56dda9..e18ceaad 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -67,7 +67,7 @@ def test_prefix_not_matched(self): def test_for_top_level(self): tests = [ ('egginfo-pkg', 'mod'), - ('empty_egg-pkg', ''), + ('egg_with_no_modules-pkg', ''), ] for pkg_name, expect_content in tests: with self.subTest(pkg_name): @@ -79,7 +79,7 @@ def test_for_top_level(self): def test_read_text(self): tests = [ ('egginfo-pkg', 'mod\n'), - ('empty_egg-pkg', '\n'), + ('egg_with_no_modules-pkg', '\n'), ] for pkg_name, expect_content in tests: with self.subTest(pkg_name): @@ -187,7 +187,7 @@ def test_files_dist_info(self): def test_files_egg_info(self): self._test_files(files('egginfo-pkg')) self._test_files(files('egg_with_module-pkg')) - self._test_files(files('empty_egg-pkg')) + self._test_files(files('egg_with_no_modules-pkg')) self._test_files(files('sources_fallback-pkg')) def test_version_egg_info_file(self): diff --git a/tests/test_main.py b/tests/test_main.py index a0f80f02..c7c39094 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -183,7 +183,7 @@ def test_package_discovery(self): assert all(isinstance(dist, Distribution) for dist in dists) assert any(dist.metadata['Name'] == 'egginfo-pkg' for dist in dists) assert any(dist.metadata['Name'] == 'egg_with_module-pkg' for dist in dists) - assert any(dist.metadata['Name'] == 'empty_egg-pkg' for dist in dists) + assert any(dist.metadata['Name'] == 'egg_with_no_modules-pkg' for dist in dists) assert any(dist.metadata['Name'] == 'sources_fallback-pkg' for dist in dists) assert any(dist.metadata['Name'] == 'distinfo-pkg' for dist in dists) @@ -393,9 +393,9 @@ def import_names_from_package(package_name): # installed-files.txt (top_level.txt is missing) assert import_names_from_package('egg_with_module-pkg') == {'egg_with_module'} - # empty_egg-pkg should not be associated with any import names + # egg_with_no_modules-pkg should not be associated with any import names # (top_level.txt is empty, and installed-files.txt has no .py files) - assert import_names_from_package('empty_egg-pkg') == set() + assert import_names_from_package('egg_with_no_modules-pkg') == set() # sources_fallback-pkg has one import ('sources_fallback') inferred from # SOURCES.txt (top_level.txt and installed-files.txt is missing) From 61eca31a7456ccc39e3fda8e098497a4f28482b4 Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Sun, 19 Mar 2023 03:03:19 +0100 Subject: [PATCH 062/131] squash! Distribution.files: Only return files that actually exist Remove unnecessary list() call. --- importlib_metadata/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index dd7839db..d83025da 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -478,7 +478,7 @@ def make_files(lines): return list( filter( lambda package_path: package_path.locate().exists(), - list(starmap(make_file, csv.reader(lines))), + starmap(make_file, csv.reader(lines)), ) ) From 9b165a91af49bb11b7cf06b5661801f603aaeba6 Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Sun, 19 Mar 2023 03:23:47 +0100 Subject: [PATCH 063/131] Refactor logic for skipping missing files out of magic_files() --- importlib_metadata/__init__.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index d83025da..2884bed0 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -475,17 +475,18 @@ def make_file(name, hash=None, size_str=None): @pass_none def make_files(lines): - return list( - filter( - lambda package_path: package_path.locate().exists(), - starmap(make_file, csv.reader(lines)), - ) - ) + return starmap(make_file, csv.reader(lines)) - return make_files( - self._read_files_distinfo() - or self._read_files_egginfo_installed() - or self._read_files_egginfo_sources() + @pass_none + def skip_missing_files(package_paths): + return list(filter(lambda path: path.locate().exists(), package_paths)) + + return skip_missing_files( + make_files( + self._read_files_distinfo() + or self._read_files_egginfo_installed() + or self._read_files_egginfo_sources() + ) ) def _read_files_distinfo(self): From 33eb7b4a0312017048d849cf1aef0321282d329d Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Sun, 19 Mar 2023 03:24:13 +0100 Subject: [PATCH 064/131] Rewrite docstrings to clarify the expected output format, and why we need quoting --- importlib_metadata/__init__.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 2884bed0..776896b3 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -498,10 +498,10 @@ def _read_files_distinfo(self): def _read_files_egginfo_installed(self): """ - installed-files.txt might contain literal commas, so wrap - each line in quotes. Also, the entries in installed-files.txt - are relative to the .egg-info/ subdir (not relative to the - parent site-packages directory that make_file() expects). + Read installed-files.txt and return lines in a similar + CSV-parsable format as RECORD: each file must be placed + relative to the site-packages directory, and must also be + quoted (since file names can contain literal commas). This file is written when the package is installed by pip, but it might not be written for other installation methods. @@ -526,8 +526,9 @@ def _read_files_egginfo_installed(self): def _read_files_egginfo_sources(self): """ - SOURCES.txt might contain literal commas, so wrap each line - in quotes. + Read SOURCES.txt and return lines in a similar CSV-parsable + format as RECORD: each file name must be quoted (since it + might contain literal commas). Note that SOURCES.txt is not a reliable source for what files are installed by a package. This file is generated From fa9cca4ebb6c5aaf43197b04c4e28bf62bdb11ce Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Sun, 19 Mar 2023 04:09:18 +0100 Subject: [PATCH 065/131] test_packages_distributions_all_module_types() must create existing files for all the entries in RECORD --- tests/fixtures.py | 1 + tests/test_main.py | 21 ++++++++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index 6c589b40..7a96dca6 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -351,6 +351,7 @@ def build_files(file_defs, prefix=pathlib.Path()): full_name.mkdir() build_files(contents, prefix=full_name) else: + full_name.parent.mkdir(parents=True, exist_ok=True) if isinstance(contents, bytes): with full_name.open('wb') as f: f.write(contents) diff --git a/tests/test_main.py b/tests/test_main.py index c7c39094..8225e2d1 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -3,6 +3,7 @@ import unittest import importlib import importlib_metadata +import itertools import pyfakefs.fake_filesystem_unittest as ffs from . import fixtures @@ -338,6 +339,17 @@ def test_packages_distributions_all_module_types(self): Test top-level modules detected on a package without 'top-level.txt'. """ suffixes = importlib.machinery.all_suffixes() + filenames = list( + itertools.chain.from_iterable( + [ + f'{i}-top-level{suffix}', + f'{i}-in-namespace/mod{suffix}', + f'{i}-in-package/__init__.py', + f'{i}-in-package/mod{suffix}', + ] + for i, suffix in enumerate(suffixes) + ) + ) fixtures.build_files( { 'all_distributions-1.0.0.dist-info': { @@ -345,17 +357,12 @@ def test_packages_distributions_all_module_types(self): Name: all_distributions Version: 1.0.0 """, - 'RECORD': ''.join( - f'{i}-top-level{suffix},,\n' - f'{i}-in-namespace/mod{suffix},,\n' - f'{i}-in-package/__init__.py,,\n' - f'{i}-in-package/mod{suffix},,\n' - for i, suffix in enumerate(suffixes) - ), + 'RECORD': ''.join(f'{fname},,\n' for fname in filenames), }, }, prefix=self.site_dir, ) + fixtures.build_files({fname: "" for fname in filenames}, prefix=self.site_dir) distributions = packages_distributions() From 70ff991377f8d0848865c8c210edd2c7c41c119c Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Sun, 19 Mar 2023 19:04:12 +0100 Subject: [PATCH 066/131] test_packages_distributions_all_module_types: Create valid import names The import names that were created by these tests were not valid Python identifiers. Fix that, and furthermore: add another check to verify that _all_ import names returned from packages_distributions() are always valid Python identifiers. Ideally we should check that all keys returned from packages_distributions() are valid import names (i.e. can be imported), but this is at least a step in the right direction. --- tests/test_main.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index 8225e2d1..883b4487 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -342,10 +342,10 @@ def test_packages_distributions_all_module_types(self): filenames = list( itertools.chain.from_iterable( [ - f'{i}-top-level{suffix}', - f'{i}-in-namespace/mod{suffix}', - f'{i}-in-package/__init__.py', - f'{i}-in-package/mod{suffix}', + f'top_level_{i}{suffix}', + f'in_namespace_{i}/mod{suffix}', + f'in_package_{i}/__init__.py', + f'in_package_{i}/mod{suffix}', ] for i, suffix in enumerate(suffixes) ) @@ -367,9 +367,14 @@ def test_packages_distributions_all_module_types(self): distributions = packages_distributions() for i in range(len(suffixes)): - assert distributions[f'{i}-top-level'] == ['all_distributions'] - assert distributions[f'{i}-in-namespace'] == ['all_distributions'] - assert distributions[f'{i}-in-package'] == ['all_distributions'] + assert distributions[f'top_level_{i}'] == ['all_distributions'] + assert distributions[f'in_namespace_{i}'] == ['all_distributions'] + assert distributions[f'in_package_{i}'] == ['all_distributions'] + + # All keys returned from packages_distributions() should be valid import + # names, which means that they must _at least_ be valid identifiers: + for import_name in distributions.keys(): + assert import_name.isidentifier(), import_name class PackagesDistributionsEggTest( From cfd9b2641451c3af78c0a3ca0d13367fc94bdddc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 25 Mar 2023 10:46:40 -0400 Subject: [PATCH 067/131] Mark test as xfail as it's about to fail. Ref #442. --- tests/test_main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_main.py b/tests/test_main.py index 16367793..3c8103d8 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -323,6 +323,9 @@ def test_packages_distributions_neither_toplevel_nor_files(self): ) packages_distributions() + import pytest + + @pytest.mark.xfail(reason="442") def test_packages_distributions_all_module_types(self): """ Test top-level modules detected on a package without 'top-level.txt'. From 5e8260c8e545d7f21c779fb8b57004bc280ae330 Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Sat, 25 Mar 2023 10:49:47 -0400 Subject: [PATCH 068/131] Add test capturing missed expectation. Ref #442. --- tests/test_main.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index 3c8103d8..96a02788 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -339,10 +339,10 @@ def test_packages_distributions_all_module_types(self): Version: 1.0.0 """, 'RECORD': ''.join( - f'{i}-top-level{suffix},,\n' - f'{i}-in-namespace/mod{suffix},,\n' - f'{i}-in-package/__init__.py,,\n' - f'{i}-in-package/mod{suffix},,\n' + f'top_level_{i}{suffix},,\n' + f'in_namespace_{i}/mod{suffix},,\n' + f'in_package_{i}/__init__.py,,\n' + f'in_package_{i}/mod{suffix},,\n' for i, suffix in enumerate(suffixes) ), }, @@ -353,6 +353,11 @@ def test_packages_distributions_all_module_types(self): distributions = packages_distributions() for i in range(len(suffixes)): - assert distributions[f'{i}-top-level'] == ['all_distributions'] - assert distributions[f'{i}-in-namespace'] == ['all_distributions'] - assert distributions[f'{i}-in-package'] == ['all_distributions'] + assert distributions[f'top_level_{i}'] == ['all_distributions'] + assert distributions[f'in_namespace_{i}'] == ['all_distributions'] + assert distributions[f'in_package_{i}'] == ['all_distributions'] + + # All keys return from packages_distributions() should be valid import + # names, which means that they must _at least_ be valid identifiers: + for import_name in distributions.keys(): + assert import_name.isidentifier(), import_name From da5785526aeeb0cfbf4842fd640bf84570489515 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 25 Mar 2023 10:50:33 -0400 Subject: [PATCH 069/131] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins?= =?UTF-8?q?=20(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pep8 states that comments should be limited to 72 characters. --- tests/test_main.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index 96a02788..83f04f80 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -357,7 +357,8 @@ def test_packages_distributions_all_module_types(self): assert distributions[f'in_namespace_{i}'] == ['all_distributions'] assert distributions[f'in_package_{i}'] == ['all_distributions'] - # All keys return from packages_distributions() should be valid import - # names, which means that they must _at least_ be valid identifiers: + # All keys return from packages_distributions() should be valid + # import names, which means that they must _at least_ be valid + # identifiers: for import_name in distributions.keys(): assert import_name.isidentifier(), import_name From c8676416f80bb8ef79f46c24b8cc25812cc0708b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 25 Mar 2023 10:51:52 -0400 Subject: [PATCH 070/131] Prefer all when asserting all. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⚫ Fade to black. --- tests/test_main.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index 83f04f80..73747405 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -360,5 +360,4 @@ def test_packages_distributions_all_module_types(self): # All keys return from packages_distributions() should be valid # import names, which means that they must _at least_ be valid # identifiers: - for import_name in distributions.keys(): - assert import_name.isidentifier(), import_name + assert all(import_name.isidentifier() for import_name in distributions.keys()) From 340cac39996ed9cb9f0522993cdd259ce4b99480 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 25 Mar 2023 11:19:29 -0400 Subject: [PATCH 071/131] Filter non-identifiers from module names. Fixes #442. --- importlib_metadata/__init__.py | 7 ++++++- tests/test_main.py | 3 --- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 8d9f0016..e44ea31a 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -902,4 +902,9 @@ def _top_level_inferred(dist): f.parts[0] if len(f.parts) > 1 else inspect.getmodulename(f) for f in always_iterable(dist.files) } - return filter(None, opt_names) + + @pass_none + def valid_module(name): + return name.isidentifier() + + return filter(valid_module, opt_names) diff --git a/tests/test_main.py b/tests/test_main.py index 73747405..ba752d88 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -323,9 +323,6 @@ def test_packages_distributions_neither_toplevel_nor_files(self): ) packages_distributions() - import pytest - - @pytest.mark.xfail(reason="442") def test_packages_distributions_all_module_types(self): """ Test top-level modules detected on a package without 'top-level.txt'. From 73c138548afd1ac37d9f80c7fecf458bf133a5aa Mon Sep 17 00:00:00 2001 From: David Hotham Date: Fri, 7 Apr 2023 14:07:38 +0100 Subject: [PATCH 072/131] add .get() to the PackageMetadata protocol --- importlib_metadata/_meta.py | 8 ++++++++ tests/test_api.py | 14 ++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/importlib_metadata/_meta.py b/importlib_metadata/_meta.py index 8621c680..e27d34aa 100644 --- a/importlib_metadata/_meta.py +++ b/importlib_metadata/_meta.py @@ -18,6 +18,14 @@ def __getitem__(self, key: str) -> str: def __iter__(self) -> Iterator[str]: ... # pragma: no cover + @overload + def get(self, name: str, failobj: None = None) -> Optional[str]: + ... # pragma: no cover + + @overload + def get(self, name: str, failobj: _T) -> Union[str, _T]: + ... # pragma: no cover + # overload per python/importlib_metadata#435 @overload def get_all(self, name: str, failobj: None = None) -> Optional[List[Any]]: diff --git a/tests/test_api.py b/tests/test_api.py index 2932c2d2..6dbce1fc 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -148,6 +148,20 @@ def test_missing_key_legacy(self): with suppress_known_deprecation(): assert md['does-not-exist'] is None + def test_get_key(self): + """ + Getting a key gets the key. + """ + md = metadata('egginfo-pkg') + assert md.get('Name') == 'egginfo-pkg' + + def test_get_missing_key(self): + """ + Requesting a missing key will return None. + """ + md = metadata('distinfo-pkg') + assert md.get('does-not-exist') is None + @staticmethod def _test_files(files): root = files[0].root From 15fffb8811e21bd92101f2fa47daaf50ee48c8dd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 7 Apr 2023 12:43:55 -0400 Subject: [PATCH 073/131] Update changelog. --- CHANGES.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index eccdd5ba..3583a211 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,10 @@ +v6.2.0 +====== + +* #384: ``PackageMetadata`` now stipulates an additional ``get`` + method allowing for easy querying of metadata keys that may not + be present. + v6.1.0 ====== From a4e2a9b0905992e3ccad9d3dfb59cbeeb1a3b8b9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 9 Apr 2023 10:45:29 -0400 Subject: [PATCH 074/131] Relax assertion that all names in packages_distributions are identifiers. The main contstraint here for an importable module is that it must not contain a module separator ('.'). Other names that contain dashes or spaces cannot be imported with the 'import' statement, but can be imported with 'importlib.import_module' or invoked with 'runpy'. --- tests/test_main.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index ba752d88..7f711f16 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -354,7 +354,9 @@ def test_packages_distributions_all_module_types(self): assert distributions[f'in_namespace_{i}'] == ['all_distributions'] assert distributions[f'in_package_{i}'] == ['all_distributions'] - # All keys return from packages_distributions() should be valid - # import names, which means that they must _at least_ be valid - # identifiers: - assert all(import_name.isidentifier() for import_name in distributions.keys()) + def is_importable(name): + return '.' not in name + + # All keys returned from packages_distributions() should be + # importable. + assert all(map(is_importable, distributions)) From 7968a088e5ec63313d19100010d7a10a7f66b729 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 9 Apr 2023 11:02:51 -0400 Subject: [PATCH 075/131] Expand test to include importable names that aren't identifiers and honor that expectation. --- importlib_metadata/__init__.py | 6 +++--- tests/test_main.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index e44ea31a..217ca9cc 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -904,7 +904,7 @@ def _top_level_inferred(dist): } @pass_none - def valid_module(name): - return name.isidentifier() + def importable_name(name): + return '.' not in name - return filter(valid_module, opt_names) + return filter(importable_name, opt_names) diff --git a/tests/test_main.py b/tests/test_main.py index 7f711f16..d04deba0 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -336,7 +336,7 @@ def test_packages_distributions_all_module_types(self): Version: 1.0.0 """, 'RECORD': ''.join( - f'top_level_{i}{suffix},,\n' + f'importable-name {i}{suffix},,\n' f'in_namespace_{i}/mod{suffix},,\n' f'in_package_{i}/__init__.py,,\n' f'in_package_{i}/mod{suffix},,\n' @@ -350,7 +350,7 @@ def test_packages_distributions_all_module_types(self): distributions = packages_distributions() for i in range(len(suffixes)): - assert distributions[f'top_level_{i}'] == ['all_distributions'] + assert distributions[f'importable-name {i}'] == ['all_distributions'] assert distributions[f'in_namespace_{i}'] == ['all_distributions'] assert distributions[f'in_package_{i}'] == ['all_distributions'] From 1a831b670b9521acedc8bef80c19bbf7eb524588 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 9 Apr 2023 11:07:12 -0400 Subject: [PATCH 076/131] Capture expectation that 'dist-info' should not appear in inferred top-level names. Ref #442. --- tests/test_main.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_main.py b/tests/test_main.py index 3c8103d8..e24acf37 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -338,7 +338,8 @@ def test_packages_distributions_all_module_types(self): Name: all_distributions Version: 1.0.0 """, - 'RECORD': ''.join( + 'RECORD': 'all_distributions-1.0.0.dist-info/METADATA\n' + + ''.join( f'{i}-top-level{suffix},,\n' f'{i}-in-namespace/mod{suffix},,\n' f'{i}-in-package/__init__.py,,\n' @@ -356,3 +357,5 @@ def test_packages_distributions_all_module_types(self): assert distributions[f'{i}-top-level'] == ['all_distributions'] assert distributions[f'{i}-in-namespace'] == ['all_distributions'] assert distributions[f'{i}-in-package'] == ['all_distributions'] + + assert not any(name.endswith('.dist-info') for name in distributions) From 4c02b186ba77cb02dbc153515146e15f9bcfc16a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 9 Apr 2023 11:16:12 -0400 Subject: [PATCH 077/131] Update changelog. --- CHANGES.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index eccdd5ba..bab2571b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,10 @@ +v6.1.1 +====== + +* #442: Fixed issue introduced in v6.1.0 where non-importable + names (metadata dirs) began appearing in + ``packages_distributions``. + v6.1.0 ====== From 5dbe83cdb0565e13f5a57629a6a9a334e07ebe93 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 9 Apr 2023 15:49:38 -0400 Subject: [PATCH 078/131] Revert "test_packages_distributions_all_module_types: Create valid import names" This reverts commit 70ff991377f8d0848865c8c210edd2c7c41c119c. This behavior was adopted in 5e8260c8e545d7f21c779fb8b57004bc280ae330 and subsequently adapted as part of #443. --- tests/test_main.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index 883b4487..8225e2d1 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -342,10 +342,10 @@ def test_packages_distributions_all_module_types(self): filenames = list( itertools.chain.from_iterable( [ - f'top_level_{i}{suffix}', - f'in_namespace_{i}/mod{suffix}', - f'in_package_{i}/__init__.py', - f'in_package_{i}/mod{suffix}', + f'{i}-top-level{suffix}', + f'{i}-in-namespace/mod{suffix}', + f'{i}-in-package/__init__.py', + f'{i}-in-package/mod{suffix}', ] for i, suffix in enumerate(suffixes) ) @@ -367,14 +367,9 @@ def test_packages_distributions_all_module_types(self): distributions = packages_distributions() for i in range(len(suffixes)): - assert distributions[f'top_level_{i}'] == ['all_distributions'] - assert distributions[f'in_namespace_{i}'] == ['all_distributions'] - assert distributions[f'in_package_{i}'] == ['all_distributions'] - - # All keys returned from packages_distributions() should be valid import - # names, which means that they must _at least_ be valid identifiers: - for import_name in distributions.keys(): - assert import_name.isidentifier(), import_name + assert distributions[f'{i}-top-level'] == ['all_distributions'] + assert distributions[f'{i}-in-namespace'] == ['all_distributions'] + assert distributions[f'{i}-in-package'] == ['all_distributions'] class PackagesDistributionsEggTest( From 4e7f79f5998cb08c2d84ca250c83a2820dd32e8a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 9 Apr 2023 18:17:40 -0400 Subject: [PATCH 079/131] Revert "test_packages_distributions_all_module_types() must create existing files for all the entries in RECORD" This reverts commit fa9cca4ebb6c5aaf43197b04c4e28bf62bdb11ce. --- tests/fixtures.py | 1 - tests/test_main.py | 21 +++++++-------------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index 7a96dca6..6c589b40 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -351,7 +351,6 @@ def build_files(file_defs, prefix=pathlib.Path()): full_name.mkdir() build_files(contents, prefix=full_name) else: - full_name.parent.mkdir(parents=True, exist_ok=True) if isinstance(contents, bytes): with full_name.open('wb') as f: f.write(contents) diff --git a/tests/test_main.py b/tests/test_main.py index 8225e2d1..c7c39094 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -3,7 +3,6 @@ import unittest import importlib import importlib_metadata -import itertools import pyfakefs.fake_filesystem_unittest as ffs from . import fixtures @@ -339,17 +338,6 @@ def test_packages_distributions_all_module_types(self): Test top-level modules detected on a package without 'top-level.txt'. """ suffixes = importlib.machinery.all_suffixes() - filenames = list( - itertools.chain.from_iterable( - [ - f'{i}-top-level{suffix}', - f'{i}-in-namespace/mod{suffix}', - f'{i}-in-package/__init__.py', - f'{i}-in-package/mod{suffix}', - ] - for i, suffix in enumerate(suffixes) - ) - ) fixtures.build_files( { 'all_distributions-1.0.0.dist-info': { @@ -357,12 +345,17 @@ def test_packages_distributions_all_module_types(self): Name: all_distributions Version: 1.0.0 """, - 'RECORD': ''.join(f'{fname},,\n' for fname in filenames), + 'RECORD': ''.join( + f'{i}-top-level{suffix},,\n' + f'{i}-in-namespace/mod{suffix},,\n' + f'{i}-in-package/__init__.py,,\n' + f'{i}-in-package/mod{suffix},,\n' + for i, suffix in enumerate(suffixes) + ), }, }, prefix=self.site_dir, ) - fixtures.build_files({fname: "" for fname in filenames}, prefix=self.site_dir) distributions = packages_distributions() From 2cb9b4548d7a9aa844bde0141eb924a47bb9beb5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 9 Apr 2023 19:18:36 -0400 Subject: [PATCH 080/131] Vendor jaraco.path.build from jaraco.path 3.5 --- tests/_path.py | 104 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 tests/_path.py diff --git a/tests/_path.py b/tests/_path.py new file mode 100644 index 00000000..fcc480c6 --- /dev/null +++ b/tests/_path.py @@ -0,0 +1,104 @@ +# from jaraco.path 3.5 + +import functools +import pathlib +from typing import Dict, Union + +try: + from typing import Protocol, runtime_checkable +except ImportError: # pragma: no cover + # Python 3.7 + from typing_extensions import Protocol, runtime_checkable # type: ignore + + +FilesSpec = Dict[str, Union[str, bytes, 'FilesSpec']] # type: ignore + + +@runtime_checkable +class TreeMaker(Protocol): + def __truediv__(self, *args, **kwargs): + ... # pragma: no cover + + def mkdir(self, **kwargs): + ... # pragma: no cover + + def write_text(self, content, **kwargs): + ... # pragma: no cover + + def write_bytes(self, content): + ... # pragma: no cover + + +def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker: + return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore + + +def build( + spec: FilesSpec, + prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore +): + """ + 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, _ensure_tree_maker(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') + + +class Recording: + """ + A TreeMaker object that records everything that would be written. + + >>> r = Recording() + >>> build({'foo': {'foo1.txt': 'yes'}, 'bar.txt': 'abc'}, r) + >>> r.record + ['foo/foo1.txt', 'bar.txt'] + """ + + def __init__(self, loc=pathlib.PurePosixPath(), record=None): + self.loc = loc + self.record = record if record is not None else [] + + def __truediv__(self, other): + return Recording(self.loc / other, self.record) + + def write_text(self, content, **kwargs): + self.record.append(str(self.loc)) + + write_bytes = write_text + + def mkdir(self, **kwargs): + return From 32fde63a7affda4b47994af73179821663247794 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 9 Apr 2023 19:58:12 -0400 Subject: [PATCH 081/131] Generate the files definition with the mod_files. Use 'build_record' to build the metadata RECORD from the files definition. --- tests/fixtures.py | 13 +++++++++++++ tests/test_main.py | 44 ++++++++++++++++++++++++++------------------ 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index 6d9a9d2b..bcbba5d4 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -11,6 +11,9 @@ from .py39compat import FS_NONASCII from typing import Dict, Union +from . import _path + + try: from importlib import resources # type: ignore @@ -266,6 +269,16 @@ def build_files(file_defs, prefix=pathlib.Path()): f.write(DALS(contents)) +def build_record(file_defs): + return ''.join(f'{name},,\n' for name in record_names(file_defs)) + + +def record_names(file_defs): + recording = _path.Recording() + _path.build(file_defs, recording) + return recording.record + + class FileBuilder: def unicode_filename(self): return FS_NONASCII or self.skip("File system does not support non-ascii.") diff --git a/tests/test_main.py b/tests/test_main.py index 3edd938e..c4043651 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -3,6 +3,7 @@ import unittest import importlib import importlib_metadata +import itertools import pyfakefs.fake_filesystem_unittest as ffs from . import fixtures @@ -328,25 +329,32 @@ def test_packages_distributions_all_module_types(self): Test top-level modules detected on a package without 'top-level.txt'. """ suffixes = importlib.machinery.all_suffixes() - fixtures.build_files( - { - 'all_distributions-1.0.0.dist-info': { - 'METADATA': """ - Name: all_distributions - Version: 1.0.0 - """, - 'RECORD': 'all_distributions-1.0.0.dist-info/METADATA\n' - + ''.join( - f'importable-name {i}{suffix},,\n' - f'in_namespace_{i}/mod{suffix},,\n' - f'in_package_{i}/__init__.py,,\n' - f'in_package_{i}/mod{suffix},,\n' - for i, suffix in enumerate(suffixes) - ), - }, - }, - prefix=self.site_dir, + metadata = dict( + METADATA=""" + Name: all_distributions + Version: 1.0.0 + """, ) + files = { + 'all_distributions-1.0.0.dist-info': metadata, + } + mod_files = {} + for i, suffix in enumerate(suffixes): + mod_files.update( + { + f'importable-name {i}{suffix}': '', + f'in_namespace_{i}': { + f'mod{suffix}': '', + }, + f'in_package_{i}': { + '__init__.py': '', + f'mod{suffix}': '', + }, + } + ) + all_files = dict(**files, **mod_files) + metadata.update(RECORD=fixtures.build_record(all_files)) + fixtures.build_files(files, prefix=self.site_dir) distributions = packages_distributions() From 142d0dd768de1196bb9f7eb1921e02dc5ea4acb8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 9 Apr 2023 20:05:51 -0400 Subject: [PATCH 082/131] Consolidate construction of files list, causing creation of the module files. --- tests/test_main.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index c4043651..cc26c56c 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -338,9 +338,8 @@ def test_packages_distributions_all_module_types(self): files = { 'all_distributions-1.0.0.dist-info': metadata, } - mod_files = {} for i, suffix in enumerate(suffixes): - mod_files.update( + files.update( { f'importable-name {i}{suffix}': '', f'in_namespace_{i}': { @@ -352,8 +351,7 @@ def test_packages_distributions_all_module_types(self): }, } ) - all_files = dict(**files, **mod_files) - metadata.update(RECORD=fixtures.build_record(all_files)) + metadata.update(RECORD=fixtures.build_record(files)) fixtures.build_files(files, prefix=self.site_dir) distributions = packages_distributions() From 8818432f9c71f77bbb79fcf56a9aa53e4b9a6bd5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 9 Apr 2023 21:31:31 -0400 Subject: [PATCH 083/131] =?UTF-8?q?=E2=9A=AB=20Fade=20to=20black.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/fixtures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index 7db4c30a..6e72c6ab 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -243,7 +243,7 @@ class EggInfoPkgPipInstalledNoToplevel(OnSysPath, SiteDir): "egg_with_module.py": """ def main(): print("hello world") - """ + """, } def setUp(self): From 3d7ee19cd58ceb67f91a6766e9f6035a918b95f2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 9 Apr 2023 22:13:59 -0400 Subject: [PATCH 084/131] Refactor to avoid missed coverage --- importlib_metadata/__init__.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 1008a2c6..a298d7a1 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -12,6 +12,7 @@ import functools import itertools import posixpath +import contextlib import collections import inspect @@ -513,16 +514,14 @@ def _read_files_egginfo_installed(self): # But this subdir is only available in the PathDistribution's self._path # which is not easily accessible from this base class... subdir = getattr(self, '_path', None) - try: - if text and subdir: - ret = [ - str((subdir / line).resolve().relative_to(self.locate_file(''))) - for line in text.splitlines() - ] - return map('"{}"'.format, ret) - except Exception: - pass - return None + if not text or not subdir: + return + with contextlib.suppress(Exception): + ret = [ + str((subdir / line).resolve().relative_to(self.locate_file(''))) + for line in text.splitlines() + ] + return map('"{}"'.format, ret) def _read_files_egginfo_sources(self): """ From b8a8b5d35e6ca31a4a7cdfa5a3ee61562e8e7456 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 9 Apr 2023 22:16:03 -0400 Subject: [PATCH 085/131] Update changelog. --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 1bc5222e..6ec9d1f3 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +v6.3.0 +====== + +* #115: Support ``installed-files.txt`` for ``Distribution.files`` + when present. + v6.2.1 ====== From 5957d58266e479f124b31f30e4322e798fdf386b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 10 Apr 2023 21:57:33 -0400 Subject: [PATCH 086/131] Remove unnecessary and incorrect copyright notice. Fixes jaraco/skeleton#78. --- LICENSE | 2 -- 1 file changed, 2 deletions(-) diff --git a/LICENSE b/LICENSE index 353924be..1bb5a443 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,3 @@ -Copyright Jason R. Coombs - 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 From 5ff3f3b462a727f0a53aa48a87dfabb24fff5524 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 12 Apr 2023 20:32:58 -0400 Subject: [PATCH 087/131] Re-use _path.build for building files. --- tests/_path.py | 5 +++++ tests/fixtures.py | 34 ++++------------------------------ 2 files changed, 9 insertions(+), 30 deletions(-) diff --git a/tests/_path.py b/tests/_path.py index fcc480c6..71a70438 100644 --- a/tests/_path.py +++ b/tests/_path.py @@ -78,6 +78,11 @@ def _(content: str, path): path.write_text(content, encoding='utf-8') +@create.register +def _(content: str, path): + path.write_text(content, encoding='utf-8') + + class Recording: """ A TreeMaker object that records everything that would be written. diff --git a/tests/fixtures.py b/tests/fixtures.py index 6e72c6ab..35a5e4c0 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -328,38 +328,12 @@ def setUp(self): build_files(EggInfoFile.files, prefix=self.site_dir) -def build_files(file_defs, prefix=pathlib.Path()): - """Build a set of files/directories, as described by the +# dedent all text strings before writing +orig = _path.create.registry[str] +_path.create.register(str, lambda content, path: orig(DALS(content), path)) - file_defs dictionary. Each key/value pair in the dictionary is - interpreted as a filename/contents pair. If the contents value is a - dictionary, a directory is created, and the dictionary interpreted - as the files within it, recursively. - For example: - - {"README.txt": "A README file", - "foo": { - "__init__.py": "", - "bar": { - "__init__.py": "", - }, - "baz.py": "# Some code", - } - } - """ - for name, contents in file_defs.items(): - full_name = prefix / name - if isinstance(contents, dict): - full_name.mkdir() - build_files(contents, prefix=full_name) - else: - if isinstance(contents, bytes): - with full_name.open('wb') as f: - f.write(contents) - else: - with full_name.open('w', encoding='utf-8') as f: - f.write(DALS(contents)) +build_files = _path.build def build_record(file_defs): From b0cce8a9139559d84d25faccdf7d75198d2252cd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 12 Apr 2023 20:36:24 -0400 Subject: [PATCH 088/131] Re-use FilesSpec from _path. --- tests/fixtures.py | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index 35a5e4c0..6d26bb91 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -9,9 +9,9 @@ import contextlib from .py39compat import FS_NONASCII -from typing import Dict, Union from . import _path +from ._path import FilesSpec try: @@ -85,15 +85,8 @@ def setUp(self): self.fixtures.enter_context(self.add_sys_path(self.site_dir)) -# Except for python/mypy#731, prefer to define -# FilesDef = Dict[str, Union['FilesDef', str, bytes]] -FilesDef = Dict[ - str, Union[Dict[str, Union[Dict[str, Union[str, bytes]], str, bytes]], str, bytes] -] - - class DistInfoPkg(OnSysPath, SiteDir): - files: FilesDef = { + files: FilesSpec = { "distinfo_pkg-1.0.0.dist-info": { "METADATA": """ Name: distinfo-pkg @@ -135,7 +128,7 @@ def make_uppercase(self): class DistInfoPkgWithDot(OnSysPath, SiteDir): - files: FilesDef = { + files: FilesSpec = { "pkg_dot-1.0.0.dist-info": { "METADATA": """ Name: pkg.dot @@ -150,7 +143,7 @@ def setUp(self): class DistInfoPkgWithDotLegacy(OnSysPath, SiteDir): - files: FilesDef = { + files: FilesSpec = { "pkg.dot-1.0.0.dist-info": { "METADATA": """ Name: pkg.dot @@ -177,7 +170,7 @@ def setUp(self): class EggInfoPkg(OnSysPath, SiteDir): - files: FilesDef = { + files: FilesSpec = { "egginfo_pkg.egg-info": { "PKG-INFO": """ Name: egginfo-pkg @@ -217,7 +210,7 @@ def setUp(self): class EggInfoPkgPipInstalledNoToplevel(OnSysPath, SiteDir): - files: FilesDef = { + files: FilesSpec = { "egg_with_module_pkg.egg-info": { "PKG-INFO": "Name: egg_with_module-pkg", # SOURCES.txt is made from the source archive, and contains files @@ -252,7 +245,7 @@ def setUp(self): class EggInfoPkgPipInstalledNoModules(OnSysPath, SiteDir): - files: FilesDef = { + files: FilesSpec = { "egg_with_no_modules_pkg.egg-info": { "PKG-INFO": "Name: egg_with_no_modules-pkg", # SOURCES.txt is made from the source archive, and contains files @@ -282,7 +275,7 @@ def setUp(self): class EggInfoPkgSourcesFallback(OnSysPath, SiteDir): - files: FilesDef = { + files: FilesSpec = { "sources_fallback_pkg.egg-info": { "PKG-INFO": "Name: sources_fallback-pkg", # SOURCES.txt is made from the source archive, and contains files @@ -308,7 +301,7 @@ def setUp(self): class EggInfoFile(OnSysPath, SiteDir): - files: FilesDef = { + files: FilesSpec = { "egginfo_file.egg-info": """ Metadata-Version: 1.0 Name: egginfo_file From d3908983078a47acecf430115b2bcc2d104f54a6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 14 Apr 2023 09:12:59 -0400 Subject: [PATCH 089/131] Add type hint to read_text result. --- importlib_metadata/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index a298d7a1..96571f4a 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -31,7 +31,7 @@ from importlib import import_module from importlib.abc import MetaPathFinder from itertools import starmap -from typing import List, Mapping, Optional +from typing import List, Mapping, Optional, cast __all__ = [ @@ -352,7 +352,7 @@ class Distribution(metaclass=abc.ABCMeta): """A Python distribution package.""" @abc.abstractmethod - def read_text(self, filename): + def read_text(self, filename) -> Optional[str]: """Attempt to load metadata file given by the name. :param filename: The name of the file in the distribution info. @@ -426,7 +426,7 @@ def metadata(self) -> _meta.PackageMetadata: The returned object will have keys that name the various bits of metadata. See PEP 566 for details. """ - text = ( + opt_text = ( self.read_text('METADATA') or self.read_text('PKG-INFO') # This last clause is here to support old egg-info files. Its @@ -434,6 +434,7 @@ def metadata(self) -> _meta.PackageMetadata: # (which points to the egg-info file) attribute unchanged. or self.read_text('') ) + text = cast(str, opt_text) return _adapters.Message(email.message_from_string(text)) @property From 791e45acd113716a57fc40b2e972f524657eafe2 Mon Sep 17 00:00:00 2001 From: David Hotham Date: Sat, 15 Apr 2023 14:20:33 +0100 Subject: [PATCH 090/131] type annotations --- docs/conf.py | 2 + importlib_metadata/__init__.py | 102 ++++++++++++++++++--------------- importlib_metadata/_meta.py | 2 +- 3 files changed, 60 insertions(+), 46 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 8e7762d0..0d6c66ae 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -64,6 +64,8 @@ nitpick_ignore = [ # Workaround for #316 ('py:class', 'importlib_metadata.EntryPoints'), + ('py:class', 'importlib_metadata.PackagePath'), + ('py:class', 'importlib_metadata.PathDistribution'), ('py:class', 'importlib_metadata.SelectableGroups'), ('py:class', 'importlib_metadata._meta._T'), # Workaround for #435 diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 96571f4a..f0561401 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -5,6 +5,7 @@ import sys import zipp import email +import inspect import pathlib import operator import textwrap @@ -14,7 +15,6 @@ import posixpath import contextlib import collections -import inspect from . import _adapters, _meta, _py39compat from ._collections import FreezableDefaultDict, Pair @@ -31,8 +31,9 @@ from importlib import import_module from importlib.abc import MetaPathFinder from itertools import starmap -from typing import List, Mapping, Optional, cast +from typing import Iterator, List, Mapping, Optional, Set, Union, cast +StrPath = Union[str, "os.PathLike[str]"] __all__ = [ 'Distribution', @@ -53,11 +54,11 @@ class PackageNotFoundError(ModuleNotFoundError): """The package was not found.""" - def __str__(self): + def __str__(self) -> str: return f"No package metadata was found for {self.name}" @property - def name(self): + def name(self) -> str: # type: ignore[override] (name,) = self.args return name @@ -123,8 +124,8 @@ def read(text, filter_=None): yield Pair(name, value) @staticmethod - def valid(line): - return line and not line.startswith('#') + def valid(line: str) -> bool: + return bool(line) and not line.startswith('#') class DeprecatedTuple: @@ -198,7 +199,7 @@ class EntryPoint(DeprecatedTuple): dist: Optional['Distribution'] = None - def __init__(self, name, value, group): + def __init__(self, name: str, value: str, group: str) -> None: vars(self).update(name=name, value=value, group=group) def load(self): @@ -212,18 +213,21 @@ def load(self): return functools.reduce(getattr, attrs, module) @property - def module(self): + def module(self) -> str: match = self.pattern.match(self.value) + assert match is not None return match.group('module') @property - def attr(self): + def attr(self) -> str: match = self.pattern.match(self.value) + assert match is not None return match.group('attr') @property - def extras(self): + def extras(self) -> List[str]: match = self.pattern.match(self.value) + assert match is not None return re.findall(r'\w+', match.group('extras') or '') def _for(self, dist): @@ -271,7 +275,7 @@ def __repr__(self): f'group={self.group!r})' ) - def __hash__(self): + def __hash__(self) -> int: return hash(self._key()) @@ -282,7 +286,7 @@ class EntryPoints(tuple): __slots__ = () - def __getitem__(self, name): # -> EntryPoint: + def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override] """ Get the EntryPoint in self matching name. """ @@ -299,14 +303,14 @@ def select(self, **params): return EntryPoints(ep for ep in self if _py39compat.ep_matches(ep, **params)) @property - def names(self): + def names(self) -> Set[str]: """ Return the set of all names of all entry points. """ return {ep.name for ep in self} @property - def groups(self): + def groups(self) -> Set[str]: """ Return the set of all groups of all entry points. """ @@ -327,24 +331,28 @@ def _from_text(text): class PackagePath(pathlib.PurePosixPath): """A reference to a path in a package""" - def read_text(self, encoding='utf-8'): + hash: Optional["FileHash"] + size: int + dist: "Distribution" + + def read_text(self, encoding: str = 'utf-8') -> str: # type: ignore[override] with self.locate().open(encoding=encoding) as stream: return stream.read() - def read_binary(self): + def read_binary(self) -> bytes: with self.locate().open('rb') as stream: return stream.read() - def locate(self): + def locate(self) -> pathlib.Path: """Return a path-like object for this path""" return self.dist.locate_file(self) class FileHash: - def __init__(self, spec): + def __init__(self, spec: str) -> None: self.mode, _, self.value = spec.partition('=') - def __repr__(self): + def __repr__(self) -> str: return f'' @@ -360,14 +368,14 @@ def read_text(self, filename) -> Optional[str]: """ @abc.abstractmethod - def locate_file(self, path): + def locate_file(self, path: StrPath) -> pathlib.Path: """ Given a path to a file in this distribution, return a path to it. """ @classmethod - def from_name(cls, name: str): + def from_name(cls, name: str) -> "Distribution": """Return the Distribution for the given package name. :param name: The name of the distribution package to search for. @@ -385,14 +393,14 @@ def from_name(cls, name: str): raise PackageNotFoundError(name) @classmethod - def discover(cls, **kwargs): - """Return an iterable of Distribution objects for all packages. + def discover(cls, **kwargs) -> Iterator["Distribution"]: + """Return an iterator of Distribution objects for all packages. Pass a ``context`` or pass keyword arguments for constructing a context. :context: A ``DistributionFinder.Context`` object. - :return: Iterable of Distribution objects for all packages. + :return: Iterator of Distribution objects for all packages. """ context = kwargs.pop('context', None) if context and kwargs: @@ -403,7 +411,7 @@ def discover(cls, **kwargs): ) @staticmethod - def at(path): + def at(path: StrPath) -> "PathDistribution": """Return a Distribution for the indicated metadata path :param path: a string or path-like object @@ -438,7 +446,7 @@ def metadata(self) -> _meta.PackageMetadata: return _adapters.Message(email.message_from_string(text)) @property - def name(self): + def name(self) -> str: """Return the 'Name' metadata for the distribution package.""" return self.metadata['Name'] @@ -448,16 +456,16 @@ def _normalized_name(self): return Prepared.normalize(self.name) @property - def version(self): + def version(self) -> str: """Return the 'Version' metadata for the distribution package.""" return self.metadata['Version'] @property - def entry_points(self): + def entry_points(self) -> EntryPoints: return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self) @property - def files(self): + def files(self) -> Optional[List[PackagePath]]: """Files in this distribution. :return: List of PackagePath for this distribution or None @@ -540,7 +548,7 @@ def _read_files_egginfo_sources(self): return text and map('"{}"'.format, text.splitlines()) @property - def requires(self): + def requires(self) -> Optional[List[str]]: """Generated requirements specified for this Distribution""" reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs() return reqs and list(reqs) @@ -619,7 +627,7 @@ def __init__(self, **kwargs): vars(self).update(kwargs) @property - def path(self): + def path(self) -> List[str]: """ The sequence of directory path that a distribution finder should search. @@ -630,11 +638,11 @@ def path(self): return vars(self).get('path', sys.path) @abc.abstractmethod - def find_distributions(self, context=Context()): + def find_distributions(self, context=Context()) -> Iterator[Distribution]: """ Find distributions. - Return an iterable of all Distribution instances capable of + Return an iterator of all Distribution instances capable of loading the metadata for packages matching the ``context``, a DistributionFinder.Context instance. """ @@ -765,11 +773,13 @@ class MetadataPathFinder(NullFinder, DistributionFinder): of Python that do not have a PathFinder find_distributions(). """ - def find_distributions(self, context=DistributionFinder.Context()): + def find_distributions( + self, context=DistributionFinder.Context() + ) -> Iterator["PathDistribution"]: """ Find distributions. - Return an iterable of all Distribution instances capable of + Return an iterator of all Distribution instances capable of loading the metadata for packages matching ``context.name`` (or all names if ``None`` indicated) along the paths in the list of directories ``context.path``. @@ -785,19 +795,19 @@ def _search_paths(cls, name, paths): path.search(prepared) for path in map(FastPath, paths) ) - def invalidate_caches(cls): + def invalidate_caches(cls) -> None: FastPath.__new__.cache_clear() class PathDistribution(Distribution): - def __init__(self, path: SimplePath): + def __init__(self, path: SimplePath) -> None: """Construct a distribution. :param path: SimplePath indicating the metadata directory. """ self._path = path - def read_text(self, filename): + def read_text(self, filename: StrPath) -> Optional[str]: with suppress( FileNotFoundError, IsADirectoryError, @@ -807,9 +817,11 @@ def read_text(self, filename): ): return self._path.joinpath(filename).read_text(encoding='utf-8') + return None + read_text.__doc__ = Distribution.read_text.__doc__ - def locate_file(self, path): + def locate_file(self, path: StrPath) -> pathlib.Path: return self._path.parent / path @property @@ -842,7 +854,7 @@ def _name_from_stem(stem): return name -def distribution(distribution_name): +def distribution(distribution_name) -> Distribution: """Get the ``Distribution`` instance for the named package. :param distribution_name: The name of the distribution package as a string. @@ -851,10 +863,10 @@ def distribution(distribution_name): return Distribution.from_name(distribution_name) -def distributions(**kwargs): +def distributions(**kwargs) -> Iterator[Distribution]: """Get all ``Distribution`` instances in the current environment. - :return: An iterable of ``Distribution`` instances. + :return: An iterator of ``Distribution`` instances. """ return Distribution.discover(**kwargs) @@ -868,7 +880,7 @@ def metadata(distribution_name) -> _meta.PackageMetadata: return Distribution.from_name(distribution_name).metadata -def version(distribution_name): +def version(distribution_name) -> str: """Get the version string for the named package. :param distribution_name: The name of the distribution package to query. @@ -902,7 +914,7 @@ def entry_points(**params) -> EntryPoints: return EntryPoints(eps).select(**params) -def files(distribution_name): +def files(distribution_name) -> Optional[List[PackagePath]]: """Return a list of files for the named package. :param distribution_name: The name of the distribution package to query. @@ -911,7 +923,7 @@ def files(distribution_name): return distribution(distribution_name).files -def requires(distribution_name): +def requires(distribution_name) -> Optional[List[str]]: """ Return a list of requirements for the named package. diff --git a/importlib_metadata/_meta.py b/importlib_metadata/_meta.py index e27d34aa..0c7e8791 100644 --- a/importlib_metadata/_meta.py +++ b/importlib_metadata/_meta.py @@ -49,7 +49,7 @@ class SimplePath(Protocol[_T]): A minimal subset of pathlib.Path required by PathDistribution. """ - def joinpath(self) -> _T: + def joinpath(self, other: Union[str, _T]) -> _T: ... # pragma: no cover def __truediv__(self, other: Union[str, _T]) -> _T: From 00801f747853fc81d5a9495d2c8f595c568b02b6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 15 Apr 2023 09:48:43 -0400 Subject: [PATCH 091/131] Update changelog --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 6ec9d1f3..22ca6cc4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +v6.4.0 +====== + +* Consolidated some behaviors in tests around ``_path``. +* Added type annotation for ``Distribution.read_text``. + v6.3.0 ====== From 1cf7385798dc37532351f1fe4892529f35095c61 Mon Sep 17 00:00:00 2001 From: Stanley <46876382+slateny@users.noreply.github.com> Date: Tue, 25 Oct 2022 20:26:28 -0700 Subject: [PATCH 092/131] docs: Change links to label refs (python/cpython#98454) Co-authored-by: C.A.M. Gerlach --- docs/using.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 831ad62b..68d350e1 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -343,7 +343,7 @@ Because :term:`packaging:Distribution Package` metadata is not available through :data:`sys.path` searches, or package loaders directly, the metadata for a distribution is found through import -system `finders`_. To find a distribution package's metadata, +system :ref:`finders `. To find a distribution package's metadata, ``importlib.metadata`` queries the list of :term:`meta path finders ` on :data:`sys.meta_path`. @@ -379,4 +379,3 @@ a custom finder, return instances of this derived ``Distribution`` in the .. _`entry point API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points .. _`metadata API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#metadata-api -.. _`finders`: https://docs.python.org/3/reference/import.html#finders-and-loaders From 96f071647312f062d267030d19a46c278881aabf Mon Sep 17 00:00:00 2001 From: Paul Watson Date: Tue, 14 Mar 2023 13:40:12 -0500 Subject: [PATCH 093/131] gh-102354: change python3 to python in docs examples (python/cpython#102696) --- docs/using.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index 68d350e1..17af34d3 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -62,7 +62,7 @@ Let's say you wanted to get the version string for a using ``pip``. We start by creating a virtual environment and installing something into it:: - $ python3 -m venv example + $ python -m venv example $ source example/bin/activate (example) $ python -m pip install importlib_metadata (example) $ python -m pip install wheel From 6ebb1f91d51438f0f8c9e4d940d602203cc8476f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 15 Apr 2023 11:17:37 -0400 Subject: [PATCH 094/131] Update changelog --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 22ca6cc4..744d2d4e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +v6.4.1 +====== + +* Updated docs with tweaks from upstream CPython. + v6.4.0 ====== From 95654e0b584d5aa63fa67693b6774b6b4adeec43 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 17 Apr 2023 19:49:32 -0400 Subject: [PATCH 095/131] Deprecate construction of Distribution and subclasses without implementing abstract methods. Fixes #422. --- importlib_metadata/__init__.py | 21 ++++++++++++++++++++- tests/_context.py | 13 +++++++++++++ tests/test_main.py | 13 +++++++++++++ 3 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 tests/_context.py diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 96571f4a..e9ae0d19 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -348,7 +348,26 @@ def __repr__(self): return f'' -class Distribution(metaclass=abc.ABCMeta): +class DeprecatedNonAbstract: + def __new__(cls, *args, **kwargs): + all_names = { + name for subclass in inspect.getmro(cls) for name in vars(subclass) + } + abstract = { + name + for name in all_names + if getattr(getattr(cls, name), '__isabstractmethod__', False) + } + if abstract: + warnings.warn( + f"Unimplemented abstract methods {abstract}", + DeprecationWarning, + stacklevel=2, + ) + return super().__new__(cls) + + +class Distribution(DeprecatedNonAbstract): """A Python distribution package.""" @abc.abstractmethod diff --git a/tests/_context.py b/tests/_context.py new file mode 100644 index 00000000..8a53eb55 --- /dev/null +++ b/tests/_context.py @@ -0,0 +1,13 @@ +import contextlib + + +# from jaraco.context 4.3 +class suppress(contextlib.suppress, contextlib.ContextDecorator): + """ + A version of contextlib.suppress with decorator support. + + >>> @suppress(KeyError) + ... def key_error(): + ... {}[''] + >>> key_error() + """ diff --git a/tests/test_main.py b/tests/test_main.py index 7d6c79a4..a7650172 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,12 +1,15 @@ import re import pickle import unittest +import warnings import importlib import importlib_metadata +import contextlib import itertools import pyfakefs.fake_filesystem_unittest as ffs from . import fixtures +from ._context import suppress from importlib_metadata import ( Distribution, EntryPoint, @@ -20,6 +23,13 @@ ) +@contextlib.contextmanager +def suppress_known_deprecation(): + with warnings.catch_warnings(record=True) as ctx: + warnings.simplefilter('default', category=DeprecationWarning) + yield ctx + + class BasicTests(fixtures.DistInfoPkg, unittest.TestCase): version_pattern = r'\d+\.\d+(\.\d)?' @@ -44,6 +54,9 @@ def test_package_not_found_mentions_metadata(self): assert "metadata" in str(ctx.exception) + # expected to fail until ABC is enforced + @suppress(AssertionError) + @suppress_known_deprecation() def test_abc_enforced(self): with self.assertRaises(TypeError): type('DistributionSubclass', (Distribution,), {})() From 0e4bd94e20d6e3308ab336762299909f49809e9e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 17 Apr 2023 21:16:08 -0400 Subject: [PATCH 096/131] =?UTF-8?q?=F0=9F=9A=A1=20Toil=20the=20docs.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/conf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 8e7762d0..164564aa 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -68,4 +68,6 @@ ('py:class', 'importlib_metadata._meta._T'), # Workaround for #435 ('py:class', '_T'), + # Other workarounds + ('py:class', 'importlib_metadata.DeprecatedNonAbstract'), ] From 41240d0bc9307a3e07ead49d6e7139d362baeb72 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 17 Apr 2023 21:17:43 -0400 Subject: [PATCH 097/131] Update changelog --- CHANGES.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 744d2d4e..bd9cd385 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,10 @@ +v6.5.0 +====== + +* #422: Removed ABC metaclass from ``Distribution`` and instead + deprecated construction of ``Distribution`` objects without + concrete methods. + v6.4.1 ====== From fb69af42f17b6817abea01156afd7f8e41130eb1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 18 Apr 2023 08:51:50 -0400 Subject: [PATCH 098/131] Use proper RTD slugs. Fixes #447. --- README.rst | 4 ++-- docs/index.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index dae339b9..62c712c5 100644 --- a/README.rst +++ b/README.rst @@ -52,7 +52,7 @@ were contributed to different versions in the standard library: Usage ===== -See the `online documentation `_ +See the `online documentation `_ for usage details. `Finder authors @@ -76,7 +76,7 @@ Project details * 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/ + * Documentation: https://importlib-metadata.readthedocs.io/ For Enterprise ============== diff --git a/docs/index.rst b/docs/index.rst index e5298d76..a5bacd4a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -28,7 +28,7 @@ Project details * 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/ + * Documentation: https://importlib-metadata.readthedocs.io/ Indices and tables From 11b9ddfc392adf69901a3bee510223a2dde5bdc1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 18 Apr 2023 12:37:39 -0400 Subject: [PATCH 099/131] Update compatibility for Python 3.12 (python/cpython#103584). --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 62c712c5..a315c0f5 100644 --- a/README.rst +++ b/README.rst @@ -39,7 +39,7 @@ were contributed to different versions in the standard library: * - importlib_metadata - stdlib - * - 5.0 + * - 6.5 - 3.12 * - 4.13 - 3.11 From b58d47f1e8a9bdf77bcca8bdf6a8909f98f146b3 Mon Sep 17 00:00:00 2001 From: David Hotham Date: Tue, 18 Apr 2023 18:29:03 +0100 Subject: [PATCH 100/131] code review --- importlib_metadata/__init__.py | 33 ++++++++++++++++----------------- importlib_metadata/_compat.py | 10 ++++++++++ 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index f0561401..515f2070 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -20,6 +20,7 @@ from ._collections import FreezableDefaultDict, Pair from ._compat import ( NullFinder, + StrPath, install, pypy_partial, ) @@ -31,9 +32,7 @@ from importlib import import_module from importlib.abc import MetaPathFinder from itertools import starmap -from typing import Iterator, List, Mapping, Optional, Set, Union, cast - -StrPath = Union[str, "os.PathLike[str]"] +from typing import Iterable, List, Mapping, Optional, Set, cast __all__ = [ 'Distribution', @@ -124,8 +123,8 @@ def read(text, filter_=None): yield Pair(name, value) @staticmethod - def valid(line: str) -> bool: - return bool(line) and not line.startswith('#') + def valid(line: str): + return line and not line.startswith('#') class DeprecatedTuple: @@ -388,19 +387,19 @@ def from_name(cls, name: str) -> "Distribution": if not name: raise ValueError("A distribution name is required.") try: - return next(cls.discover(name=name)) + return next(iter(cls.discover(name=name))) except StopIteration: raise PackageNotFoundError(name) @classmethod - def discover(cls, **kwargs) -> Iterator["Distribution"]: - """Return an iterator of Distribution objects for all packages. + def discover(cls, **kwargs) -> Iterable["Distribution"]: + """Return an iterable of Distribution objects for all packages. Pass a ``context`` or pass keyword arguments for constructing a context. :context: A ``DistributionFinder.Context`` object. - :return: Iterator of Distribution objects for all packages. + :return: Iterable of Distribution objects for all packages. """ context = kwargs.pop('context', None) if context and kwargs: @@ -411,7 +410,7 @@ def discover(cls, **kwargs) -> Iterator["Distribution"]: ) @staticmethod - def at(path: StrPath) -> "PathDistribution": + def at(path: StrPath) -> "Distribution": """Return a Distribution for the indicated metadata path :param path: a string or path-like object @@ -638,11 +637,11 @@ def path(self) -> List[str]: return vars(self).get('path', sys.path) @abc.abstractmethod - def find_distributions(self, context=Context()) -> Iterator[Distribution]: + def find_distributions(self, context=Context()) -> Iterable[Distribution]: """ Find distributions. - Return an iterator of all Distribution instances capable of + Return an iterable of all Distribution instances capable of loading the metadata for packages matching the ``context``, a DistributionFinder.Context instance. """ @@ -775,11 +774,11 @@ class MetadataPathFinder(NullFinder, DistributionFinder): def find_distributions( self, context=DistributionFinder.Context() - ) -> Iterator["PathDistribution"]: + ) -> Iterable["PathDistribution"]: """ Find distributions. - Return an iterator of all Distribution instances capable of + Return an iterable of all Distribution instances capable of loading the metadata for packages matching ``context.name`` (or all names if ``None`` indicated) along the paths in the list of directories ``context.path``. @@ -863,10 +862,10 @@ def distribution(distribution_name) -> Distribution: return Distribution.from_name(distribution_name) -def distributions(**kwargs) -> Iterator[Distribution]: +def distributions(**kwargs) -> Iterable[Distribution]: """Get all ``Distribution`` instances in the current environment. - :return: An iterator of ``Distribution`` instances. + :return: An iterable of ``Distribution`` instances. """ return Distribution.discover(**kwargs) @@ -927,7 +926,7 @@ def requires(distribution_name) -> Optional[List[str]]: """ Return a list of requirements for the named package. - :return: An iterator of requirements, suitable for + :return: An iterable of requirements, suitable for packaging.requirement.Requirement. """ return distribution(distribution_name).requires diff --git a/importlib_metadata/_compat.py b/importlib_metadata/_compat.py index 3d78566e..638e7791 100644 --- a/importlib_metadata/_compat.py +++ b/importlib_metadata/_compat.py @@ -1,6 +1,9 @@ +import os import sys import platform +from typing import Union + __all__ = ['install', 'NullFinder', 'Protocol'] @@ -70,3 +73,10 @@ def pypy_partial(val): """ is_pypy = platform.python_implementation() == 'PyPy' return val + is_pypy + + +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]"] # pragma: no cover From 4b61913ecef7a79cec4bc5b1d2759af67bb2c311 Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Wed, 19 Apr 2023 02:41:20 +0200 Subject: [PATCH 101/131] Add test to demonstrate issue with symlinked packages --- tests/fixtures.py | 26 ++++++++++++++++++++++++++ tests/test_main.py | 26 ++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/tests/fixtures.py b/tests/fixtures.py index 6d26bb91..b39a1c4b 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -169,6 +169,32 @@ def setUp(self): build_files(DistInfoPkg.files, self.site_dir) +class DistInfoSymlinkedPkg(OnSysPath, SiteDir): + files: FilesSpec = { + "symlinked_pkg-1.0.0.dist-info": { + "METADATA": """ + Name: symlinked-pkg + Version: 1.0.0 + """, + "RECORD": "symlinked,,\n", + }, + ".symlink.target": { + "__init__.py": """ + def main(): + print("hello world") + """, + }, + # "symlinked" -> ".symlink.target", see below + } + + def setUp(self): + super().setUp() + build_files(DistInfoSymlinkedPkg.files, self.site_dir) + target = self.site_dir / ".symlink.target" + assert target.is_dir() + (self.site_dir / "symlinked").symlink_to(target, target_is_directory=True) + + class EggInfoPkg(OnSysPath, SiteDir): files: FilesSpec = { "egginfo_pkg.egg-info": { diff --git a/tests/test_main.py b/tests/test_main.py index a7650172..bec1303b 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -387,6 +387,32 @@ def test_packages_distributions_all_module_types(self): assert not any(name.endswith('.dist-info') for name in distributions) +class PackagesDistributionsDistTest( + fixtures.DistInfoPkg, + fixtures.DistInfoSymlinkedPkg, + unittest.TestCase, +): + def test_packages_distributions_on_dist_info(self): + """ + Test _top_level_inferred() on various dist-info packages. + """ + distributions = packages_distributions() + + def import_names_from_package(package_name): + return { + import_name + for import_name, package_names in distributions.items() + if package_name in package_names + } + + # distinfo-pkg has one import ('mod') inferred from RECORD + assert import_names_from_package('distinfo-pkg') == {'mod'} + + # symlinked-pkg has one import ('symlinked') inderred from RECORD which + # references a symlink to the real package dir elsewhere. + assert import_names_from_package('symlinked-pkg') == {'symlinked'} + + class PackagesDistributionsEggTest( fixtures.EggInfoPkg, fixtures.EggInfoPkgPipInstalledNoToplevel, From 0023c15d8fe1664184f6e0f80a9fca29fc3d159e Mon Sep 17 00:00:00 2001 From: Johan Herland Date: Wed, 19 Apr 2023 02:41:44 +0200 Subject: [PATCH 102/131] Attempt to fix issue with symlinked packages --- importlib_metadata/__init__.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index e9ae0d19..76ccacbc 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -31,7 +31,7 @@ from importlib import import_module from importlib.abc import MetaPathFinder from itertools import starmap -from typing import List, Mapping, Optional, cast +from typing import Iterable, Iterator, List, Mapping, Optional, cast __all__ = [ @@ -961,10 +961,32 @@ def _top_level_declared(dist): return (dist.read_text('top_level.txt') or '').split() +def _walk_dirs(package_paths: Iterable[PackagePath]) -> Iterator[PackagePath]: + for package_path in package_paths: + + def make_file(name): + result = PackagePath(name) + result.hash = None + result.size = None + result.dist = package_path.dist + return result + + real_path = package_path.locate() + real_sitedir = package_path.dist.locate_file("") # type: ignore + if real_path.is_dir() and real_path.is_symlink(): + # .files only mentions symlink, we must recurse into it ourselves: + for root, dirs, files in os.walk(real_path): + for filename in files: + real_file = pathlib.Path(root, filename) + yield make_file(real_file.relative_to(real_sitedir)) + else: + yield package_path + + def _top_level_inferred(dist): opt_names = { f.parts[0] if len(f.parts) > 1 else inspect.getmodulename(f) - for f in always_iterable(dist.files) + for f in _walk_dirs(always_iterable(dist.files)) } @pass_none From 4119720f69883cbfb2a7d12b7b9ae67f6e758ae7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 21 Apr 2023 21:46:08 -0400 Subject: [PATCH 103/131] Resolve the located directory and remove suppression of Exceptions. Ref python/cpython#103661. --- importlib_metadata/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index e9ae0d19..3c4e0a25 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -536,12 +536,12 @@ def _read_files_egginfo_installed(self): subdir = getattr(self, '_path', None) if not text or not subdir: return - with contextlib.suppress(Exception): - ret = [ - str((subdir / line).resolve().relative_to(self.locate_file(''))) - for line in text.splitlines() - ] - return map('"{}"'.format, ret) + + ret = [ + str((subdir / line).resolve().relative_to(self.locate_file('').resolve())) + for line in text.splitlines() + ] + return map('"{}"'.format, ret) def _read_files_egginfo_sources(self): """ From ac0df0d7565d781a157e1517cea6aa3d30c3beec Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 21 Apr 2023 21:57:36 -0400 Subject: [PATCH 104/131] Wrap 'subdir/line' in PosixPath to ensure the output uses posix path separators. Ref python/cpython#103661. --- importlib_metadata/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 3c4e0a25..410e7a76 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -538,7 +538,10 @@ def _read_files_egginfo_installed(self): return ret = [ - str((subdir / line).resolve().relative_to(self.locate_file('').resolve())) + (subdir / line) + .resolve() + .relative_to(self.locate_file('').resolve()) + .as_posix() for line in text.splitlines() ] return map('"{}"'.format, ret) From be58651120fb65d0fadf93dcc5f609b300968704 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 21 Apr 2023 22:30:32 -0400 Subject: [PATCH 105/131] Update changelog --- CHANGES.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index bd9cd385..bed2dd99 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,10 @@ +v6.5.1 +====== + +* python/cpython#103661: Removed excess error suppression in + ``_read_files_egginfo_installed`` and fixed path handling + on Windows. + v6.5.0 ====== From 56b0cf7be2167332c3d5af281b522fcbd98d5c5e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 21 Apr 2023 22:03:42 -0400 Subject: [PATCH 106/131] Rename 'line' to 'name' for better context. --- importlib_metadata/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 410e7a76..609b246e 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -538,11 +538,11 @@ def _read_files_egginfo_installed(self): return ret = [ - (subdir / line) + (subdir / name) .resolve() .relative_to(self.locate_file('').resolve()) .as_posix() - for line in text.splitlines() + for name in text.splitlines() ] return map('"{}"'.format, ret) From ffc289fe4de80ffe84f936786c61f71db9f1d813 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 21 Apr 2023 22:06:56 -0400 Subject: [PATCH 107/131] Reword to prefer imperative voice and more a more concise description. --- importlib_metadata/__init__.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 609b246e..b3028782 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -521,18 +521,17 @@ def _read_files_egginfo_installed(self): """ Read installed-files.txt and return lines in a similar CSV-parsable format as RECORD: each file must be placed - relative to the site-packages directory, and must also be + relative to the site-packages directory and must also be quoted (since file names can contain literal commas). This file is written when the package is installed by pip, but it might not be written for other installation methods. - Hence, even if we can assume that this file is accurate - when it exists, we cannot assume that it always exists. + Assume the file is accurate if it exists. """ text = self.read_text('installed-files.txt') - # We need to prepend the .egg-info/ subdir to the lines in this file. - # But this subdir is only available in the PathDistribution's self._path - # which is not easily accessible from this base class... + # Prepend the .egg-info/ subdir to the lines in this file. + # But this subdir is only available from PathDistribution's + # self._path. subdir = getattr(self, '_path', None) if not text or not subdir: return From 512a3df847da555b8afd648b570e44acac821fbe Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 21 Apr 2023 22:36:10 -0400 Subject: [PATCH 108/131] Use generator expression for paths. --- importlib_metadata/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index b3028782..1b0198c1 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -536,14 +536,14 @@ def _read_files_egginfo_installed(self): if not text or not subdir: return - ret = [ + paths = ( (subdir / name) .resolve() .relative_to(self.locate_file('').resolve()) .as_posix() for name in text.splitlines() - ] - return map('"{}"'.format, ret) + ) + return map('"{}"'.format, paths) def _read_files_egginfo_sources(self): """ From a4c314793e0ab96cab1b711b4c4150327642356a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 22 Apr 2023 08:31:42 -0400 Subject: [PATCH 109/131] Remove nitpick_ignore no longer needed. --- docs/conf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 0d6c66ae..6f9deda9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -65,7 +65,6 @@ # Workaround for #316 ('py:class', 'importlib_metadata.EntryPoints'), ('py:class', 'importlib_metadata.PackagePath'), - ('py:class', 'importlib_metadata.PathDistribution'), ('py:class', 'importlib_metadata.SelectableGroups'), ('py:class', 'importlib_metadata._meta._T'), # Workaround for #435 From 2e7816256d2b9aadf4299b945cc1b37ada0f367f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 22 Apr 2023 08:33:20 -0400 Subject: [PATCH 110/131] Update changelog --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 6ec9d1f3..dbc28038 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +v6.6.0 +====== + +* #449: Expanded type annotations. + v6.3.0 ====== From d2ec0473f8d4c25cc6f696e70ba110e1061e4dfe Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 10 May 2023 20:27:17 -0400 Subject: [PATCH 111/131] Replace flake8 with ruff. Fixes jaraco/skeleton#79 and sheds debt. --- .flake8 | 9 --------- pyproject.toml | 6 +++--- pytest.ini | 8 -------- setup.cfg | 6 +----- 4 files changed, 4 insertions(+), 25 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 48b2e246..00000000 --- a/.flake8 +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -max-line-length = 88 - -# jaraco/skeleton#34 -max-complexity = 10 - -extend-ignore = - # Black creates whitespace before colon - E203 diff --git a/pyproject.toml b/pyproject.toml index 60de2424..d5f3487e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,8 +13,8 @@ addopts = "--black" [tool.pytest-enabler.mypy] addopts = "--mypy" -[tool.pytest-enabler.flake8] -addopts = "--flake8" - [tool.pytest-enabler.cov] addopts = "--cov" + +[tool.pytest-enabler.ruff] +addopts = "--ruff" diff --git a/pytest.ini b/pytest.ini index 99a25199..94515aaf 100644 --- a/pytest.ini +++ b/pytest.ini @@ -7,19 +7,11 @@ filterwarnings= # Ensure ResourceWarnings are emitted default::ResourceWarning - # Suppress deprecation warning in flake8 - ignore:SelectableGroups dict interface is deprecated::flake8 - # shopkeep/pytest-black#55 ignore: is not using a cooperative constructor:pytest.PytestDeprecationWarning ignore:The \(fspath. py.path.local\) argument to BlackItem is deprecated.:pytest.PytestDeprecationWarning ignore:BlackItem is an Item subclass and should not be a collector:pytest.PytestWarning - # tholo/pytest-flake8#83 - ignore: is not using a cooperative constructor:pytest.PytestDeprecationWarning - ignore:The \(fspath. py.path.local\) argument to Flake8Item is deprecated.:pytest.PytestDeprecationWarning - ignore:Flake8Item is an Item subclass and should not be a collector:pytest.PytestWarning - # shopkeep/pytest-black#67 ignore:'encoding' argument not specified::pytest_black diff --git a/setup.cfg b/setup.cfg index c062c7b9..6b31311e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,11 +30,6 @@ testing = # upstream pytest >= 6 pytest-checkdocs >= 2.4 - pytest-flake8; \ - # workaround for tholo/pytest-flake8#87 - python_version < "3.12" - # workaround for tholo/pytest-flake8#87 - flake8 < 5 pytest-black >= 0.3.7; \ # workaround for jaraco/skeleton#22 python_implementation != "PyPy" @@ -43,6 +38,7 @@ testing = # workaround for jaraco/skeleton#22 python_implementation != "PyPy" pytest-enabler >= 1.3 + pytest-ruff # local From a12a34537aa9566011ad8d9386e5c22d5425e6a5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 10 May 2023 21:27:42 -0400 Subject: [PATCH 112/131] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblins?= =?UTF-8?q?=20(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- importlib_metadata/__init__.py | 1 - tests/test_main.py | 1 - 2 files changed, 2 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 281cfb00..857c9198 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -13,7 +13,6 @@ import functools import itertools import posixpath -import contextlib import collections from . import _adapters, _meta, _py39compat diff --git a/tests/test_main.py b/tests/test_main.py index a7650172..6eefe92b 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -5,7 +5,6 @@ import importlib import importlib_metadata import contextlib -import itertools import pyfakefs.fake_filesystem_unittest as ffs from . import fixtures From 96ebfe14538c2279b54dd19567e5922880b4fdf3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 28 May 2023 12:31:51 -0400 Subject: [PATCH 113/131] Make substitution fields more prominent and distinct from true 'skeleton' references. (#71) Fixes #70 --- README.rst | 14 +++++++------- docs/index.rst | 2 +- setup.cfg | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.rst b/README.rst index af0efb05..1f66d195 100644 --- a/README.rst +++ b/README.rst @@ -1,18 +1,18 @@ -.. image:: https://img.shields.io/pypi/v/skeleton.svg - :target: https://pypi.org/project/skeleton +.. image:: https://img.shields.io/pypi/v/PROJECT.svg + :target: https://pypi.org/project/PROJECT -.. image:: https://img.shields.io/pypi/pyversions/skeleton.svg +.. image:: https://img.shields.io/pypi/pyversions/PROJECT.svg -.. image:: https://github.com/jaraco/skeleton/workflows/tests/badge.svg - :target: https://github.com/jaraco/skeleton/actions?query=workflow%3A%22tests%22 +.. image:: https://github.com/PROJECT_PATH/workflows/tests/badge.svg + :target: https://github.com/PROJECT_PATH/actions?query=workflow%3A%22tests%22 :alt: tests .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black :alt: Code style: Black -.. .. image:: https://readthedocs.org/projects/skeleton/badge/?version=latest -.. :target: https://skeleton.readthedocs.io/en/latest/?badge=latest +.. .. image:: https://readthedocs.org/projects/PROJECT_RTD/badge/?version=latest +.. :target: https://PROJECT_RTD.readthedocs.io/en/latest/?badge=latest .. image:: https://img.shields.io/badge/skeleton-2023-informational :target: https://blog.jaraco.com/skeleton diff --git a/docs/index.rst b/docs/index.rst index 325842bb..53117d16 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,7 +7,7 @@ Welcome to |project| documentation! history -.. automodule:: skeleton +.. automodule:: PROJECT :members: :undoc-members: :show-inheritance: diff --git a/setup.cfg b/setup.cfg index 6b31311e..0cee3d34 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,10 +1,10 @@ [metadata] -name = skeleton +name = PROJECT author = Jason R. Coombs author_email = jaraco@jaraco.com -description = skeleton +description = PROJECT_DESCRIPTION long_description = file:README.rst -url = https://github.com/jaraco/skeleton +url = https://github.com/PROJECT_PATH classifiers = Development Status :: 5 - Production/Stable Intended Audience :: Developers From 4ce054b47df31b4845968043c8772ee4a604390a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 28 May 2023 20:15:16 -0400 Subject: [PATCH 114/131] Suppress EncodingWarning in build.env. Ref pypa/build#615. --- pytest.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pytest.ini b/pytest.ini index 94515aaf..3d30458f 100644 --- a/pytest.ini +++ b/pytest.ini @@ -21,4 +21,7 @@ filterwarnings= # python/cpython#100750 ignore:'encoding' argument not specified::platform + # pypa/build#615 + ignore:'encoding' argument not specified:EncodingWarning:build.env + ## end upstream From a0acaace3e29937d0711b3de8019cd3fe4799cf7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 28 May 2023 20:29:31 -0400 Subject: [PATCH 115/131] Remove reference to EncodingWarning as it doesn't exist on some Pythons. --- pytest.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest.ini b/pytest.ini index 3d30458f..d9a15ed1 100644 --- a/pytest.ini +++ b/pytest.ini @@ -22,6 +22,6 @@ filterwarnings= ignore:'encoding' argument not specified::platform # pypa/build#615 - ignore:'encoding' argument not specified:EncodingWarning:build.env + ignore:'encoding' argument not specified::build.env ## end upstream From 70a6075f98e4ac6154a8b3de72dd39073b0d885b Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Wed, 3 May 2023 04:55:22 -0700 Subject: [PATCH 116/131] gh-98040: Backport python/cpython#98059 Removed ``fixtures.NullFinder``. --------- Co-authored-by: Oleg Iarygin Co-authored-by: Adam Turner <9087854+aa-turner@users.noreply.github.com> --- tests/fixtures.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index 6d26bb91..c0b0fa32 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -349,11 +349,6 @@ def DALS(str): return textwrap.dedent(str).lstrip() -class NullFinder: - def find_module(self, name): - pass - - class ZipFixtures: root = 'tests.data' From 7d2c559119dcf2e20a66389590ee77adad6cfb88 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 8 Jun 2023 15:36:46 -0400 Subject: [PATCH 117/131] Inline the NullFinder behavior. --- tests/test_integration.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index c382a506..93981669 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -29,11 +29,14 @@ def is_installed(package_spec): class FinderTests(fixtures.Fixtures, unittest.TestCase): def test_finder_without_module(self): - class ModuleFreeFinder(fixtures.NullFinder): + class ModuleFreeFinder: """ A finder without an __module__ attribute """ + def find_module(self, name): + pass + def __getattribute__(self, name): if name == '__module__': raise AttributeError(name) From d96e7e740dcdd8b28a01f7c573126b7d35873b9e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 8 Jun 2023 15:45:51 -0400 Subject: [PATCH 118/131] Add docstring to test_integration to give some context. --- tests/test_integration.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_integration.py b/tests/test_integration.py index 93981669..7d0c13cc 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,3 +1,12 @@ +""" +Test behaviors specific to importlib_metadata. + +These tests are excluded downstream in CPython as they +test functionality only in importlib_metadata or require +behaviors ('packaging') that aren't available in the +stdlib. +""" + import unittest import packaging.requirements import packaging.version From 50e9f816c1cea50ab19ee58edf077c687af47fe5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 8 Jun 2023 15:46:08 -0400 Subject: [PATCH 119/131] Remove Python 2 compatibility in _compat.NullFinder. --- importlib_metadata/_compat.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/importlib_metadata/_compat.py b/importlib_metadata/_compat.py index 638e7791..b7abd09b 100644 --- a/importlib_metadata/_compat.py +++ b/importlib_metadata/_compat.py @@ -56,14 +56,6 @@ class NullFinder: def find_spec(*args, **kwargs): return None - # In Python 2, the import system requires finders - # to have a find_module() method, but this usage - # is deprecated in Python 3 in favor of find_spec(). - # For the purposes of this finder (i.e. being present - # on sys.meta_path but having no other import - # system functionality), the two methods are identical. - find_module = find_spec - def pypy_partial(val): """ From c10a5aa0d1f53ee318ed91d42afe730ddaaa3732 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 8 Jun 2023 15:53:06 -0400 Subject: [PATCH 120/131] Move test_interleaved_discovery from test_integration to test_main. --- tests/test_integration.py | 14 -------------- tests/test_main.py | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index 7d0c13cc..5258bada 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -15,7 +15,6 @@ from importlib_metadata import ( MetadataPathFinder, _compat, - distributions, version, ) @@ -64,16 +63,3 @@ def test_search_dist_dirs(self): """ res = MetadataPathFinder._search_paths('any-name', []) assert list(res) == [] - - def test_interleaved_discovery(self): - """ - When the search is cached, it is - possible for searches to be interleaved, so make sure - those use-cases are safe. - - Ref #293 - """ - dists = distributions() - next(dists) - version('importlib_metadata') - next(dists) diff --git a/tests/test_main.py b/tests/test_main.py index 6eefe92b..ad007595 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -204,6 +204,20 @@ def test_invalid_usage(self): with self.assertRaises(ValueError): list(distributions(context='something', name='else')) + def test_interleaved_discovery(self): + """ + Ensure interleaved searches are safe. + + When the search is cached, it is possible for searches to be + interleaved, so make sure those use-cases are safe. + + Ref #293 + """ + dists = distributions() + next(dists) + version('egginfo-pkg') + next(dists) + class DirectoryTest(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase): def test_egg_info(self): From 4ebe49067b980a932c785ab20f7ade27e879e10b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 8 Jun 2023 15:59:37 -0400 Subject: [PATCH 121/131] Remove test_search_dist_dirs as it was never used. Ref python/importlib_metadata#111. --- tests/test_integration.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index 5258bada..f7af67f3 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -13,7 +13,6 @@ from . import fixtures from importlib_metadata import ( - MetadataPathFinder, _compat, version, ) @@ -52,14 +51,3 @@ def __getattribute__(self, name): self.fixtures.enter_context(fixtures.install_finder(ModuleFreeFinder())) _compat.disable_stdlib_finder() - - -class DistSearch(unittest.TestCase): - def test_search_dist_dirs(self): - """ - Pip needs the _search_paths interface to locate - distribution metadata dirs. Protect it for PyPA - use-cases (only). Ref python/importlib_metadata#111. - """ - res = MetadataPathFinder._search_paths('any-name', []) - assert list(res) == [] From 851b921c33acf8fcfd9f009cda2bc6176ba26e94 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 18 Jun 2023 16:17:57 -0400 Subject: [PATCH 122/131] Extract _topmost and _get_toplevel_name functions. --- importlib_metadata/__init__.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 857c9198..6f20fb63 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -973,11 +973,34 @@ def _top_level_declared(dist): return (dist.read_text('top_level.txt') or '').split() +def _topmost(name: PackagePath) -> Optional[str]: + """ + Return the top-most parent as long as there is a parent. + """ + top, *rest = name.parts + return top if rest else None + + +def _get_toplevel_name(name: PackagePath) -> str: + """ + Infer a possibly importable module name from a name presumed on + sys.path. + + >>> _get_toplevel_name(PackagePath('foo.py')) + 'foo' + >>> _get_toplevel_name(PackagePath('foo.pyc')) + 'foo' + >>> _get_toplevel_name(PackagePath('foo/__init__.py')) + 'foo' + """ + return _topmost(name) or ( + # python/typeshed#10328 + inspect.getmodulename(name) # type: ignore + ) + + def _top_level_inferred(dist): - opt_names = { - f.parts[0] if len(f.parts) > 1 else inspect.getmodulename(f) - for f in always_iterable(dist.files) - } + opt_names = set(map(_get_toplevel_name, always_iterable(dist.files))) @pass_none def importable_name(name): From e50ebd77363dd59af06fb0fb1633c5e1e7fa9151 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 18 Jun 2023 16:17:57 -0400 Subject: [PATCH 123/131] Extract _topmost and _get_toplevel_name functions. --- importlib_metadata/__init__.py | 56 +++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 4bf232ee..37b664f4 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -31,8 +31,7 @@ from importlib import import_module from importlib.abc import MetaPathFinder from itertools import starmap -from typing import Iterable, Iterator, List, Mapping, Optional, Set, cast - +from typing import Iterable, List, Mapping, Optional, Set, cast __all__ = [ 'Distribution', @@ -974,35 +973,42 @@ def _top_level_declared(dist): return (dist.read_text('top_level.txt') or '').split() -def _walk_dirs(package_paths: Iterable[PackagePath]) -> Iterator[PackagePath]: - for package_path in package_paths: +def _topmost(name: PackagePath) -> Optional[str]: + """ + Return the top-most parent as long as there is a parent. + """ + top, *rest = name.parts + return top if rest else None - def make_file(name): - result = PackagePath(name) - result.hash = None - result.size = None - result.dist = package_path.dist - return result - real_path = package_path.locate() - real_sitedir = package_path.dist.locate_file("") # type: ignore - if real_path.is_dir() and real_path.is_symlink(): - # .files only mentions symlink, we must recurse into it ourselves: - for root, dirs, files in os.walk(real_path): - for filename in files: - real_file = pathlib.Path(root, filename) - yield make_file(real_file.relative_to(real_sitedir)) - else: - yield package_path +def _get_toplevel_name(name: PackagePath) -> str: + """ + Infer a possibly importable module name from a name presumed on + sys.path. + + >>> _get_toplevel_name(PackagePath('foo.py')) + 'foo' + >>> _get_toplevel_name(PackagePath('foo')) + 'foo' + >>> _get_toplevel_name(PackagePath('foo.pyc')) + 'foo' + >>> _get_toplevel_name(PackagePath('foo.dist-info')) + 'foo.dist-info' + >>> _get_toplevel_name(PackagePath('foo.pth')) + 'foo.pth' + >>> _get_toplevel_name(PackagePath('foo/__init__.py')) + 'foo' + """ + return _topmost(name) or ( + # python/typeshed#10328 + inspect.getmodulename(name) # type: ignore + or str(name) + ) def _top_level_inferred(dist): - opt_names = { - f.parts[0] if len(f.parts) > 1 else inspect.getmodulename(f) - for f in _walk_dirs(always_iterable(dist.files)) - } + opt_names = set(map(_get_toplevel_name, always_iterable(dist.files))) - @pass_none def importable_name(name): return '.' not in name From 1c4b32878693c41843cd5f04a2c5d2ca8fd86a23 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 18 Jun 2023 16:33:30 -0400 Subject: [PATCH 124/131] Capture that _get_toplevel_name can return None. --- importlib_metadata/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 6f20fb63..68329964 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -981,7 +981,7 @@ def _topmost(name: PackagePath) -> Optional[str]: return top if rest else None -def _get_toplevel_name(name: PackagePath) -> str: +def _get_toplevel_name(name: PackagePath) -> Optional[str]: """ Infer a possibly importable module name from a name presumed on sys.path. @@ -992,6 +992,8 @@ def _get_toplevel_name(name: PackagePath) -> str: 'foo' >>> _get_toplevel_name(PackagePath('foo/__init__.py')) 'foo' + >>> _get_toplevel_name(PackagePath('foo.pth')) + >>> _get_toplevel_name(PackagePath('foo.dist-info')) """ return _topmost(name) or ( # python/typeshed#10328 From 7a5e025f51e88cf49092fb33e2cb5bea82dc7ff5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 18 Jun 2023 16:46:33 -0400 Subject: [PATCH 125/131] Streamline the test to check one expectation (the standard dist-info expectation is handled by other tests above. --- tests/test_main.py | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index 5d3f39c6..5f653f3d 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -401,29 +401,15 @@ def test_packages_distributions_all_module_types(self): class PackagesDistributionsDistTest( - fixtures.DistInfoPkg, fixtures.DistInfoSymlinkedPkg, unittest.TestCase, ): - def test_packages_distributions_on_dist_info(self): + def test_packages_distributions_symlinked_top_level(self): """ - Test _top_level_inferred() on various dist-info packages. + Distribution is resolvable from a simple top-level symlink in RECORD. + See #452. """ - distributions = packages_distributions() - - def import_names_from_package(package_name): - return { - import_name - for import_name, package_names in distributions.items() - if package_name in package_names - } - - # distinfo-pkg has one import ('mod') inferred from RECORD - assert import_names_from_package('distinfo-pkg') == {'mod'} - - # symlinked-pkg has one import ('symlinked') inderred from RECORD which - # references a symlink to the real package dir elsewhere. - assert import_names_from_package('symlinked-pkg') == {'symlinked'} + assert packages_distributions()['symlinked'] == ['symlinked-pkg'] class PackagesDistributionsEggTest( From fa705d37265d25581eb9bfd5c1f41ea11c94743b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 18 Jun 2023 16:50:32 -0400 Subject: [PATCH 126/131] Inline the symlink setup. --- tests/fixtures.py | 26 -------------------------- tests/test_main.py | 26 ++++++++++++++++++++++++-- 2 files changed, 24 insertions(+), 28 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index e0413cc8..c0b0fa32 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -169,32 +169,6 @@ def setUp(self): build_files(DistInfoPkg.files, self.site_dir) -class DistInfoSymlinkedPkg(OnSysPath, SiteDir): - files: FilesSpec = { - "symlinked_pkg-1.0.0.dist-info": { - "METADATA": """ - Name: symlinked-pkg - Version: 1.0.0 - """, - "RECORD": "symlinked,,\n", - }, - ".symlink.target": { - "__init__.py": """ - def main(): - print("hello world") - """, - }, - # "symlinked" -> ".symlink.target", see below - } - - def setUp(self): - super().setUp() - build_files(DistInfoSymlinkedPkg.files, self.site_dir) - target = self.site_dir / ".symlink.target" - assert target.is_dir() - (self.site_dir / "symlinked").symlink_to(target, target_is_directory=True) - - class EggInfoPkg(OnSysPath, SiteDir): files: FilesSpec = { "egginfo_pkg.egg-info": { diff --git a/tests/test_main.py b/tests/test_main.py index 5f653f3d..0bcb7ac9 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -401,14 +401,36 @@ def test_packages_distributions_all_module_types(self): class PackagesDistributionsDistTest( - fixtures.DistInfoSymlinkedPkg, - unittest.TestCase, + fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase ): def test_packages_distributions_symlinked_top_level(self): """ Distribution is resolvable from a simple top-level symlink in RECORD. See #452. """ + + files: fixtures.FilesSpec = { + "symlinked_pkg-1.0.0.dist-info": { + "METADATA": """ + Name: symlinked-pkg + Version: 1.0.0 + """, + "RECORD": "symlinked,,\n", + }, + ".symlink.target": { + "__init__.py": """ + def main(): + print("hello world") + """, + }, + # "symlinked" -> ".symlink.target", see below + } + + fixtures.build_files(files, self.site_dir) + target = self.site_dir / ".symlink.target" + assert target.is_dir() + (self.site_dir / "symlinked").symlink_to(target, target_is_directory=True) + assert packages_distributions()['symlinked'] == ['symlinked-pkg'] From 7a19e8a4a933f00b12d26719ddb5b474045817ae Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 18 Jun 2023 16:52:51 -0400 Subject: [PATCH 127/131] Consolidate PackageDistributions tests. --- tests/test_main.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index 0bcb7ac9..fbf79a62 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -399,10 +399,6 @@ def test_packages_distributions_all_module_types(self): assert not any(name.endswith('.dist-info') for name in distributions) - -class PackagesDistributionsDistTest( - fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase -): def test_packages_distributions_symlinked_top_level(self): """ Distribution is resolvable from a simple top-level symlink in RECORD. From 62144eb57ba48ed6dafc0d7e5694a1b34fe95141 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 18 Jun 2023 17:17:30 -0400 Subject: [PATCH 128/131] Update _path to jaraco.path 3.6 with symlink support. --- tests/_path.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/tests/_path.py b/tests/_path.py index 71a70438..9762c5ec 100644 --- a/tests/_path.py +++ b/tests/_path.py @@ -1,4 +1,4 @@ -# from jaraco.path 3.5 +# from jaraco.path 3.6 import functools import pathlib @@ -11,7 +11,13 @@ from typing_extensions import Protocol, runtime_checkable # type: ignore -FilesSpec = Dict[str, Union[str, bytes, 'FilesSpec']] # type: ignore +class Symlink(str): + """ + A string indicating the target of a symlink. + """ + + +FilesSpec = Dict[str, Union[str, bytes, Symlink, 'FilesSpec']] # type: ignore @runtime_checkable @@ -28,6 +34,9 @@ def write_text(self, content, **kwargs): def write_bytes(self, content): ... # pragma: no cover + def symlink_to(self, target): + ... # pragma: no cover + def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker: return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore @@ -51,12 +60,16 @@ def build( ... "__init__.py": "", ... }, ... "baz.py": "# Some code", - ... } + ... "bar.py": Symlink("baz.py"), + ... }, + ... "bing": Symlink("foo"), ... } >>> target = getfixture('tmp_path') >>> build(spec, target) >>> target.joinpath('foo/baz.py').read_text(encoding='utf-8') '# Some code' + >>> target.joinpath('bing/bar.py').read_text(encoding='utf-8') + '# Some code' """ for name, contents in spec.items(): create(contents, _ensure_tree_maker(prefix) / name) @@ -79,8 +92,8 @@ def _(content: str, path): @create.register -def _(content: str, path): - path.write_text(content, encoding='utf-8') +def _(content: Symlink, path): + path.symlink_to(content) class Recording: @@ -107,3 +120,6 @@ def write_text(self, content, **kwargs): def mkdir(self, **kwargs): return + + def symlink_to(self, target): + pass From d5f723f8ec5623740593011bae63df16be50a1c3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 18 Jun 2023 17:19:51 -0400 Subject: [PATCH 129/131] Utilize the new Symlink in preparing the test case. --- tests/test_main.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index fbf79a62..4543e21d 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -9,6 +9,7 @@ from . import fixtures from ._context import suppress +from ._path import Symlink from importlib_metadata import ( Distribution, EntryPoint, @@ -419,14 +420,10 @@ def main(): print("hello world") """, }, - # "symlinked" -> ".symlink.target", see below + "symlinked": Symlink(".symlink.target"), } fixtures.build_files(files, self.site_dir) - target = self.site_dir / ".symlink.target" - assert target.is_dir() - (self.site_dir / "symlinked").symlink_to(target, target_is_directory=True) - assert packages_distributions()['symlinked'] == ['symlinked-pkg'] From e8bc802866861ee32049fd60cf9e4f3e30592f26 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 18 Jun 2023 17:23:58 -0400 Subject: [PATCH 130/131] Update changelog. --- CHANGES.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index c7e5889c..255bbd32 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,10 @@ +v6.7.0 +====== + +* #453: When inferring top-level names that are importable for + distributions in ``package_distributions``, now symlinks to + other directories are honored. + v6.6.0 ====== From 53e47d9ae8e7784da051e264376bdb7221a45871 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 18 Jun 2023 17:31:33 -0400 Subject: [PATCH 131/131] Remove '__init__.py', not needed. --- tests/test_main.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index 4543e21d..79181bf4 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -414,12 +414,7 @@ def test_packages_distributions_symlinked_top_level(self): """, "RECORD": "symlinked,,\n", }, - ".symlink.target": { - "__init__.py": """ - def main(): - print("hello world") - """, - }, + ".symlink.target": {}, "symlinked": Symlink(".symlink.target"), }