From f3ff0541b6967ee91be3f572c5afe4f559436aa1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 26 Jan 2019 12:12:55 -0500 Subject: [PATCH 001/480] Add Tidelift template --- README.rst | 11 +++++++++++ docs/_templates/tidelift-sidebar.html | 6 ++++++ docs/conf.py | 4 ++++ 3 files changed, 21 insertions(+) create mode 100644 README.rst create mode 100644 docs/_templates/tidelift-sidebar.html create mode 100644 docs/conf.py diff --git a/README.rst b/README.rst new file mode 100644 index 00000000..5d77d8d3 --- /dev/null +++ b/README.rst @@ -0,0 +1,11 @@ +.. image:: https://tidelift.com/badges/github/GROUP/PROJECT + :target: https://tidelift.com/subscription/pkg/pypi-PROJECT?utm_source=pypi-PROJECT&utm_medium=readme + + +Security Contact +================ + +If you wish to report a security vulnerability, the public disclosure +of which may exacerbate the risk, please +`Contact Tidelift security `_, +which will coordinate the fix and disclosure privately. diff --git a/docs/_templates/tidelift-sidebar.html b/docs/_templates/tidelift-sidebar.html new file mode 100644 index 00000000..c89c0f09 --- /dev/null +++ b/docs/_templates/tidelift-sidebar.html @@ -0,0 +1,6 @@ +

Professional support

+ +

+Professionally-supported {{ project }} is available with the +Tidelift Subscription. +

diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 00000000..d0287332 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,4 @@ + +# Custom sidebar templates, maps document names to template names. +templates_path = ['_templates'] +html_sidebars = {'index': 'tidelift-sidebar.html'} From c6655951aa8292127f01d53c337da1da642efe74 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 26 Jan 2019 13:18:15 -0500 Subject: [PATCH 002/480] Rely on alabaster theme to support sidebar rendering. --- docs/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/conf.py b/docs/conf.py index d0287332..3d109305 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,4 +1,5 @@ # Custom sidebar templates, maps document names to template names. +html_theme = 'alabaster' templates_path = ['_templates'] html_sidebars = {'index': 'tidelift-sidebar.html'} From 9edd5b701549833ebbfb354c072962c58e5394ac Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 22 Mar 2019 19:54:23 -0400 Subject: [PATCH 003/480] Use nicer, simpler phrasing --- README.rst | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 5d77d8d3..35762622 100644 --- a/README.rst +++ b/README.rst @@ -1,11 +1,9 @@ .. image:: https://tidelift.com/badges/github/GROUP/PROJECT :target: https://tidelift.com/subscription/pkg/pypi-PROJECT?utm_source=pypi-PROJECT&utm_medium=readme - Security Contact ================ -If you wish to report a security vulnerability, the public disclosure -of which may exacerbate the risk, please -`Contact Tidelift security `_, -which will coordinate the fix and disclosure privately. +To report a security vulnerability, please use the +`Tidelift security contact `_. +Tidelift will coordinate the fix and disclosure. From e8db26a129378279833620da952a1ecc6cef937b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 17 May 2019 16:17:33 -0400 Subject: [PATCH 004/480] Add support for automatic publishing of release notes --- .travis.yml | 6 ++++++ tox.ini | 7 +++++++ 2 files changed, 13 insertions(+) create mode 100644 .travis.yml create mode 100644 tox.ini diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..13f58071 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +jobs: + include: + - stage: deploy + env: + # TIDELIFT_TOKEN + - secure: ... diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..8f419798 --- /dev/null +++ b/tox.ini @@ -0,0 +1,7 @@ +[testenv:release] +passenv = + TIDELIFT_TOKEN +deps = + jaraco.tidelift +commands = + python -m jaraco.tidelift.publish-release-notes From 534678f19edd76e6f93ad31900d9a92d00ef25fd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 17 May 2019 16:37:45 -0400 Subject: [PATCH 005/480] Use technique for environment passing matching that found in jaraco/skeleton --- .travis.yml | 1 + tox.ini | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 13f58071..a9afa23f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,3 +4,4 @@ jobs: env: # TIDELIFT_TOKEN - secure: ... + TOX_TESTENV_PASSENV="TIDELIFT_TOKEN" diff --git a/tox.ini b/tox.ini index 8f419798..7f9b6c1e 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,4 @@ [testenv:release] -passenv = - TIDELIFT_TOKEN deps = jaraco.tidelift commands = From fd540b6535f56647581df61333bac7eadc1309f8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 22 Aug 2019 20:01:38 -0400 Subject: [PATCH 006/480] Move Tidelift token into Travis configuration --- .travis.yml | 7 ------- tox.ini | 2 ++ 2 files changed, 2 insertions(+), 7 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index a9afa23f..00000000 --- a/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ -jobs: - include: - - stage: deploy - env: - # TIDELIFT_TOKEN - - secure: ... - TOX_TESTENV_PASSENV="TIDELIFT_TOKEN" diff --git a/tox.ini b/tox.ini index 7f9b6c1e..35053514 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,7 @@ [testenv:release] deps = jaraco.tidelift +passenv = + TIDELIFT_TOKEN commands = python -m jaraco.tidelift.publish-release-notes From 211ff140a4bd82fcb4f01a5569cdc86e4badea8b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 9 Sep 2019 16:36:40 +0100 Subject: [PATCH 007/480] Update badge URL --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 35762622..420bfb4f 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -.. image:: https://tidelift.com/badges/github/GROUP/PROJECT +.. image:: https://tidelift.com/badges/package/pypi/PROJECT :target: https://tidelift.com/subscription/pkg/pypi-PROJECT?utm_source=pypi-PROJECT&utm_medium=readme Security Contact From 8f99a0c1b8ee2cb28a8bdb1811ef96da68636d1b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 9 Sep 2019 17:30:03 +0100 Subject: [PATCH 008/480] Add funding reference to project --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..230b556c --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +tidelift: pypi/PROJECT From 1c187ad0cf50fbc14626f63cb669a9ec5949012f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 9 Sep 2019 17:45:58 +0100 Subject: [PATCH 009/480] List sidebars to avoid errors looking for template 't' --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 3d109305..dbf962dd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -2,4 +2,4 @@ # Custom sidebar templates, maps document names to template names. html_theme = 'alabaster' templates_path = ['_templates'] -html_sidebars = {'index': 'tidelift-sidebar.html'} +html_sidebars = {'index': ['tidelift-sidebar.html']} From b10e8186a9305d1f899f0fd7d1319b7b15ed1ecd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 30 Nov 2019 20:33:33 -0500 Subject: [PATCH 010/480] Rebrand to 'For Enterprise' --- docs/_templates/tidelift-sidebar.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/_templates/tidelift-sidebar.html b/docs/_templates/tidelift-sidebar.html index c89c0f09..ce48f46b 100644 --- a/docs/_templates/tidelift-sidebar.html +++ b/docs/_templates/tidelift-sidebar.html @@ -1,6 +1,6 @@ -

Professional support

+

For Enterprise

Professionally-supported {{ project }} is available with the -Tidelift Subscription. +Tidelift Subscription.

From bb357fb88ca6df10b84235eff42bafa1461b2b75 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 30 Nov 2019 20:38:21 -0500 Subject: [PATCH 011/480] Add a 'For Enterprise' section to the README --- README.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.rst b/README.rst index 420bfb4f..7b317c71 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,15 @@ .. image:: https://tidelift.com/badges/package/pypi/PROJECT :target: https://tidelift.com/subscription/pkg/pypi-PROJECT?utm_source=pypi-PROJECT&utm_medium=readme +For Enterprise +============== + +Available as part of the Tidelift Subscription. + +This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. + +`Learn more `_. + Security Contact ================ From 34d87688eae5d10970f3c8269ac01bcca4ad0229 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 11 Jan 2020 17:23:00 -0500 Subject: [PATCH 012/480] Include token passthrough for azure pipelines publish stage. --- azure-pipelines.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 azure-pipelines.yml diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 00000000..01bfa5f5 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,2 @@ + env: + TIDELIFT_TOKEN: $(Tidelift-token) From fb8d9c43500c06f6f2286ea7c7ae452d41cce412 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 14 Nov 2020 20:32:18 -0500 Subject: [PATCH 013/480] Move Tidelift release note publishing to Github Actions. --- .github/workflows/main.yml | 6 ++++++ azure-pipelines.yml | 2 -- 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/main.yml delete mode 100644 azure-pipelines.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..01999cab --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,6 @@ +jobs: + release: + steps: + - name: Release + env: + TIDELIFT_TOKEN: ${{ secrets.TIDELIFT_TOKEN }} diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index 01bfa5f5..00000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,2 +0,0 @@ - env: - TIDELIFT_TOKEN: $(Tidelift-token) From 4b1568c71b5299b39a5579bb778c8930991448b8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 22 Mar 2021 19:58:47 -0400 Subject: [PATCH 014/480] Tidelift no longer requires or expects publishing release notes. --- tox.ini | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 tox.ini diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 35053514..00000000 --- a/tox.ini +++ /dev/null @@ -1,7 +0,0 @@ -[testenv:release] -deps = - jaraco.tidelift -passenv = - TIDELIFT_TOKEN -commands = - python -m jaraco.tidelift.publish-release-notes From 842eb1423ba76dadbf568f9b0abf04a233711529 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 23 Mar 2021 18:52:10 -0400 Subject: [PATCH 015/480] Remove Tidelift from main.yml, no longer needed --- .github/workflows/main.yml | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index 01999cab..00000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,6 +0,0 @@ -jobs: - release: - steps: - - name: Release - env: - TIDELIFT_TOKEN: ${{ secrets.TIDELIFT_TOKEN }} From 918a415b41225f442d6e9b319ecebff19b52a1a5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 18 Jul 2021 14:48:16 -0400 Subject: [PATCH 016/480] Implement tidelift banner using a Sphinx directive implemented by jaraco.tidelift. --- docs/_templates/tidelift-sidebar.html | 6 ------ docs/conf.py | 6 +----- docs/index.rst | 1 + setup.cfg | 4 ++++ 4 files changed, 6 insertions(+), 11 deletions(-) delete mode 100644 docs/_templates/tidelift-sidebar.html create mode 100644 docs/index.rst create mode 100644 setup.cfg diff --git a/docs/_templates/tidelift-sidebar.html b/docs/_templates/tidelift-sidebar.html deleted file mode 100644 index ce48f46b..00000000 --- a/docs/_templates/tidelift-sidebar.html +++ /dev/null @@ -1,6 +0,0 @@ -

For Enterprise

- -

-Professionally-supported {{ project }} is available with the -Tidelift Subscription. -

diff --git a/docs/conf.py b/docs/conf.py index dbf962dd..bd8564ed 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1 @@ - -# Custom sidebar templates, maps document names to template names. -html_theme = 'alabaster' -templates_path = ['_templates'] -html_sidebars = {'index': ['tidelift-sidebar.html']} +extensions += ['jaraco.tidelift'] diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..6e321fdc --- /dev/null +++ b/docs/index.rst @@ -0,0 +1 @@ +.. tidelift-referral-banner:: diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..798b1033 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,4 @@ +[options.extras_require] +docs = + # upstream + jaraco.tidelift >= 1.4 From 1a6b828304e7a8896b55d9ebf83f481ba7ebd568 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Fri, 22 Apr 2022 17:43:46 +0200 Subject: [PATCH 017/480] Inject check job into CI workflow as ultimate flag (#55) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch adds a job that is able to accurately signal whether all the expectations of the required jobs to succeed are met. This job can then be used as a source of truth for judging whether "CI passes" and can be used in the branch protection. It also plays a role of a convenient "gate" — this is the only job that would have to be listed in the branch protection as opposed to listing every single job name generated by the test matrix (and they all have different names — it's not possible to just select one `test` job name). Ref: https://github.com/re-actors/alls-green#why --- .github/workflows/main.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5424298d..b54fd6a1 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -27,8 +27,23 @@ jobs: - name: Run tests run: tox + check: # This job does nothing and is only used for the branch protection + if: always() + + needs: + - test + + runs-on: ubuntu-latest + + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} + release: - needs: test + needs: + - check if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') runs-on: ubuntu-latest From c68e382f7c1a5a1240fd5cc7d17e8e9240a26b1c Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 19 May 2022 16:48:44 +0100 Subject: [PATCH 018/480] gh-92417: `importlib` docs: remove references to unsupported Python versions (GH-92424) Co-authored-by: CAM Gerlach --- docs/using.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 0bbc0df8..4c6c961b 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -8,9 +8,9 @@ package metadata. 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 -:mod:`importlib.resources` in Python 3.7 -and newer (backported as :doc:`importlib_resources ` for older versions of -Python), this can eliminate the need to use the older and less efficient +:mod:`importlib.resources` (with new features backported to the +`importlib_resources`_ package), this can eliminate the need to use the older +and less efficient ``pkg_resources`` package. By "installed package" we generally mean a third-party package installed into From b06c3826ec2765c433dd773c27a5263969bd7799 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 21 May 2022 10:57:05 -0400 Subject: [PATCH 019/480] Removed shadowed variable and make _name_from_stem a staticmethod. --- importlib_metadata/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 5ac8be23..122eed40 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -958,8 +958,9 @@ def _normalized_name(self): stem = os.path.basename(str(self._path)) return self._name_from_stem(stem) or super()._normalized_name - def _name_from_stem(self, stem): - name, ext = os.path.splitext(stem) + @staticmethod + def _name_from_stem(stem): + _, ext = os.path.splitext(stem) if ext not in ('.dist-info', '.egg-info'): return name, sep, rest = stem.partition('-') From 4461855128600c120032a446624b7872ffe7cc70 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 21 May 2022 10:58:13 -0400 Subject: [PATCH 020/480] Add some (failing) tests for _name_from_stem. Ref #377 --- importlib_metadata/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 122eed40..75dc9dfe 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -960,6 +960,14 @@ def _normalized_name(self): @staticmethod def _name_from_stem(stem): + """ + >>> PathDistribution._name_from_stem('foo-3.0.egg-info') + 'foo' + >>> PathDistribution._name_from_stem('CherryPy-3.0.dist-info') + 'CherryPy' + >>> PathDistribution._name_from_stem('face.egg-info') + 'face' + """ _, ext = os.path.splitext(stem) if ext not in ('.dist-info', '.egg-info'): return From 01843df1917d6b3b1a2b4a466c211919abc4de0e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 21 May 2022 11:00:20 -0400 Subject: [PATCH 021/480] Fix issue when metadata on the file system has no version. No more egg on face. Ref #377. --- 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 75dc9dfe..e4e3a506 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -968,10 +968,10 @@ def _name_from_stem(stem): >>> PathDistribution._name_from_stem('face.egg-info') 'face' """ - _, ext = os.path.splitext(stem) + filename, ext = os.path.splitext(stem) if ext not in ('.dist-info', '.egg-info'): return - name, sep, rest = stem.partition('-') + name, sep, rest = filename.partition('-') return name From 9deee509e7f2d32e4305bded506883c1c84d6f5e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 21 May 2022 11:01:29 -0400 Subject: [PATCH 022/480] Update changelog. Ref #377. --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 91e3bcba..4f941897 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +v4.11.4 +======= + +* #377: In ``PathDistribution._name_from_stem``, avoid including + parts of the extension in the result. + v4.11.3 ======= From 4c7ef61e6e83bd93e497307abc3484fb3d038262 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 21 May 2022 11:14:19 -0400 Subject: [PATCH 023/480] Add another test for _name_from_stem. --- importlib_metadata/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index e4e3a506..98fc639c 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -967,6 +967,7 @@ def _name_from_stem(stem): 'CherryPy' >>> PathDistribution._name_from_stem('face.egg-info') 'face' + >>> PathDistribution._name_from_stem('foo.bar') """ filename, ext = os.path.splitext(stem) if ext not in ('.dist-info', '.egg-info'): From 6eb6f7176f59850aa9dbb7c7bbb3de6891d0f0e1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 21 May 2022 11:31:30 -0400 Subject: [PATCH 024/480] Add test expanding expectation on unique packages to include normalization. Ref #377. --- tests/test_api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 26731884..3f75cebb 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -89,15 +89,15 @@ def test_entry_points_distribution(self): self.assertIn(ep.dist.name, ('distinfo-pkg', 'egginfo-pkg')) self.assertEqual(ep.dist.version, "1.0.0") - def test_entry_points_unique_packages(self): + def test_entry_points_unique_packages_normalized(self): """ Entry points should only be exposed for the first package - on sys.path with a given name. + on sys.path with a given name (even when normalized). """ alt_site_dir = self.fixtures.enter_context(fixtures.tempdir()) self.fixtures.enter_context(self.add_sys_path(alt_site_dir)) alt_pkg = { - "distinfo_pkg-1.1.0.dist-info": { + "DistInfo_pkg-1.1.0.dist-info": { "METADATA": """ Name: distinfo-pkg Version: 1.1.0 From 45b8841aa0113366a11a1367d0fc332e8b580afc Mon Sep 17 00:00:00 2001 From: Benoit Pierre Date: Thu, 12 May 2022 21:15:45 +0200 Subject: [PATCH 025/480] fix `PathDistribution._normalized_name` implementation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - apply PEP 503 normalization to the extracted names (e.g.: `zope..inter_face-4.2.dist-info` must yield the name `zope_inter_face`) `entry_points(…)` can yield the entry-points of a shadowed distribution. For example: with a version of `mypkg` in the system' site-packages directory when working from another development checkout of the same package (with a `mypkg.egg-info` directory mishandled by the first bug). --- 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 98fc639c..b1db5a6e 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -956,7 +956,10 @@ def _normalized_name(self): normalized name from the file system path. """ stem = os.path.basename(str(self._path)) - return self._name_from_stem(stem) or super()._normalized_name + return ( + pass_none(Prepared.normalize)(self._name_from_stem(stem)) + or super()._normalized_name + ) @staticmethod def _name_from_stem(stem): From 21bacb91565b109a8154f6632b79e49c07750d17 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 21 May 2022 11:41:52 -0400 Subject: [PATCH 026/480] Update changelog. --- CHANGES.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 4f941897..7e98644d 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,6 +3,10 @@ v4.11.4 * #377: In ``PathDistribution._name_from_stem``, avoid including parts of the extension in the result. +# #381: In ``PathDistribution._normalized_name``, ensure names + loaded from the stem of the filename are also normalized, ensuring + duplicate entry points by packages varying only by non-normalized + name are hidden. v4.11.3 ======= From 1d840cbf3ebaba37f02dcfb3906e98fac968f486 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 21 May 2022 11:46:15 -0400 Subject: [PATCH 027/480] Reference the main PR where the change occurred. --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 7e98644d..6fbcfd7c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,7 +1,7 @@ v4.11.4 ======= -* #377: In ``PathDistribution._name_from_stem``, avoid including +* #379: In ``PathDistribution._name_from_stem``, avoid including parts of the extension in the result. # #381: In ``PathDistribution._normalized_name``, ensure names loaded from the stem of the filename are also normalized, ensuring From 9c80e090cc24c0fbaf6ba2e8716afebb72768289 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 21 May 2022 11:54:02 -0400 Subject: [PATCH 028/480] Use build_files for rendering fixtures in test_main. --- tests/test_main.py | 52 ++++++++++++++++++++++------------------------ 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index 1a64af56..bba2624b 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,7 +1,6 @@ import re import json import pickle -import textwrap import unittest import warnings import importlib @@ -82,11 +81,12 @@ def pkg_with_dashes(site_dir): Create minimal metadata for a package with dashes in the name (and thus underscores in the filename). """ - metadata_dir = site_dir / 'my_pkg.dist-info' - metadata_dir.mkdir() - metadata = metadata_dir / 'METADATA' - with metadata.open('w', encoding='utf-8') as strm: - strm.write('Version: 1.0\n') + contents = { + 'my_pkg.dist-info': { + 'METADATA': 'VERSION: 1.0\n', + }, + } + fixtures.build_files(contents, site_dir) return 'my-pkg' def test_dashes_in_dist_name_found_as_underscores(self): @@ -103,11 +103,12 @@ def pkg_with_mixed_case(site_dir): Create minimal metadata for a package with mixed case in the name. """ - metadata_dir = site_dir / 'CherryPy.dist-info' - metadata_dir.mkdir() - metadata = metadata_dir / 'METADATA' - with metadata.open('w', encoding='utf-8') as strm: - strm.write('Version: 1.0\n') + contents = { + 'CherryPy.dist-info': { + 'METADATA': 'VERSION: 1.0\n', + }, + } + fixtures.build_files(contents, site_dir) return 'CherryPy' def test_dist_name_found_as_any_case(self): @@ -127,11 +128,12 @@ def pkg_with_non_ascii_description(site_dir): Create minimal metadata for a package with non-ASCII in the description. """ - metadata_dir = site_dir / 'portend.dist-info' - metadata_dir.mkdir() - metadata = metadata_dir / 'METADATA' - with metadata.open('w', encoding='utf-8') as fp: - fp.write('Description: pôrˈtend') + contents = { + 'portend.dist-info': { + 'METADATA': 'Description: pôrˈtend', + }, + } + fixtures.build_files(contents, site_dir) return 'portend' @staticmethod @@ -140,19 +142,15 @@ def pkg_with_non_ascii_description_egg_info(site_dir): Create minimal metadata for an egg-info package with non-ASCII in the description. """ - metadata_dir = site_dir / 'portend.dist-info' - metadata_dir.mkdir() - metadata = metadata_dir / 'METADATA' - with metadata.open('w', encoding='utf-8') as fp: - fp.write( - textwrap.dedent( - """ + contents = { + 'portend.dist-info': { + 'METADATA': """ Name: portend - pôrˈtend - """ - ).strip() - ) + pôrˈtend""", + }, + } + fixtures.build_files(contents, site_dir) return 'portend' def test_metadata_loads(self): From 2df8b5a1e6d641f96627c82483caab940be42263 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 21 May 2022 12:04:28 -0400 Subject: [PATCH 029/480] Consolidate logic for generating minimal metadata. --- tests/test_main.py | 33 +++++++++------------------------ 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/tests/test_main.py b/tests/test_main.py index bba2624b..c88ce35a 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -76,46 +76,31 @@ def test_resolve_without_attr(self): class NameNormalizationTests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase): @staticmethod - def pkg_with_dashes(site_dir): + def make_pkg(name): """ - Create minimal metadata for a package with dashes - in the name (and thus underscores in the filename). + Create minimal metadata for a dist-info package with + the indicated name on the file system. """ - contents = { - 'my_pkg.dist-info': { + return { + f'{name}.dist-info': { 'METADATA': 'VERSION: 1.0\n', }, } - fixtures.build_files(contents, site_dir) - return 'my-pkg' def test_dashes_in_dist_name_found_as_underscores(self): """ For a package with a dash in the name, the dist-info metadata uses underscores in the name. Ensure the metadata loads. """ - pkg_name = self.pkg_with_dashes(self.site_dir) - assert version(pkg_name) == '1.0' - - @staticmethod - def pkg_with_mixed_case(site_dir): - """ - Create minimal metadata for a package with mixed case - in the name. - """ - contents = { - 'CherryPy.dist-info': { - 'METADATA': 'VERSION: 1.0\n', - }, - } - fixtures.build_files(contents, site_dir) - return 'CherryPy' + fixtures.build_files(self.make_pkg('my_pkg'), self.site_dir) + assert version('my-pkg') == '1.0' def test_dist_name_found_as_any_case(self): """ Ensure the metadata loads when queried with any case. """ - pkg_name = self.pkg_with_mixed_case(self.site_dir) + pkg_name = 'CherryPy' + fixtures.build_files(self.make_pkg(pkg_name), self.site_dir) assert version(pkg_name) == '1.0' assert version(pkg_name.lower()) == '1.0' assert version(pkg_name.upper()) == '1.0' From a3ad5f708054e1a86a97a086f8ec01c34549eed6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 21 May 2022 11:56:47 -0400 Subject: [PATCH 030/480] Extract '_unique' for identifying unique distributions. --- importlib_metadata/__init__.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index b1db5a6e..29ce1175 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -1015,6 +1015,15 @@ def version(distribution_name): return distribution(distribution_name).version +_unique = functools.partial( + unique_everseen, + key=operator.attrgetter('_normalized_name'), +) +""" +Wrapper for ``distributions`` to return unique distributions by name. +""" + + def entry_points(**params) -> Union[EntryPoints, SelectableGroups]: """Return EntryPoint objects for all installed packages. @@ -1032,10 +1041,8 @@ def entry_points(**params) -> Union[EntryPoints, SelectableGroups]: :return: EntryPoints or SelectableGroups for all installed packages. """ - norm_name = operator.attrgetter('_normalized_name') - unique = functools.partial(unique_everseen, key=norm_name) eps = itertools.chain.from_iterable( - dist.entry_points for dist in unique(distributions()) + dist.entry_points for dist in _unique(distributions()) ) return SelectableGroups.load(eps).select(**params) From da980921eaf63bdaf5b0242356a85ba815cb930d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 21 May 2022 12:15:34 -0400 Subject: [PATCH 031/480] Add test for _unique. --- tests/test_main.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_main.py b/tests/test_main.py index c88ce35a..215662dd 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -13,6 +13,7 @@ EntryPoint, MetadataPathFinder, PackageNotFoundError, + _unique, distributions, entry_points, metadata, @@ -105,6 +106,21 @@ def test_dist_name_found_as_any_case(self): assert version(pkg_name.lower()) == '1.0' assert version(pkg_name.upper()) == '1.0' + def test_unique_distributions(self): + """ + Two distributions varying only by non-normalized name on + the file system should resolve as the same. + """ + fixtures.build_files(self.make_pkg('abc'), self.site_dir) + before = list(_unique(distributions())) + + alt_site_dir = self.fixtures.enter_context(fixtures.tempdir()) + self.fixtures.enter_context(self.add_sys_path(alt_site_dir)) + fixtures.build_files(self.make_pkg('ABC'), alt_site_dir) + after = list(_unique(distributions())) + + assert len(after) == len(before) + class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase): @staticmethod From 4f33d4040a5a45e2d758b356dd079f5462b9a6c8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 22 May 2022 21:53:50 -0400 Subject: [PATCH 032/480] Fix bug in changelog --- CHANGES.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 6fbcfd7c..bf7098cc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -3,7 +3,7 @@ v4.11.4 * #379: In ``PathDistribution._name_from_stem``, avoid including parts of the extension in the result. -# #381: In ``PathDistribution._normalized_name``, ensure names +* #381: In ``PathDistribution._normalized_name``, ensure names loaded from the stem of the filename are also normalized, ensuring duplicate entry points by packages varying only by non-normalized name are hidden. From f2a256ab19330066c0ee299666189f5f32768ccb Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 22 May 2022 21:59:07 -0400 Subject: [PATCH 033/480] Fix reference in docs build. --- docs/using.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 4c6c961b..8bd92f62 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -8,8 +8,9 @@ package metadata. 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 -:mod:`importlib.resources` (with new features backported to the -`importlib_resources`_ package), this can eliminate the need to use the older +:mod:`importlib.resources` (with new features backported to +:doc:`importlib_resources `), +this package can eliminate the need to use the older and less efficient ``pkg_resources`` package. From 10bf1b1fb9e09e9836bea9e2edec620cd9eea7f9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 4 Jun 2022 21:37:40 -0400 Subject: [PATCH 034/480] Add Python 3.11 into the matrix using workaround from actions/setup-python#213. Drop 3.9 from matrix for efficiency. --- .github/workflows/main.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b54fd6a1..6468ee0d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,9 +7,11 @@ jobs: strategy: matrix: python: - - 3.7 - - 3.9 - - "3.10" + # Build on pre-releases until stable, then stable releases. + # actions/setup-python#213 + - ~3.7.0-0 + - ~3.10.0-0 + - ~3.11.0-0 platform: - ubuntu-latest - macos-latest From a4f5b769793af19f7b858816889c1bf026f55f5c Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade Date: Sun, 5 Jun 2022 04:47:15 +0300 Subject: [PATCH 035/480] Update base URL for PEPs (#61) --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 4ae74093..319b1384 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,7 +19,7 @@ ), dict( pattern=r'PEP[- ](?P\d+)', - url='https://www.python.org/dev/peps/pep-{pep_number:0>4}/', + url='https://peps.python.org/pep-{pep_number:0>4}/', ), ], ) From 74f337fec4c233b3a6750fa64b61d03c189d9416 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sun, 5 Jun 2022 02:50:24 +0100 Subject: [PATCH 036/480] Update Github actions to v3 (#62) --- .github/workflows/main.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6468ee0d..948da052 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,9 +18,9 @@ jobs: - windows-latest runs-on: ${{ matrix.platform }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: ${{ matrix.python }} - name: Install tox @@ -50,9 +50,9 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v3 with: python-version: "3.10" - name: Install tox From e719f86c138a750f0c4599cd01cb8067b1ca95c8 Mon Sep 17 00:00:00 2001 From: wim glenn Date: Sun, 5 Jun 2022 15:01:02 -0500 Subject: [PATCH 037/480] exclude build env from cov reporting (#60) * Update .coveragerc * Keep whitespace consistent. Co-authored-by: Jason R. Coombs --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 6a34e662..01164f62 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,6 +2,7 @@ omit = # leading `*/` for pytest-dev/pytest-cov#456 */.tox/* + */pep517-build-env-* [report] show_missing = True From 6dcd157a7057ec8e1f1f6afebe2115f55df4aaed Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 15 Jun 2022 20:57:40 -0400 Subject: [PATCH 038/480] Prefer spaces for rst. Fixes jaraco/skeleton#64. --- .editorconfig | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.editorconfig b/.editorconfig index b8aeea17..304196f8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -14,3 +14,6 @@ max_line_length = 88 [*.{yml,yaml}] indent_style = space indent_size = 2 + +[*.rst] +indent_style = space From 0d3e49e06c6aa6f6f75e6062640cba9f23c3d743 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 15 Jun 2022 22:14:01 -0400 Subject: [PATCH 039/480] Update license. Fixes python/importlib_metadata#387. --- LICENSE | 209 +++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 199 insertions(+), 10 deletions(-) diff --git a/LICENSE b/LICENSE index be7e092b..d6456956 100644 --- a/LICENSE +++ b/LICENSE @@ -1,13 +1,202 @@ -Copyright 2017-2019 Jason R. Coombs, Barry Warsaw -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ -http://www.apache.org/licenses/LICENSE-2.0 + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. From 2678a7e82d581c07691575d90cd255b64ee63a27 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 22 Jun 2022 15:56:54 -0400 Subject: [PATCH 040/480] Honor PEP 518 with pytest-enabler. --- pyproject.toml | 8 ++++---- setup.cfg | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 190b3551..60de2424 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,14 +7,14 @@ skip-string-normalization = true [tool.setuptools_scm] -[pytest.enabler.black] +[tool.pytest-enabler.black] addopts = "--black" -[pytest.enabler.mypy] +[tool.pytest-enabler.mypy] addopts = "--mypy" -[pytest.enabler.flake8] +[tool.pytest-enabler.flake8] addopts = "--flake8" -[pytest.enabler.cov] +[tool.pytest-enabler.cov] addopts = "--cov" diff --git a/setup.cfg b/setup.cfg index 3b7ac309..baa37e5e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,7 +38,7 @@ testing = pytest-mypy >= 0.9.1; \ # workaround for jaraco/skeleton#22 python_implementation != "PyPy" - pytest-enabler >= 1.0.1 + pytest-enabler >= 1.3 # local From ebb57b17e34b0fb38db3f7861e5cc8befc1fce01 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 23 Jun 2022 17:18:21 -0400 Subject: [PATCH 041/480] Exclude pip-run from coverage. It's just noise. --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 9b849bb4..1b8c50e9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -7,6 +7,7 @@ omit = prepare/* */_itertools.py exercises.py + */pip-run-* [report] show_missing = True From a4ae953dca38da768bcd1786aeba84bada32efb4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 23 Jun 2022 16:56:20 -0400 Subject: [PATCH 042/480] Add xfail test capturing new expectation. --- tests/fixtures.py | 16 ++++++++++++++++ tests/test_main.py | 10 ++++++++++ 2 files changed, 26 insertions(+) diff --git a/tests/fixtures.py b/tests/fixtures.py index 08a478ac..6d9a9d2b 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -5,6 +5,7 @@ import pathlib import tempfile import textwrap +import functools import contextlib from .py39compat import FS_NONASCII @@ -294,3 +295,18 @@ def setUp(self): # Add self.zip_name to the front of sys.path. self.resources = contextlib.ExitStack() self.addCleanup(self.resources.close) + + +def parameterize(*args_set): + """Run test method with a series of parameters.""" + + def wrapper(func): + @functools.wraps(func) + def _inner(self): + for args in args_set: + with self.subTest(**args): + func(self, **args) + + return _inner + + return wrapper diff --git a/tests/test_main.py b/tests/test_main.py index 215662dd..041220f1 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,6 +1,7 @@ import re import json import pickle +import pytest import unittest import warnings import importlib @@ -50,6 +51,15 @@ def test_new_style_classes(self): self.assertIsInstance(Distribution, type) self.assertIsInstance(MetadataPathFinder, type) + @pytest.mark.xfail(reason="Not implemented") + @fixtures.parameterize( + dict(name=None), + dict(name=''), + ) + def test_invalid_inputs_to_from_name(self, name): + with self.assertRaises(Exception): + Distribution.from_name(name) + class ImportTests(fixtures.DistInfoPkg, unittest.TestCase): def test_import_nonexistent_module(self): From d3fe031dbad4590896829f18ecbd8d9d8a132f53 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 24 Jun 2022 14:02:11 -0400 Subject: [PATCH 043/480] Add comment about the compatibility factor. --- importlib_metadata/_compat.py | 1 + 1 file changed, 1 insertion(+) diff --git a/importlib_metadata/_compat.py b/importlib_metadata/_compat.py index 8fe4e4e3..3d78566e 100644 --- a/importlib_metadata/_compat.py +++ b/importlib_metadata/_compat.py @@ -8,6 +8,7 @@ try: from typing import Protocol except ImportError: # pragma: no cover + # Python 3.7 compatibility from typing_extensions import Protocol # type: ignore From eb19c647519c754dd93b42a0c421101af73cf7a4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 24 Jun 2022 21:15:31 -0400 Subject: [PATCH 044/480] In Distribution.from_name, require a non-empty string. Fixes python/cpython#93259. --- importlib_metadata/__init__.py | 5 ++++- tests/test_main.py | 2 -- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 29ce1175..bb1105b6 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -548,7 +548,7 @@ def locate_file(self, path): """ @classmethod - def from_name(cls, name): + def from_name(cls, name: str): """Return the Distribution for the given package name. :param name: The name of the distribution package to search for. @@ -556,7 +556,10 @@ def from_name(cls, name): package, if found. :raises PackageNotFoundError: When the named package's distribution metadata cannot be found. + :raises ValueError: When an invalid value is supplied for name. """ + if not name: + raise ValueError("A distribution name is required.") for resolver in cls._discover_resolvers(): dists = resolver(DistributionFinder.Context(name=name)) dist = next(iter(dists), None) diff --git a/tests/test_main.py b/tests/test_main.py index 041220f1..921f5d9c 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,7 +1,6 @@ import re import json import pickle -import pytest import unittest import warnings import importlib @@ -51,7 +50,6 @@ def test_new_style_classes(self): self.assertIsInstance(Distribution, type) self.assertIsInstance(MetadataPathFinder, type) - @pytest.mark.xfail(reason="Not implemented") @fixtures.parameterize( dict(name=None), dict(name=''), From 344a6ffc612eec611592e7686264ced72f64da5a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 24 Jun 2022 22:18:07 -0400 Subject: [PATCH 045/480] Refactor Distribution.from_name to avoid return in loop and unnecessary None sentinel. --- 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 29ce1175..5c52a6d3 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -557,12 +557,13 @@ def from_name(cls, name): :raises PackageNotFoundError: When the named package's distribution metadata cannot be found. """ - for resolver in cls._discover_resolvers(): - dists = resolver(DistributionFinder.Context(name=name)) - dist = next(iter(dists), None) - if dist is not None: - return dist - else: + dists = itertools.chain.from_iterable( + resolver(DistributionFinder.Context(name=name)) + for resolver in cls._discover_resolvers() + ) + try: + return next(dists) + except StopIteration: raise PackageNotFoundError(name) @classmethod From f52757d0c8a9a555d0591a86b334a17028e2ead9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 24 Jun 2022 22:21:45 -0400 Subject: [PATCH 046/480] In Distribution.from_name, re-use discover. --- importlib_metadata/__init__.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 5c52a6d3..ac9bb0bc 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -557,12 +557,8 @@ def from_name(cls, name): :raises PackageNotFoundError: When the named package's distribution metadata cannot be found. """ - dists = itertools.chain.from_iterable( - resolver(DistributionFinder.Context(name=name)) - for resolver in cls._discover_resolvers() - ) try: - return next(dists) + return next(cls.discover(name=name)) except StopIteration: raise PackageNotFoundError(name) From 91b71494226a95251134c4fe6ea65a1dd25f495c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 25 Jun 2022 11:53:00 -0400 Subject: [PATCH 047/480] Update changelog --- CHANGES.rst | 7 +++++++ docs/conf.py | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index bf7098cc..84684eec 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,10 @@ +v4.12.0 +======= + +* py-93259: Now raise ``ValueError`` when ``None`` or an empty + string are passed to ``Distribution.from_name`` (and other + callers). + v4.11.4 ======= diff --git a/docs/conf.py b/docs/conf.py index 0192ca80..ec2bfe59 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -20,6 +20,10 @@ pattern=r'PEP[- ](?P\d+)', url='https://peps.python.org/pep-{pep_number:0>4}/', ), + dict( + pattern=r'(Python #|py-)(?P\d+)', + url='https://github.com/python/cpython/issues/{python}', + ), ], ) } From 516f2a78061d27a3baff53ce4b08f95b638f60d3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 25 Jun 2022 12:00:16 -0400 Subject: [PATCH 048/480] Fix reference in docs build. --- docs/index.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 168f7fbc..1ebbf345 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,8 +9,7 @@ and newer (backported as :doc:`importlib_resources `) this package can eliminate the need to use the older and less efficient ``pkg_resources`` package. -``importlib_metadata`` supplies a backport of -:doc:`importlib.metadata `, +``importlib_metadata`` supplies a backport of :mod:`importlib.metadata`, enabling early access to features of future Python versions and making functionality available for older Python versions. Users are encouraged to use the Python standard library where suitable and fall back to From fb47ce0b191d15d0c66db182e3ddb12e6f2306d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 9 Apr 2022 21:37:01 +0300 Subject: [PATCH 049/480] bpo-22295: use python -m pip rather than plain pip in more examples (GH-24003) --- docs/using.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 8bd92f62..d3c9a3cc 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -40,8 +40,8 @@ something into it:: $ python3 -m venv example $ source example/bin/activate - (example) $ pip install importlib_metadata - (example) $ pip install wheel + (example) $ python -m pip install importlib_metadata + (example) $ python -m pip install wheel You can get the version string for ``wheel`` by running the following:: From 2279f76e7760dbc1e26223630e5372b0506c1714 Mon Sep 17 00:00:00 2001 From: Oleg Iarygin Date: Tue, 21 Jun 2022 21:55:18 +0300 Subject: [PATCH 050/480] gh-93851: Fix all broken links in Doc/ (GH-93853) --- docs/using.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index d3c9a3cc..d49ed4e1 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -132,7 +132,7 @@ Inspect the resolved entry point:: The ``group`` and ``name`` are arbitrary values defined by the package author and usually a client will wish to resolve all entry points for a particular group. Read `the setuptools docs -`_ +`_ for more information on entry points, their definition, and usage. *Compatibility Note* From 85d104cc56f9c291f10598ffe5b54b82aa51cdf3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 25 Jun 2022 14:21:40 -0400 Subject: [PATCH 051/480] Consolidate more language between the backport and the canonical usage docs. --- docs/index.rst | 12 ------------ docs/using.rst | 11 +++++------ 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 1ebbf345..1c8d359c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,14 +1,6 @@ Welcome to |project| documentation! =================================== -``importlib_metadata`` is a library which provides an API for accessing an -installed package's metadata (see :pep:`566`), such as its entry points or its top-level -name. This functionality intends to replace most uses of ``pkg_resources`` -`entry point API`_ and `metadata API`_. Along with :mod:`importlib.resources` -and newer (backported as :doc:`importlib_resources `), -this package can eliminate the need to use the older and less -efficient ``pkg_resources`` package. - ``importlib_metadata`` supplies a backport of :mod:`importlib.metadata`, enabling early access to features of future Python versions and making functionality available for older Python versions. Users are encouraged to @@ -43,7 +35,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - - -.. _`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 diff --git a/docs/using.rst b/docs/using.rst index d49ed4e1..6dbe17fd 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -4,14 +4,13 @@ Using :mod:`!importlib_metadata` ================================= -``importlib_metadata`` is a library that provides for access to installed -package metadata. Built in part on Python's import system, this library +``importlib_metadata`` is a library that provides access to installed +package metadata, such as its entry points or its +top-level name. 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 -:mod:`importlib.resources` (with new features backported to -:doc:`importlib_resources `), -this package can eliminate the need to use the older -and less efficient +:mod:`importlib.resources`, +this package can eliminate the need to use the older and less efficient ``pkg_resources`` package. By "installed package" we generally mean a third-party package installed into From fea1e7cdd57d330f22ac54512ae2df19083c6ec7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 11 Jul 2022 18:53:07 -0400 Subject: [PATCH 052/480] Ran pre-commit autoupdate --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index edf6f55f..af502010 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ repos: - repo: https://github.com/psf/black - rev: 22.1.0 + rev: 22.6.0 hooks: - id: black From 9c2e16e6b2d67bcada26c48e268dd1a9e378c768 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 18 Jul 2022 10:25:50 +0100 Subject: [PATCH 053/480] Capture expectations about _normalized_name compatibility in test --- tests/test_integration.py | 47 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/test_integration.py b/tests/test_integration.py index c382a506..4e51bab5 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,3 +1,5 @@ +import sys +import pathlib import unittest import packaging.requirements import packaging.version @@ -6,7 +8,10 @@ from importlib_metadata import ( MetadataPathFinder, _compat, + distribution, distributions, + entry_points, + metadata, version, ) @@ -43,6 +48,48 @@ def __getattribute__(self, name): _compat.disable_stdlib_finder() +class OldStdlibFinderTests(fixtures.DistInfoPkgOffPath, unittest.TestCase): + def setUp(self): + python_version = sys.version_info[:2] + if python_version < (3, 8) or python_version > (3, 9): + self.skipTest("Tests specific for Python 3.8/3.9") + super().setUp() + + def _meta_path_finder(self): + from importlib.metadata import DistributionFinder, PathDistribution + from importlib.util import spec_from_file_location + + path = pathlib.Path(self.site_dir) + + class CustomFinder: + @classmethod + def find_spec(cls, fullname, _path=None, _target=None): + candidate = pathlib.Path(path, *fullname.split(".")).with_suffix(".py") + if candidate.exists(): + return spec_from_file_location(fullname, candidate) + + @classmethod + def find_distributions(self, context=DistributionFinder.Context()): + for dist_info in path.glob("*.dist-info"): + yield PathDistribution(dist_info) + + return CustomFinder + + def test_compatibility_with_old_stdlib_path_distribution(self): + """ + Given a custom finder that uses Python 3.8/3.9 importlib.metadata is installed, + when importlib_metadata functions are called, there should be no exceptions. + Ref python/importlib_metadata#396. + """ + self.fixtures.enter_context(fixtures.install_finder(self._meta_path_finder())) + + assert list(distributions()) + assert distribution("distinfo_pkg") + assert version("distinfo_pkg") > "0" + assert list(metadata("distinfo_pkg")) + assert list(entry_points(group="entries")) + + class DistSearch(unittest.TestCase): def test_search_dist_dirs(self): """ From eb621bdfd8e2a9cd8bac1250ee9831ef1e0e2dc9 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 15 Jul 2022 20:07:58 +0100 Subject: [PATCH 054/480] Add compatibility layer for Python 3.8 stdlib lack of _normalized_name --- 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 8761307a..2677a81f 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -1015,9 +1015,18 @@ def version(distribution_name): return distribution(distribution_name).version +def _compat_normalized_name(dist: Distribution) -> Optional[str]: + """ + Compatibility layer that honor name normalization for distributions + that don't provide ``_normalized_name`` + (as in ``importlib.metadata`` for Python 3.8/3.9). + """ + return getattr(dist, '_normalized_name', None) or Prepared.normalize(dist.name) + + _unique = functools.partial( unique_everseen, - key=operator.attrgetter('_normalized_name'), + key=_compat_normalized_name, ) """ Wrapper for ``distributions`` to return unique distributions by name. From 56b071f60821a3f8e7cbb57cb241526762eed630 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 18 Jul 2022 11:10:02 +0100 Subject: [PATCH 055/480] Add compatibility for EntryPoint.matches in stdlib 3.8/3.9 --- importlib_metadata/__init__.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 2677a81f..3c72f4c7 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -29,7 +29,7 @@ from importlib import import_module from importlib.abc import MetaPathFinder from itertools import starmap -from typing import List, Mapping, Optional, Union +from typing import List, Mapping, Optional, Tuple, Union __all__ = [ @@ -378,7 +378,8 @@ def select(self, **params): Select entry points from self that match the given parameters (typically group and/or name). """ - return EntryPoints(ep for ep in self if ep.matches(**params)) + candidates = (_ep_matches(ep, **params) for ep in self) + return EntryPoints(ep for ep, ep_matches in candidates if ep_matches) @property def names(self): @@ -410,6 +411,15 @@ def _from_text(text): ) +def _ep_matches(ep: EntryPoint, **params) -> Tuple[EntryPoint, bool]: + """Compatibility layer for EntryPoint objects in Python 3.8/3.9 stdlib.""" + try: + return ep, ep.matches(**params) + except AttributeError: + _ep = EntryPoint(ep.name, ep.value, ep.group) + return _ep, _ep.matches(**params) + + class Deprecated: """ Compatibility add-in for mapping to indicate that @@ -1021,7 +1031,8 @@ def _compat_normalized_name(dist: Distribution) -> Optional[str]: that don't provide ``_normalized_name`` (as in ``importlib.metadata`` for Python 3.8/3.9). """ - return getattr(dist, '_normalized_name', None) or Prepared.normalize(dist.name) + normalized = getattr(dist, '_normalized_name', None) + return normalized or Prepared.normalize(getattr(dist, "name", "")) _unique = functools.partial( From dff9d32c6a848460fef83fcb627d75b4911f9747 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 18 Jul 2022 11:14:20 +0100 Subject: [PATCH 056/480] Add type annotations to EntryPoint.name/value/group This is required to fix typecheck false positive. --- importlib_metadata/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 3c72f4c7..e26c8fb6 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -189,6 +189,10 @@ class EntryPoint(DeprecatedTuple): following the attr, and following any extras. """ + name: str + value: str + group: str + dist: Optional['Distribution'] = None def __init__(self, name, value, group): From 718cdef2d14f8817b4296d7f1bf235d3699999cf Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 18 Jul 2022 11:17:12 +0100 Subject: [PATCH 057/480] Make sure Python 3.8/3.9 compatibility tests run on CI --- .github/workflows/main.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 19b03429..5ee72836 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,6 +16,11 @@ jobs: - ubuntu-latest - macos-latest - windows-latest + include: + - platform: ubuntu-latest + python: ~3.8.0-0 + - platform: ubuntu-latest + python: ~3.9.0-0 runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v3 From 65d65e14cc9d6df50d1288138a908acf12e7327c Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 18 Jul 2022 11:55:55 +0100 Subject: [PATCH 058/480] Workaround missing distribution name --- importlib_metadata/__init__.py | 15 +++++++++++---- tests/test_integration.py | 22 +++++++++++++++++++++- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index e26c8fb6..77ddc63a 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -29,7 +29,7 @@ from importlib import import_module from importlib.abc import MetaPathFinder from itertools import starmap -from typing import List, Mapping, Optional, Tuple, Union +from typing import List, Mapping, Optional, Tuple, Union, cast __all__ = [ @@ -1031,12 +1031,19 @@ def version(distribution_name): def _compat_normalized_name(dist: Distribution) -> Optional[str]: """ - Compatibility layer that honor name normalization for distributions + Compatibility shim to honor name normalization for distributions that don't provide ``_normalized_name`` (as in ``importlib.metadata`` for Python 3.8/3.9). """ - normalized = getattr(dist, '_normalized_name', None) - return normalized or Prepared.normalize(getattr(dist, "name", "")) + try: + return dist._normalized_name + except AttributeError: + if "PathDistribution" in dist.__class__.__name__: + dist_ = cast(PathDistribution, dist) # old implementation + return PathDistribution(dist_._path)._normalized_name + elif hasattr(dist, "name"): + return Prepared.normalize(dist.name) + raise _unique = functools.partial( diff --git a/tests/test_integration.py b/tests/test_integration.py index 4e51bab5..b11ed2af 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -56,11 +56,26 @@ def setUp(self): super().setUp() def _meta_path_finder(self): - from importlib.metadata import DistributionFinder, PathDistribution + from importlib.metadata import ( + Distribution, + DistributionFinder, + PathDistribution, + ) from importlib.util import spec_from_file_location path = pathlib.Path(self.site_dir) + class CustomDistribution(Distribution): + def __init__(self, name, path): + self.name = name + self._path_distribution = PathDistribution(path) + + def read_text(self, filename): + return self._path_distribution.read_text(filename) + + def locate_file(self, path): + return self._path_distribution.locate_file(path) + class CustomFinder: @classmethod def find_spec(cls, fullname, _path=None, _target=None): @@ -72,6 +87,8 @@ def find_spec(cls, fullname, _path=None, _target=None): def find_distributions(self, context=DistributionFinder.Context()): for dist_info in path.glob("*.dist-info"): yield PathDistribution(dist_info) + name, _, _ = str(dist_info).partition("-") + yield CustomDistribution(name + "_custom", dist_info) return CustomFinder @@ -85,8 +102,11 @@ def test_compatibility_with_old_stdlib_path_distribution(self): assert list(distributions()) assert distribution("distinfo_pkg") + assert distribution("distinfo_pkg_custom") assert version("distinfo_pkg") > "0" + assert version("distinfo_pkg_custom") > "0" assert list(metadata("distinfo_pkg")) + assert list(metadata("distinfo_pkg_custom")) assert list(entry_points(group="entries")) From ab40e390a1c6e737f81eee7edd93df1a872ce9ce Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 18 Jul 2022 13:36:34 +0100 Subject: [PATCH 059/480] Fix typecheck error by simplifying compatibility shim --- importlib_metadata/__init__.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 77ddc63a..c03a8667 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -29,7 +29,7 @@ from importlib import import_module from importlib.abc import MetaPathFinder from itertools import starmap -from typing import List, Mapping, Optional, Tuple, Union, cast +from typing import List, Mapping, Optional, Tuple, Union __all__ = [ @@ -1038,12 +1038,7 @@ def _compat_normalized_name(dist: Distribution) -> Optional[str]: try: return dist._normalized_name except AttributeError: - if "PathDistribution" in dist.__class__.__name__: - dist_ = cast(PathDistribution, dist) # old implementation - return PathDistribution(dist_._path)._normalized_name - elif hasattr(dist, "name"): - return Prepared.normalize(dist.name) - raise + return Prepared.normalize(getattr(dist, "name", dist.metadata['Name'])) _unique = functools.partial( From 9333ab549351aac0b59fc2670ae3e6dd0e9fd906 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 18 Jul 2022 13:41:30 +0100 Subject: [PATCH 060/480] Add "short circuit" optimization --- 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 c03a8667..0e3c6aa1 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -1038,7 +1038,7 @@ def _compat_normalized_name(dist: Distribution) -> Optional[str]: try: return dist._normalized_name except AttributeError: - return Prepared.normalize(getattr(dist, "name", dist.metadata['Name'])) + return Prepared.normalize(getattr(dist, "name", None) or dist.metadata['Name']) _unique = functools.partial( From 325916c8240b8b3c7c41f24b664ca591e8555ea9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Jul 2022 10:12:46 -0400 Subject: [PATCH 061/480] Use '-dev' for every Python version. Ref actions/setup-python#213. --- .github/workflows/main.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 948da052..de49ba8a 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,11 +7,9 @@ jobs: strategy: matrix: python: - # Build on pre-releases until stable, then stable releases. - # actions/setup-python#213 - - ~3.7.0-0 - - ~3.10.0-0 - - ~3.11.0-0 + - 3.7 + - '3.10' + - '3.11' platform: - ubuntu-latest - macos-latest @@ -22,7 +20,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v3 with: - python-version: ${{ matrix.python }} + python-version: ${{ matrix.python }}-dev - name: Install tox run: | python -m pip install tox From 424717b9e9f7c66379e809eb4e35daae827a1533 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Jul 2022 10:18:19 -0400 Subject: [PATCH 062/480] Use Python 3.11 for cutting releases. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index de49ba8a..3ce62d92 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -52,7 +52,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v3 with: - python-version: "3.10" + python-version: "3.11-dev" - name: Install tox run: | python -m pip install tox From e6224990448ee51a2b13d72d9e25620d1c9aa6c0 Mon Sep 17 00:00:00 2001 From: "C.A.M. Gerlach" Date: Fri, 29 Jul 2022 20:57:19 -0500 Subject: [PATCH 063/480] Add intersphinx support for packaging.python.org --- docs/conf.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index ea6385d2..cd4c4356 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -45,6 +45,13 @@ ), ) +intersphinx_mapping.update( + packaging=( + 'https://packaging.python.org/en/latest/', + None, + ), +) + # Workaround for #316 nitpick_ignore = [ ('py:class', 'importlib_metadata.EntryPoints'), From 4ff9dcc4e41623ba26ed250dbcdb8f9fa65ee28f Mon Sep 17 00:00:00 2001 From: "C.A.M. Gerlach" Date: Fri, 29 Jul 2022 21:50:11 -0500 Subject: [PATCH 064/480] Clarify import vs distribution package terms in using.rst --- docs/using.rst | 63 ++++++++++++++++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 23 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 6dbe17fd..20d3b874 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -4,21 +4,23 @@ Using :mod:`!importlib_metadata` ================================= -``importlib_metadata`` is a library that provides access to installed -package metadata, such as its entry points or its -top-level name. Built in part on Python's import system, this library +``importlib_metadata`` is a library that provides access to +the metadata of an installed distribution package, such as its entry points +or its top-level import package name. +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 :mod:`importlib.resources`, this package can eliminate the need to use the older and less efficient ``pkg_resources`` package. -By "installed package" we generally mean a third-party package installed into -Python's ``site-packages`` directory via tools such as `pip +By "installed package", we generally mean a third-party distribution package +installed into Python's ``site-packages`` directory via tools such as `pip `_. Specifically, -it means a package with either a discoverable ``dist-info`` or ``egg-info`` +it means a distribution with either a discoverable ``dist-info`` or ``egg-info`` directory, and metadata defined by :pep:`566` or its older specifications. -By default, package metadata can live on the file system or in zip archives on +By default, distribution metadata can live on the file system +or in zip archives on :data:`sys.path`. Through an extension mechanism, the metadata can live almost anywhere. @@ -33,7 +35,8 @@ anywhere. Overview ======== -Let's say you wanted to get the version string for a package you've installed +Let's say you wanted to get the version string for a +distribution package you've installed using ``pip``. We start by creating a virtual environment and installing something into it:: @@ -151,7 +154,8 @@ interface to retrieve entry points by group. Distribution metadata --------------------- -Every distribution includes some metadata, which you can extract using the +Every distribution package includes some metadata, +which you can extract using the ``metadata()`` function:: >>> wheel_metadata = metadata('wheel') @@ -182,7 +186,8 @@ all the metadata in a JSON-compatible form per PEP 566:: Distribution versions --------------------- -The ``version()`` function is the quickest way to get a distribution's version +The ``version()`` function is the quickest way to get a +distribution package's version number, as a string:: >>> version('wheel') @@ -195,7 +200,8 @@ Distribution files ------------------ You can also get the full set of files contained within a distribution. The -``files()`` function takes a distribution package name and returns all of the +``files()`` function takes a distribution package name +and returns all of the files installed by this distribution. Each file object returned is a ``PackagePath``, a :class:`pathlib.PurePath` derived object with additional ``dist``, ``size``, and ``hash`` properties as indicated by the metadata. For example:: @@ -240,19 +246,24 @@ distribution is not known to have the metadata present. Distribution requirements ------------------------- -To get the full set of requirements for a distribution, use the ``requires()`` +To get the full set of requirements for a distribution package, +use the ``requires()`` function:: >>> requires('wheel') ["pytest (>=3.0.0) ; extra == 'test'", "pytest-cov ; extra == 'test'"] -Package distributions ---------------------- +.. _package-distributions: +.. _import-distribution-package-mapping: + +Mapping import to distribution packages +--------------------------------------- -A convenience method to resolve the distribution or -distributions (in the case of a namespace package) for top-level -Python packages or modules:: +A convenience method to resolve the distribution package +name (or names, in the case of a namespace package) +that provide each importable top-level +Python module or import package:: >>> packages_distributions() {'importlib_metadata': ['importlib-metadata'], 'yaml': ['PyYAML'], 'jaraco': ['jaraco.classes', 'jaraco.functools'], ...} @@ -264,7 +275,8 @@ Distributions While the above API is the most common and convenient usage, you can get all of that information from the ``Distribution`` class. A ``Distribution`` is an -abstract object that represents the metadata for a Python package. You can +abstract object that represents the metadata for +a Python distribution package. You can get the ``Distribution`` instance:: >>> from importlib_metadata import distribution @@ -291,7 +303,9 @@ for additional details. Distribution Discovery ====================== -By default, this package provides built-in support for discovery of metadata for file system and zip file packages. This metadata finder search defaults to ``sys.path``, but varies slightly in how it interprets those values from how other import machinery does. In particular: +By default, this package provides built-in support for discovery of metadata +for file system and zip file distribution packages. +This metadata finder search defaults to ``sys.path``, but varies slightly in how it interprets those values from how other import machinery does. In particular: - ``importlib_metadata`` does not honor :class:`bytes` objects on ``sys.path``. - ``importlib_metadata`` will incidentally honor :py:class:`pathlib.Path` objects on ``sys.path`` even though such values will be ignored for imports. @@ -300,15 +314,18 @@ By default, this package provides built-in support for discovery of metadata for Extending the search algorithm ============================== -Because package metadata is not available through :data:`sys.path` searches, or -package loaders directly, the metadata for a package is found through import +Because 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, ``importlib.metadata`` queries the list of :term:`meta path finders ` on :data:`sys.meta_path`. By default ``importlib_metadata`` installs a finder for distribution packages -found on the file system. This finder doesn't actually find any *packages*, -but it can find the packages' metadata. +found on the file system. +This finder doesn't actually find any *distributions*, +but it can find their metadata. The abstract class :py:class:`importlib.abc.MetaPathFinder` defines the interface expected of finders by Python's import system. From 43233ab4132dc6f455b355560fb225719ccd192b Mon Sep 17 00:00:00 2001 From: "C.A.M. Gerlach" Date: Fri, 29 Jul 2022 21:54:02 -0500 Subject: [PATCH 065/480] Link distribution/import package terms to PyPA glossary on 1st use --- docs/using.rst | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 20d3b874..b306b7fd 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -5,8 +5,9 @@ ================================= ``importlib_metadata`` is a library that provides access to -the metadata of an installed distribution package, such as its entry points -or its top-level import package name. +the metadata of an installed :term:`packaging:Distribution Package`, +such as its entry points +or its top-level :term:`packaging:Import Package` name. 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 @@ -36,7 +37,7 @@ Overview ======== Let's say you wanted to get the version string for a -distribution package you've installed +:term:`packaging:Distribution Package` you've installed using ``pip``. We start by creating a virtual environment and installing something into it:: @@ -154,7 +155,7 @@ interface to retrieve entry points by group. Distribution metadata --------------------- -Every distribution package includes some metadata, +Every :term:`packaging:Distribution Package` includes some metadata, which you can extract using the ``metadata()`` function:: @@ -187,7 +188,7 @@ Distribution versions --------------------- The ``version()`` function is the quickest way to get a -distribution package's version +:term:`packaging:Distribution Package`'s version number, as a string:: >>> version('wheel') @@ -200,7 +201,7 @@ Distribution files ------------------ You can also get the full set of files contained within a distribution. The -``files()`` function takes a distribution package name +``files()`` function takes a :term:`packaging:Distribution Package` name and returns all of the files installed by this distribution. Each file object returned is a ``PackagePath``, a :class:`pathlib.PurePath` derived object with additional ``dist``, @@ -246,7 +247,7 @@ distribution is not known to have the metadata present. Distribution requirements ------------------------- -To get the full set of requirements for a distribution package, +To get the full set of requirements for a :term:`packaging:Distribution Package`, use the ``requires()`` function:: @@ -260,10 +261,10 @@ function:: Mapping import to distribution packages --------------------------------------- -A convenience method to resolve the distribution package +A convenience method to resolve the :term:`packaging:Distribution Package` name (or names, in the case of a namespace package) that provide each importable top-level -Python module or import package:: +Python module or :term:`packaging:Import Package`:: >>> packages_distributions() {'importlib_metadata': ['importlib-metadata'], 'yaml': ['PyYAML'], 'jaraco': ['jaraco.classes', 'jaraco.functools'], ...} @@ -276,7 +277,7 @@ Distributions While the above API is the most common and convenient usage, you can get all of that information from the ``Distribution`` class. A ``Distribution`` is an abstract object that represents the metadata for -a Python distribution package. You can +a Python :term:`packaging:Distribution Package`. You can get the ``Distribution`` instance:: >>> from importlib_metadata import distribution @@ -304,7 +305,7 @@ Distribution Discovery ====================== By default, this package provides built-in support for discovery of metadata -for file system and zip file distribution packages. +for file system and zip file :term:`packaging:Distribution Package`\s. This metadata finder search defaults to ``sys.path``, but varies slightly in how it interprets those values from how other import machinery does. In particular: - ``importlib_metadata`` does not honor :class:`bytes` objects on ``sys.path``. @@ -314,7 +315,7 @@ This metadata finder search defaults to ``sys.path``, but varies slightly in how Extending the search algorithm ============================== -Because distribution package metadata +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 From c9f2d6eeba31662225d5c7973bfa7fe1ecdb89e8 Mon Sep 17 00:00:00 2001 From: "C.A.M. Gerlach" Date: Fri, 29 Jul 2022 22:31:17 -0500 Subject: [PATCH 066/480] Add admonition further clarifying import vs distribution packages --- docs/using.rst | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index b306b7fd..99349279 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -15,11 +15,26 @@ API`_ and `metadata API`_ of ``pkg_resources``. Along with this package can eliminate the need to use the older and less efficient ``pkg_resources`` package. -By "installed package", we generally mean a third-party distribution package -installed into Python's ``site-packages`` directory via tools such as `pip -`_. Specifically, -it means a distribution with either a discoverable ``dist-info`` or ``egg-info`` -directory, and metadata defined by :pep:`566` or its older specifications. +``importlib_metadata`` operates on third-party *distribution packages* +installed into Python's ``site-packages`` directory via tools such as +`pip `_. +Specifically, it works with distributions with discoverable +``dist-info`` or ``egg-info`` directories, +and metadata defined by :pep:`566` or its older specifications. + +.. important:: + + These are *not* necessarily equivalent to or correspond 1:1 with + the top-level *import package* names + that can be imported inside Python code. + One *distribution package* can contain multiple *import packages* + (and single modules), + and one top-level *import package* + may map to multiple *distribution packages* + if it is a namespace package. + You can use :ref:`package_distributions() ` + to get a mapping between them. + By default, distribution metadata can live on the file system or in zip archives on :data:`sys.path`. Through an extension mechanism, the metadata can live almost From 03a07751ed10b3b6c6614e7ae21934c719a61cc8 Mon Sep 17 00:00:00 2001 From: "C.A.M. Gerlach" Date: Fri, 29 Jul 2022 22:34:31 -0500 Subject: [PATCH 067/480] Link to canonical core metadata spec instead of an old historical PEP --- docs/using.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 99349279..d5d64715 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -20,7 +20,7 @@ installed into Python's ``site-packages`` directory via tools such as `pip `_. Specifically, it works with distributions with discoverable ``dist-info`` or ``egg-info`` directories, -and metadata defined by :pep:`566` or its older specifications. +and metadata defined by the :ref:`packaging:core-metadata`. .. important:: @@ -312,8 +312,8 @@ instance:: >>> dist.metadata['License'] 'MIT' -The full set of available metadata is not described here. See :pep:`566` -for additional details. +The full set of available metadata is not described here. +See the :ref:`packaging:core-metadata` for additional details. Distribution Discovery From 01ab745c7ec9a578ad91575b2c35b4099533e8a1 Mon Sep 17 00:00:00 2001 From: "C.A.M. Gerlach" Date: Fri, 29 Jul 2022 23:34:09 -0500 Subject: [PATCH 068/480] Mention API ref & migration guide in importlib_metadata See Also --- docs/using.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/using.rst b/docs/using.rst index 6dbe17fd..c893d932 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -28,6 +28,12 @@ anywhere. https://importlib-metadata.readthedocs.io/ The documentation for ``importlib_metadata``, which supplies a backport of ``importlib.metadata``. + This includes an `API reference + `__ + for this module's classes and functions, + as well as a `migration guide + `__ + for existing users of ``pkg_resources``. Overview From c64902b8cafa8062398ef173278a21b042b03a77 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 30 Jul 2022 19:26:15 -0400 Subject: [PATCH 069/480] Pin flake8. Workaround for tholo/pytest-flake8#87. --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.cfg b/setup.cfg index baa37e5e..1ab93501 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,6 +31,8 @@ testing = pytest >= 6 pytest-checkdocs >= 2.4 pytest-flake8 + # workaround for tholo/pytest-flake8#87 + flake8 < 5 pytest-black >= 0.3.7; \ # workaround for jaraco/skeleton#22 python_implementation != "PyPy" From abcc15683d3abe229a0e0d07f1afa05a24e2ef8c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 18 Aug 2022 16:06:12 -0400 Subject: [PATCH 070/480] Update to setup-python v4. Fixes jaraco/skeleton#65. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3ce62d92..d17b64d6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,7 +18,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Setup Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }}-dev - name: Install tox From 47c2cb324e20f784289496ef3a7b19a1cd23d196 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 18 Aug 2022 21:42:40 -0400 Subject: [PATCH 071/480] Also update release to v4 --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d17b64d6..63fa1e8e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -50,7 +50,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Setup Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v4 with: python-version: "3.11-dev" - name: Install tox From a156b8503c44452a53111d477bcd7c2adec40c88 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 31 Aug 2022 15:55:11 +0100 Subject: [PATCH 072/480] Extract tests for Python 3.9 compatibility into their own file --- tests/test_integration.py | 65 ---------------------------------- tests/test_py39compat.py | 74 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 65 deletions(-) create mode 100644 tests/test_py39compat.py diff --git a/tests/test_integration.py b/tests/test_integration.py index b11ed2af..6d02aa75 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -8,10 +8,7 @@ from importlib_metadata import ( MetadataPathFinder, _compat, - distribution, distributions, - entry_points, - metadata, version, ) @@ -48,68 +45,6 @@ def __getattribute__(self, name): _compat.disable_stdlib_finder() -class OldStdlibFinderTests(fixtures.DistInfoPkgOffPath, unittest.TestCase): - def setUp(self): - python_version = sys.version_info[:2] - if python_version < (3, 8) or python_version > (3, 9): - self.skipTest("Tests specific for Python 3.8/3.9") - super().setUp() - - def _meta_path_finder(self): - from importlib.metadata import ( - Distribution, - DistributionFinder, - PathDistribution, - ) - from importlib.util import spec_from_file_location - - path = pathlib.Path(self.site_dir) - - class CustomDistribution(Distribution): - def __init__(self, name, path): - self.name = name - self._path_distribution = PathDistribution(path) - - def read_text(self, filename): - return self._path_distribution.read_text(filename) - - def locate_file(self, path): - return self._path_distribution.locate_file(path) - - class CustomFinder: - @classmethod - def find_spec(cls, fullname, _path=None, _target=None): - candidate = pathlib.Path(path, *fullname.split(".")).with_suffix(".py") - if candidate.exists(): - return spec_from_file_location(fullname, candidate) - - @classmethod - def find_distributions(self, context=DistributionFinder.Context()): - for dist_info in path.glob("*.dist-info"): - yield PathDistribution(dist_info) - name, _, _ = str(dist_info).partition("-") - yield CustomDistribution(name + "_custom", dist_info) - - return CustomFinder - - def test_compatibility_with_old_stdlib_path_distribution(self): - """ - Given a custom finder that uses Python 3.8/3.9 importlib.metadata is installed, - when importlib_metadata functions are called, there should be no exceptions. - Ref python/importlib_metadata#396. - """ - self.fixtures.enter_context(fixtures.install_finder(self._meta_path_finder())) - - assert list(distributions()) - assert distribution("distinfo_pkg") - assert distribution("distinfo_pkg_custom") - assert version("distinfo_pkg") > "0" - assert version("distinfo_pkg_custom") > "0" - assert list(metadata("distinfo_pkg")) - assert list(metadata("distinfo_pkg_custom")) - assert list(entry_points(group="entries")) - - class DistSearch(unittest.TestCase): def test_search_dist_dirs(self): """ diff --git a/tests/test_py39compat.py b/tests/test_py39compat.py new file mode 100644 index 00000000..5051a60a --- /dev/null +++ b/tests/test_py39compat.py @@ -0,0 +1,74 @@ +import sys +import unittest +import pathlib + +from . import fixtures +from importlib_metadata import ( + distribution, + distributions, + entry_points, + metadata, + version, +) + + +class OldStdlibFinderTests(fixtures.DistInfoPkgOffPath, unittest.TestCase): + def setUp(self): + python_version = sys.version_info[:2] + if python_version < (3, 8) or python_version > (3, 9): + self.skipTest("Tests specific for Python 3.8/3.9") + super().setUp() + + def _meta_path_finder(self): + from importlib.metadata import ( + Distribution, + DistributionFinder, + PathDistribution, + ) + from importlib.util import spec_from_file_location + + path = pathlib.Path(self.site_dir) + + class CustomDistribution(Distribution): + def __init__(self, name, path): + self.name = name + self._path_distribution = PathDistribution(path) + + def read_text(self, filename): + return self._path_distribution.read_text(filename) + + def locate_file(self, path): + return self._path_distribution.locate_file(path) + + class CustomFinder: + @classmethod + def find_spec(cls, fullname, _path=None, _target=None): + candidate = pathlib.Path(path, *fullname.split(".")).with_suffix(".py") + if candidate.exists(): + return spec_from_file_location(fullname, candidate) + + @classmethod + def find_distributions(self, context=DistributionFinder.Context()): + for dist_info in path.glob("*.dist-info"): + yield PathDistribution(dist_info) + name, _, _ = str(dist_info).partition("-") + yield CustomDistribution(name + "_custom", dist_info) + + return CustomFinder + + def test_compatibility_with_old_stdlib_path_distribution(self): + """ + Given a custom finder that uses Python 3.8/3.9 importlib.metadata is installed, + when importlib_metadata functions are called, there should be no exceptions. + Ref python/importlib_metadata#396. + """ + self.fixtures.enter_context(fixtures.install_finder(self._meta_path_finder())) + + assert list(distributions()) + assert distribution("distinfo_pkg") + assert distribution("distinfo_pkg_custom") + assert version("distinfo_pkg") > "0" + assert version("distinfo_pkg_custom") > "0" + assert list(metadata("distinfo_pkg")) + assert list(metadata("distinfo_pkg_custom")) + assert list(entry_points(group="entries")) From 0bda2fe377ce993ecfd12073f06bb412838fdec9 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 31 Aug 2022 15:55:38 +0100 Subject: [PATCH 073/480] Extract Python 3.9 compatibility layer into its own file --- importlib_metadata/__init__.py | 31 ++++---------------- importlib_metadata/_py39compat.py | 48 +++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 26 deletions(-) create mode 100644 importlib_metadata/_py39compat.py diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 0e3c6aa1..eab5d4c1 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -14,7 +14,7 @@ import posixpath import collections -from . import _adapters, _meta +from . import _adapters, _meta, _py39compat from ._collections import FreezableDefaultDict, Pair from ._compat import ( NullFinder, @@ -29,7 +29,7 @@ from importlib import import_module from importlib.abc import MetaPathFinder from itertools import starmap -from typing import List, Mapping, Optional, Tuple, Union +from typing import List, Mapping, Optional, Union __all__ = [ @@ -382,8 +382,8 @@ def select(self, **params): Select entry points from self that match the given parameters (typically group and/or name). """ - candidates = (_ep_matches(ep, **params) for ep in self) - return EntryPoints(ep for ep, ep_matches in candidates if ep_matches) + candidates = (_py39compat.ep_matches(ep, **params) for ep in self) + return EntryPoints(ep for ep, predicate in candidates if predicate) @property def names(self): @@ -415,15 +415,6 @@ def _from_text(text): ) -def _ep_matches(ep: EntryPoint, **params) -> Tuple[EntryPoint, bool]: - """Compatibility layer for EntryPoint objects in Python 3.8/3.9 stdlib.""" - try: - return ep, ep.matches(**params) - except AttributeError: - _ep = EntryPoint(ep.name, ep.value, ep.group) - return _ep, _ep.matches(**params) - - class Deprecated: """ Compatibility add-in for mapping to indicate that @@ -1029,21 +1020,9 @@ def version(distribution_name): return distribution(distribution_name).version -def _compat_normalized_name(dist: Distribution) -> Optional[str]: - """ - Compatibility shim to honor name normalization for distributions - that don't provide ``_normalized_name`` - (as in ``importlib.metadata`` for Python 3.8/3.9). - """ - try: - return dist._normalized_name - except AttributeError: - return Prepared.normalize(getattr(dist, "name", None) or dist.metadata['Name']) - - _unique = functools.partial( unique_everseen, - key=_compat_normalized_name, + key=_py39compat.normalized_name, ) """ Wrapper for ``distributions`` to return unique distributions by name. diff --git a/importlib_metadata/_py39compat.py b/importlib_metadata/_py39compat.py new file mode 100644 index 00000000..31ef0e40 --- /dev/null +++ b/importlib_metadata/_py39compat.py @@ -0,0 +1,48 @@ +""" +Compatibility layer with Python 3.8/3.9 +""" +from typing import TYPE_CHECKING, Any, Optional, Tuple + + +if TYPE_CHECKING: + from . import Distribution, EntryPoint +else: + Distribution = EntryPoint = Any + + +def normalized_name(dist: Distribution) -> Optional[str]: + """ + Honor name normalization for distributions that don't provide ``_normalized_name``. + """ + try: + return dist._normalized_name + except AttributeError: + from . import Prepared + + return Prepared.normalize(getattr(dist, "name", None) or dist.metadata['Name']) + + +def ep_matches(ep: EntryPoint, **params) -> Tuple[EntryPoint, 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) + except AttributeError: + from . import EntryPoint + + # Reconstruct the EntryPoint object to make sure it is compatible. + _ep = EntryPoint(ep.name, ep.value, ep.group) + return _ep, _ep.matches(**params) From 05cb079fd4e6b7a9bfd32c1470c9c638af5b7bc9 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 31 Aug 2022 16:03:25 +0100 Subject: [PATCH 074/480] Add comments clarifying implementation choices --- importlib_metadata/_py39compat.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/importlib_metadata/_py39compat.py b/importlib_metadata/_py39compat.py index 31ef0e40..f6383272 100644 --- a/importlib_metadata/_py39compat.py +++ b/importlib_metadata/_py39compat.py @@ -3,8 +3,7 @@ """ from typing import TYPE_CHECKING, Any, Optional, Tuple - -if TYPE_CHECKING: +if TYPE_CHECKING: # -> prevent circular imports on runtime. from . import Distribution, EntryPoint else: Distribution = EntryPoint = Any @@ -17,7 +16,7 @@ def normalized_name(dist: Distribution) -> Optional[str]: try: return dist._normalized_name except AttributeError: - from . import Prepared + from . import Prepared # -> delay to prevent circular imports. return Prepared.normalize(getattr(dist, "name", None) or dist.metadata['Name']) @@ -41,7 +40,7 @@ def ep_matches(ep: EntryPoint, **params) -> Tuple[EntryPoint, bool]: try: return ep, ep.matches(**params) except AttributeError: - from . import EntryPoint + 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) From 1e32edd99085cccef39298e3433d2dc463bc7184 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 31 Aug 2022 16:31:22 +0100 Subject: [PATCH 075/480] Fix imports --- tests/test_integration.py | 2 -- tests/test_py39compat.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index 6d02aa75..c382a506 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -1,5 +1,3 @@ -import sys -import pathlib import unittest import packaging.requirements import packaging.version diff --git a/tests/test_py39compat.py b/tests/test_py39compat.py index 5051a60a..7e6235e4 100644 --- a/tests/test_py39compat.py +++ b/tests/test_py39compat.py @@ -1,6 +1,6 @@ import sys -import unittest import pathlib +import unittest from . import fixtures from importlib_metadata import ( From 17cc30eccfc889b361611d990eba43b4c9a6db06 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Wed, 31 Aug 2022 16:46:40 +0100 Subject: [PATCH 076/480] Skip coverage in type checking statement --- importlib_metadata/_py39compat.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/importlib_metadata/_py39compat.py b/importlib_metadata/_py39compat.py index f6383272..cf9cc124 100644 --- a/importlib_metadata/_py39compat.py +++ b/importlib_metadata/_py39compat.py @@ -3,7 +3,8 @@ """ from typing import TYPE_CHECKING, Any, Optional, Tuple -if TYPE_CHECKING: # -> prevent circular imports on runtime. +if TYPE_CHECKING: # pragma: no cover + # Prevent circular imports on runtime. from . import Distribution, EntryPoint else: Distribution = EntryPoint = Any From 27c55340e745741773e875402d20ecbb7fade521 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Sep 2022 08:31:09 -0400 Subject: [PATCH 077/480] Add PyPy to the test matrix on Linux. Fixes jaraco/skeleton#63. Adds a 'dev' factor to the matrix as workaround for actions/setup-python#508. --- .github/workflows/main.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 63fa1e8e..46e1ec9c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,20 +7,26 @@ jobs: strategy: matrix: python: - - 3.7 - - '3.10' - - '3.11' + - "3.7" + - "3.10" + - "3.11" + # Workaround for actions/setup-python#508 + dev: + - -dev platform: - ubuntu-latest - macos-latest - windows-latest + include: + - python: pypy3.9 + platform: ubuntu-latest runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v3 - name: Setup Python uses: actions/setup-python@v4 with: - python-version: ${{ matrix.python }}-dev + python-version: ${{ matrix.python }}${{ matrix.dev }} - name: Install tox run: | python -m pip install tox @@ -52,7 +58,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v4 with: - python-version: "3.11-dev" + python-version: 3.11-dev - name: Install tox run: | python -m pip install tox From b4f0ae621594316e56cede2856b6a5be605a893c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Sep 2022 08:46:38 -0400 Subject: [PATCH 078/480] When rendering docs, preserve the syntax for defaults. Fixes jaraco/path#197. Incidentally, re-organize the extensions a bit for clarity. --- docs/conf.py | 12 ++++++++++-- setup.cfg | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 319b1384..9fef70a5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,10 +1,15 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -extensions = ['sphinx.ext.autodoc', 'jaraco.packaging.sphinx', 'rst.linker'] +extensions = [ + 'sphinx.ext.autodoc', + 'jaraco.packaging.sphinx', +] master_doc = "index" +# Link dates and other references in the changelog +extensions += ['rst.linker'] link_files = { '../CHANGES.rst': dict( using=dict(GH='https://github.com'), @@ -25,7 +30,7 @@ ) } -# Be strict about any broken references: +# Be strict about any broken references nitpicky = True # Include Python intersphinx mapping to prevent failures @@ -34,3 +39,6 @@ intersphinx_mapping = { 'python': ('https://docs.python.org/3', None), } + +# Preserve authored syntax for defaults +autodoc_preserve_defaults = True diff --git a/setup.cfg b/setup.cfg index 1ab93501..1d2be997 100644 --- a/setup.cfg +++ b/setup.cfg @@ -46,7 +46,7 @@ testing = docs = # upstream - sphinx + sphinx >= 3.5 jaraco.packaging >= 9 rst.linker >= 1.9 From 679eebb215c80c7376a1df02c77fd368347620b0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 30 Sep 2022 12:41:25 -0400 Subject: [PATCH 079/480] Adopt furo theme for docs. --- docs/conf.py | 1 + setup.cfg | 1 + 2 files changed, 2 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 9fef70a5..fa741a85 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -7,6 +7,7 @@ ] master_doc = "index" +html_theme = "furo" # Link dates and other references in the changelog extensions += ['rst.linker'] diff --git a/setup.cfg b/setup.cfg index 1d2be997..a0d86eba 100644 --- a/setup.cfg +++ b/setup.cfg @@ -49,6 +49,7 @@ docs = sphinx >= 3.5 jaraco.packaging >= 9 rst.linker >= 1.9 + furo # local From b2412262dc1dd5d3d697e551d86acee4d5519bb6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 30 Sep 2022 13:30:09 -0400 Subject: [PATCH 080/480] 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 25998e4c3c56f2398407cdf91803cb4c162d7419 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 1 Oct 2022 12:20:31 -0400 Subject: [PATCH 081/480] Update changelog --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 84684eec..f48b9d20 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +v4.13.0 +======= + +* #396: Added compatibility for ``PathDistributions`` originating + from Python 3.8 and 3.9. + v4.12.0 ======= From 92a16db7237e24fee9ebcd53ed771c8c9a40bd74 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 1 Oct 2022 12:24:51 -0400 Subject: [PATCH 082/480] Expand possibilities for "top-level names". --- docs/using.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/using.rst b/docs/using.rst index d5d64715..936d0a09 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 :term:`packaging:Import Package` name. +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 7e5bae4c7fbd30366e49249825171b193dff22d4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 1 Oct 2022 18:38:15 -0400 Subject: [PATCH 083/480] Remove SelectableGroups --- importlib_metadata/__init__.py | 123 ++------------------------------- tests/test_api.py | 39 ----------- 2 files changed, 4 insertions(+), 158 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 0ad0196e..6abb375c 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -29,7 +29,7 @@ from importlib import import_module from importlib.abc import MetaPathFinder from itertools import starmap -from typing import List, Mapping, Optional, Union +from typing import List, Mapping, Optional __all__ = [ @@ -344,10 +344,6 @@ def names(self): def groups(self): """ Return the set of all groups of all entry points. - - For coverage while SelectableGroups is present. - >>> EntryPoints().groups - set() """ return set(ep.group for ep in self) @@ -367,109 +363,6 @@ def _parse_groups(text): ) -def flake8_bypass(func): - # defer inspect import as performance optimization. - import inspect - - is_flake8 = any('flake8' in str(frame.filename) for frame in inspect.stack()[:5]) - return func if not is_flake8 else lambda: None - - -class Deprecated: - """ - Compatibility add-in for mapping to indicate that - mapping behavior is deprecated. - - >>> recwarn = getfixture('recwarn') - >>> class DeprecatedDict(Deprecated, dict): pass - >>> dd = DeprecatedDict(foo='bar') - >>> dd.get('baz', None) - >>> dd['foo'] - 'bar' - >>> list(dd) - ['foo'] - >>> list(dd.keys()) - ['foo'] - >>> 'foo' in dd - True - >>> list(dd.values()) - ['bar'] - >>> len(recwarn) - 1 - """ - - _warn = functools.partial( - warnings.warn, - "SelectableGroups dict interface is deprecated. Use select.", - DeprecationWarning, - stacklevel=2, - ) - - def __getitem__(self, name): - self._warn() - return super().__getitem__(name) - - def get(self, name, default=None): - flake8_bypass(self._warn)() - return super().get(name, default) - - def __iter__(self): - self._warn() - return super().__iter__() - - def __contains__(self, *args): - self._warn() - return super().__contains__(*args) - - def keys(self): - self._warn() - return super().keys() - - def values(self): - self._warn() - return super().values() - - -class SelectableGroups(Deprecated, dict): - """ - A backward- and forward-compatible result from - entry_points that fully implements the dict interface. - """ - - @classmethod - def load(cls, eps): - by_group = operator.attrgetter('group') - ordered = sorted(eps, key=by_group) - grouped = itertools.groupby(ordered, by_group) - return cls((group, EntryPoints(eps)) for group, eps in grouped) - - @property - def _all(self): - """ - Reconstruct a list of all entrypoints from the groups. - """ - groups = super(Deprecated, self).values() - return EntryPoints(itertools.chain.from_iterable(groups)) - - @property - def groups(self): - return self._all.groups - - @property - def names(self): - """ - for coverage: - >>> SelectableGroups().names - set() - """ - return self._all.names - - def select(self, **params): - if not params: - return self - return self._all.select(**params) - - class PackagePath(pathlib.PurePosixPath): """A reference to a path in a package""" @@ -964,29 +857,21 @@ def version(distribution_name): return distribution(distribution_name).version -def entry_points(**params) -> Union[EntryPoints, SelectableGroups]: +def entry_points(**params) -> EntryPoints: """Return EntryPoint objects for all installed packages. Pass selection parameters (group or name) to filter the result to entry points matching those properties (see EntryPoints.select()). - For compatibility, returns ``SelectableGroups`` object unless - selection parameters are supplied. In the future, this function - will return ``EntryPoints`` instead of ``SelectableGroups`` - even when no selection parameters are supplied. - - For maximum future compatibility, pass selection parameters - or invoke ``.select`` with parameters on the result. - - :return: EntryPoints or SelectableGroups for all installed packages. + :return: EntryPoints for all installed packages. """ norm_name = operator.attrgetter('_normalized_name') unique = functools.partial(unique_everseen, key=norm_name) eps = itertools.chain.from_iterable( dist.entry_points for dist in unique(distributions()) ) - return SelectableGroups.load(eps).select(**params) + return EntryPoints(eps).select(**params) def files(distribution_name): diff --git a/tests/test_api.py b/tests/test_api.py index 819d4841..58bf2912 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -133,45 +133,6 @@ def test_entry_points_dict_construction(self): assert expected.category is DeprecationWarning assert "Construction of dict of EntryPoints is deprecated" in str(expected) - def test_entry_points_by_index(self): - """ - Prior versions of Distribution.entry_points would return a - tuple that allowed access by index. - Capture this now deprecated use-case - See python/importlib_metadata#300 and bpo-44246. - """ - eps = distribution('distinfo-pkg').entry_points - with warnings.catch_warnings(record=True) as caught: - eps[0] - - # check warning - expected = next(iter(caught)) - assert expected.category is DeprecationWarning - assert "Accessing entry points by index is deprecated" in str(expected) - - def test_entry_points_groups_getitem(self): - """ - Prior versions of entry_points() returned a dict. Ensure - that callers using '.__getitem__()' are supported but warned to - migrate. - """ - with warnings.catch_warnings(record=True): - entry_points()['entries'] == entry_points(group='entries') - - with self.assertRaises(KeyError): - entry_points()['missing'] - - def test_entry_points_groups_get(self): - """ - Prior versions of entry_points() returned a dict. Ensure - that callers using '.get()' are supported but warned to - migrate. - """ - with warnings.catch_warnings(record=True): - entry_points().get('missing', 'default') == 'default' - entry_points().get('entries', 'default') == entry_points()['entries'] - entry_points().get('missing', ()) == () - def test_metadata_for_this_package(self): md = metadata('egginfo-pkg') assert md['author'] == 'Steven Ma' From 47544ce7303da9b2147c6603a674a1f82225248f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 1 Oct 2022 18:41:48 -0400 Subject: [PATCH 084/480] Remove DeprecatedList --- importlib_metadata/__init__.py | 95 +--------------------------------- 1 file changed, 1 insertion(+), 94 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 6abb375c..9908ac72 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -209,100 +209,7 @@ def matches(self, **params): return all(map(operator.eq, params.values(), attrs)) -class DeprecatedList(list): - """ - Allow an otherwise immutable object to implement mutability - for compatibility. - - >>> recwarn = getfixture('recwarn') - >>> dl = DeprecatedList(range(3)) - >>> dl[0] = 1 - >>> dl.append(3) - >>> del dl[3] - >>> dl.reverse() - >>> dl.sort() - >>> dl.extend([4]) - >>> dl.pop(-1) - 4 - >>> dl.remove(1) - >>> dl += [5] - >>> dl + [6] - [1, 2, 5, 6] - >>> dl + (6,) - [1, 2, 5, 6] - >>> dl.insert(0, 0) - >>> dl - [0, 1, 2, 5] - >>> dl == [0, 1, 2, 5] - True - >>> dl == (0, 1, 2, 5) - True - >>> len(recwarn) - 1 - """ - - _warn = functools.partial( - warnings.warn, - "EntryPoints list interface is deprecated. Cast to list if needed.", - DeprecationWarning, - stacklevel=2, - ) - - def __setitem__(self, *args, **kwargs): - self._warn() - return super().__setitem__(*args, **kwargs) - - def __delitem__(self, *args, **kwargs): - self._warn() - return super().__delitem__(*args, **kwargs) - - def append(self, *args, **kwargs): - self._warn() - return super().append(*args, **kwargs) - - def reverse(self, *args, **kwargs): - self._warn() - return super().reverse(*args, **kwargs) - - def extend(self, *args, **kwargs): - self._warn() - return super().extend(*args, **kwargs) - - def pop(self, *args, **kwargs): - self._warn() - return super().pop(*args, **kwargs) - - def remove(self, *args, **kwargs): - self._warn() - return super().remove(*args, **kwargs) - - def __iadd__(self, *args, **kwargs): - self._warn() - return super().__iadd__(*args, **kwargs) - - def __add__(self, other): - if not isinstance(other, tuple): - self._warn() - other = tuple(other) - return self.__class__(tuple(self) + other) - - def insert(self, *args, **kwargs): - self._warn() - return super().insert(*args, **kwargs) - - def sort(self, *args, **kwargs): - self._warn() - return super().sort(*args, **kwargs) - - def __eq__(self, other): - if not isinstance(other, tuple): - self._warn() - other = tuple(other) - - return tuple(self).__eq__(other) - - -class EntryPoints(DeprecatedList): +class EntryPoints(tuple): """ An immutable collection of selectable EntryPoint objects. """ From 0c819641d314ac496eb32b55f2b15215fa6fa55f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 1 Oct 2022 18:43:09 -0400 Subject: [PATCH 085/480] Remove compatibility for EntryPoints.__getitem__ by index. --- importlib_metadata/__init__.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 9908ac72..c243ff61 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -220,14 +220,6 @@ def __getitem__(self, name): # -> EntryPoint: """ Get the EntryPoint in self matching name. """ - if isinstance(name, int): - warnings.warn( - "Accessing entry points by index is deprecated. " - "Cast to tuple if needed.", - DeprecationWarning, - stacklevel=2, - ) - return super().__getitem__(name) try: return next(iter(self.select(name=name))) except StopIteration: From dde2b9de2973ce1c6fa9ba21dfe81069b0baa77b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 1 Oct 2022 19:04:42 -0400 Subject: [PATCH 086/480] Remove support for cast of iterable of entry points to dict. Ref #97. --- importlib_metadata/__init__.py | 12 ------------ tests/test_api.py | 18 ------------------ tests/test_main.py | 10 ---------- 3 files changed, 40 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index c243ff61..86599a7a 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -8,7 +8,6 @@ import pathlib import operator import textwrap -import warnings import functools import itertools import posixpath @@ -187,17 +186,6 @@ def _for(self, dist): self.dist = dist return self - def __iter__(self): - """ - Supply iter so one may construct dicts of EntryPoints by name. - """ - msg = ( - "Construction of dict of EntryPoints is deprecated in " - "favor of EntryPoints." - ) - warnings.warn(msg, DeprecationWarning) - return iter((self.name, self)) - def __reduce__(self): return ( self.__class__, diff --git a/tests/test_api.py b/tests/test_api.py index 58bf2912..850e6ba6 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,7 +1,6 @@ import re import textwrap import unittest -import warnings import importlib from . import fixtures @@ -116,23 +115,6 @@ def test_entry_points_missing_name(self): def test_entry_points_missing_group(self): assert entry_points(group='missing') == () - def test_entry_points_dict_construction(self): - """ - Prior versions of entry_points() returned simple lists and - allowed casting those lists into maps by name using ``dict()``. - Capture this now deprecated use-case. - """ - with warnings.catch_warnings(record=True) as caught: - eps = dict(entry_points(group='entries')) - - assert 'main' in eps - assert eps['main'] == entry_points(group='entries')['main'] - - # check warning - expected = next(iter(caught)) - assert expected.category is DeprecationWarning - assert "Construction of dict of EntryPoints is deprecated" in str(expected) - def test_metadata_for_this_package(self): md = metadata('egginfo-pkg') assert md['author'] == 'Steven Ma' diff --git a/tests/test_main.py b/tests/test_main.py index f7c9c518..d315b781 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,9 +1,7 @@ import re -import json import pickle import textwrap import unittest -import warnings import importlib import importlib_metadata import pyfakefs.fake_filesystem_unittest as ffs @@ -243,14 +241,6 @@ def test_hashable(self): """EntryPoints should be hashable""" hash(self.ep) - def test_json_dump(self): - """ - json should not expect to be able to dump an EntryPoint - """ - with self.assertRaises(Exception): - with warnings.catch_warnings(record=True): - json.dumps(self.ep) - def test_module(self): assert self.ep.module == 'value' From 2135490a9d965339de71aa18dd2955844db966aa Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 1 Oct 2022 19:34:16 -0400 Subject: [PATCH 087/480] Update changelog --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index f48b9d20..5aa5776f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,9 @@ +v5.0.0 +====== + +* #97, #284, #300: Removed compatibility shims for deprecated entry + point interfaces. + v4.13.0 ======= From 117d1b470f8c03b4f4e332a95ccfcfebdadb8f52 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 1 Oct 2022 19:48:36 -0400 Subject: [PATCH 088/480] Disable flake8 due to incompatibility. --- pyproject.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 60de2424..27b0f18c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,8 @@ addopts = "--black" addopts = "--mypy" [tool.pytest-enabler.flake8] -addopts = "--flake8" +# disabled due to PyCQA/flake8#1438 +# addopts = "--flake8" [tool.pytest-enabler.cov] addopts = "--cov" From ac9ff953a00d86c341f2a9a892c914d26075d891 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 1 Oct 2022 20:01:33 -0400 Subject: [PATCH 089/480] Update documentation around removal of SelectableGroups. --- docs/using.rst | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/using.rst b/docs/using.rst index 40616d86..bbb3824f 100644 --- a/docs/using.rst +++ b/docs/using.rst @@ -164,11 +164,10 @@ for more information on entry points, their definition, and usage. The "selectable" entry points were introduced in ``importlib_metadata`` 3.6 and Python 3.10. Prior to those changes, ``entry_points`` accepted no parameters and always returned a dictionary of entry points, keyed -by group. For compatibility, if no parameters are passed to entry_points, -a ``SelectableGroups`` object is returned, implementing that dict -interface. In the future, calling ``entry_points`` with no parameters -will return an ``EntryPoints`` object. Users should rely on the selection -interface to retrieve entry points by group. +by group. With ``importlib_metadata`` 5.0 and Python 3.12, +``entry_points`` always returns an ``EntryPoints`` object. See +`backports.entry_points_selectable `_ +for compatibility options. .. _metadata: From ecb363c061d4b24f8fb84621ebb5529dd8685f45 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 3 Oct 2022 09:30:48 -0400 Subject: [PATCH 090/480] 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 091/480] 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 092/480] 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 093/480] 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 094/480] 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 095/480] 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 096/480] 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 097/480] 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 098/480] 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 099/480] 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 100/480] 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 101/480] 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 102/480] 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 103/480] 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 104/480] 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 105/480] 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 106/480] 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 107/480] 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 108/480] 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 109/480] 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 110/480] 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 111/480] 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 112/480] 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 113/480] 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 114/480] 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 115/480] 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 116/480] 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 117/480] 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 118/480] 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 119/480] 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 120/480] 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 121/480] 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 122/480] 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 123/480] 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 124/480] 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 125/480] 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 126/480] 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 127/480] 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 128/480] 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 129/480] 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 130/480] 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 131/480] 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 132/480] 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 133/480] 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 134/480] 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 135/480] 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 136/480] 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 137/480] =?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 138/480] 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 139/480] 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 140/480] 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 141/480] 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 142/480] 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 143/480] 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 144/480] 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 145/480] 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 146/480] 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 147/480] 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 148/480] 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 149/480] 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 150/480] 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 151/480] 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 152/480] 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 153/480] 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 154/480] 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 155/480] 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 156/480] 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 157/480] =?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 158/480] 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 159/480] 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 160/480] 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 161/480] 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 162/480] 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 163/480] 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 164/480] 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 165/480] 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 166/480] 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 167/480] 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 168/480] 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 169/480] 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 170/480] 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 171/480] =?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 172/480] 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 173/480] 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 174/480] 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 175/480] 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 176/480] 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 177/480] 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 178/480] 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 179/480] 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 180/480] 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 181/480] 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 182/480] 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 183/480] 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 184/480] =?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 185/480] 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 186/480] 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 187/480] 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 188/480] 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 189/480] 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 190/480] 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 191/480] 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 192/480] 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 193/480] 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 194/480] 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 195/480] 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 196/480] 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 197/480] 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 198/480] 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 199/480] 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 200/480] =?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 201/480] 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 202/480] 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 203/480] 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 6f754807a0abd25e0b52f024df2072d53f336974 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 7 Jun 2023 09:49:46 -0400 Subject: [PATCH 204/480] Update RTD boilerplate to new issue. Ref readthedocs/readthedocs.org#10401. --- .readthedocs.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 6bef3493..053c7287 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -5,9 +5,8 @@ python: extra_requirements: - docs -# workaround for readthedocs/readthedocs.org#9623 +# required boilerplate readthedocs/readthedocs.org#10401 build: - # workaround for readthedocs/readthedocs.org#9635 os: ubuntu-22.04 tools: python: "3" From 70a6075f98e4ac6154a8b3de72dd39073b0d885b Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Wed, 3 May 2023 04:55:22 -0700 Subject: [PATCH 205/480] 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 206/480] 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 207/480] 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 208/480] 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 209/480] 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 210/480] 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 e7cd730d0d708c8f1f3eb28a29927f3475b3e855 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 8 Jun 2023 21:27:17 -0400 Subject: [PATCH 211/480] Add badge for Ruff. --- README.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.rst b/README.rst index 1f66d195..b703d490 100644 --- a/README.rst +++ b/README.rst @@ -7,6 +7,10 @@ :target: https://github.com/PROJECT_PATH/actions?query=workflow%3A%22tests%22 :alt: tests +.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json + :target: https://github.com/astral-sh/ruff + :alt: Ruff + .. image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black :alt: Code style: Black From 241541c07c9c30e48b57d59e527ef923d05c82d6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 16 Jun 2023 14:11:22 -0400 Subject: [PATCH 212/480] Remove inclusion of python version for docs --- .github/workflows/main.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3fa1c81e..93471ce8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -83,8 +83,6 @@ jobs: - 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 From 851b921c33acf8fcfd9f009cda2bc6176ba26e94 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 18 Jun 2023 16:17:57 -0400 Subject: [PATCH 213/480] 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 214/480] 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 215/480] 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 216/480] 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 217/480] 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 218/480] 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 219/480] 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 220/480] 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 221/480] 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 222/480] 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"), } From 74b0d396c87892e9122c96994cf2c26329141208 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 19 Jun 2023 21:11:01 -0400 Subject: [PATCH 223/480] Adopt towncrier for managing changelog. Fixes jaraco/skeleton#83. Renamed CHANGES.rst to NEWS.rst to align with towncrier defaults. --- CHANGES.rst => NEWS.rst | 0 docs/conf.py | 2 +- docs/history.rst | 2 +- towncrier.toml | 2 ++ tox.ini | 9 +++++++++ 5 files changed, 13 insertions(+), 2 deletions(-) rename CHANGES.rst => NEWS.rst (100%) create mode 100644 towncrier.toml diff --git a/CHANGES.rst b/NEWS.rst similarity index 100% rename from CHANGES.rst rename to NEWS.rst diff --git a/docs/conf.py b/docs/conf.py index c2043393..32150488 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -9,7 +9,7 @@ # Link dates and other references in the changelog extensions += ['rst.linker'] link_files = { - '../CHANGES.rst': dict( + '../NEWS.rst': dict( using=dict(GH='https://github.com'), replace=[ dict( diff --git a/docs/history.rst b/docs/history.rst index 8e217503..5bdc2320 100644 --- a/docs/history.rst +++ b/docs/history.rst @@ -5,4 +5,4 @@ History ******* -.. include:: ../CHANGES (links).rst +.. include:: ../NEWS (links).rst diff --git a/towncrier.toml b/towncrier.toml new file mode 100644 index 00000000..6fa480e4 --- /dev/null +++ b/towncrier.toml @@ -0,0 +1,2 @@ +[tool.towncrier] +title_format = "{version}" diff --git a/tox.ini b/tox.ini index 5a678211..32b031da 100644 --- a/tox.ini +++ b/tox.ini @@ -25,6 +25,15 @@ commands = python -m sphinx -W --keep-going . {toxinidir}/build/html python -m sphinxlint +[testenv:finalize] +skip_install = True +deps = + towncrier + jaraco.develop +passenv = * +commands = + python -m jaraco.develop.towncrier build --yes + [testenv:release] skip_install = True deps = From 45c03bdc458c06d4f48aa997398a7810c0cd8425 Mon Sep 17 00:00:00 2001 From: Gryfenfer97 Date: Wed, 21 Jun 2023 11:19:19 +0200 Subject: [PATCH 224/480] add typing for simple functions --- importlib_metadata/__init__.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 8147d2f0..6ba414e5 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -873,7 +873,7 @@ def _name_from_stem(stem): return name -def distribution(distribution_name) -> Distribution: +def distribution(distribution_name: str) -> Distribution: """Get the ``Distribution`` instance for the named package. :param distribution_name: The name of the distribution package as a string. @@ -890,7 +890,7 @@ def distributions(**kwargs) -> Iterable[Distribution]: return Distribution.discover(**kwargs) -def metadata(distribution_name) -> _meta.PackageMetadata: +def metadata(distribution_name: str) -> _meta.PackageMetadata: """Get the metadata for the named package. :param distribution_name: The name of the distribution package to query. @@ -899,7 +899,7 @@ def metadata(distribution_name) -> _meta.PackageMetadata: return Distribution.from_name(distribution_name).metadata -def version(distribution_name) -> str: +def version(distribution_name: str) -> str: """Get the version string for the named package. :param distribution_name: The name of the distribution package to query. @@ -933,7 +933,7 @@ def entry_points(**params) -> EntryPoints: return EntryPoints(eps).select(**params) -def files(distribution_name) -> Optional[List[PackagePath]]: +def files(distribution_name: str) -> Optional[List[PackagePath]]: """Return a list of files for the named package. :param distribution_name: The name of the distribution package to query. @@ -942,7 +942,7 @@ def files(distribution_name) -> Optional[List[PackagePath]]: return distribution(distribution_name).files -def requires(distribution_name) -> Optional[List[str]]: +def requires(distribution_name: str) -> Optional[List[str]]: """ Return a list of requirements for the named package. From cd145f4080ef0e954aa4716fc3f240c508a5693c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 22 Jun 2023 23:34:21 -0400 Subject: [PATCH 225/480] Replace workaround for actions/setup-python#508 with 'allow-prereleases' --- .github/workflows/main.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 93471ce8..00b21297 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -45,9 +45,6 @@ jobs: - "3.7" - "3.11" - "3.12" - # Workaround for actions/setup-python#508 - dev: - - -dev platform: - ubuntu-latest - macos-latest @@ -68,7 +65,8 @@ jobs: - name: Setup Python uses: actions/setup-python@v4 with: - python-version: ${{ matrix.python }}${{ matrix.dev }} + python-version: ${{ matrix.python }} + allow-prereleases: true - name: Install tox run: | python -m pip install tox From 07a87ea9d8671ea4f529858201866e3f78fa3afc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 23 Jun 2023 12:59:50 -0400 Subject: [PATCH 226/480] Remove tox boilerplate, no longer necessary with later versions of tox. --- tox.ini | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tox.ini b/tox.ini index 32b031da..4e8e7090 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,4 @@ [tox] -envlist = python -minversion = 3.2 -# https://github.com/jaraco/skeleton/issues/6 -tox_pip_extensions_ext_venv_update = true toxworkdir={env:TOX_WORK_DIR:.tox} From 3b7d8a912d54ccf88f79eea0dfc903d101067bb5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 18 Jun 2023 20:55:42 -0400 Subject: [PATCH 227/480] Require Python 3.8 or later. --- .github/workflows/main.yml | 4 +--- newsfragments/+drop-py37.feature.rst | 1 + setup.cfg | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) create mode 100644 newsfragments/+drop-py37.feature.rst diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 00b21297..7cc4fb82 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -42,7 +42,7 @@ jobs: strategy: matrix: python: - - "3.7" + - "3.8" - "3.11" - "3.12" platform: @@ -50,8 +50,6 @@ jobs: - macos-latest - windows-latest include: - - python: "3.8" - platform: ubuntu-latest - python: "3.9" platform: ubuntu-latest - python: "3.10" diff --git a/newsfragments/+drop-py37.feature.rst b/newsfragments/+drop-py37.feature.rst new file mode 100644 index 00000000..ccabdaa3 --- /dev/null +++ b/newsfragments/+drop-py37.feature.rst @@ -0,0 +1 @@ +Require Python 3.8 or later. diff --git a/setup.cfg b/setup.cfg index 0cee3d34..75a50d4d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -15,7 +15,7 @@ classifiers = [options] packages = find_namespace: include_package_data = true -python_requires = >=3.7 +python_requires = >=3.8 install_requires = [options.packages.find] From 8e83c3f0bc7baab5f2db37487526e374a1f68494 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 23 Jun 2023 19:52:23 -0400 Subject: [PATCH 228/480] Expand 'finalize' to commit and tag the change. --- tox.ini | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 4e8e7090..1093e028 100644 --- a/tox.ini +++ b/tox.ini @@ -25,10 +25,11 @@ commands = skip_install = True deps = towncrier - jaraco.develop + jaraco.develop >= 7.23 passenv = * commands = - python -m jaraco.develop.towncrier build --yes + python -m jaraco.develop.finalize + [testenv:release] skip_install = True From 74ba8acbe019de9f30dee6d319c8621caac070ae Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Jun 2023 22:23:40 -0400 Subject: [PATCH 229/480] Leverage pytest-enabler 2.2 for the default config. --- pyproject.toml | 12 ------------ setup.cfg | 2 +- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d5f3487e..dce944df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,15 +6,3 @@ build-backend = "setuptools.build_meta" skip-string-normalization = true [tool.setuptools_scm] - -[tool.pytest-enabler.black] -addopts = "--black" - -[tool.pytest-enabler.mypy] -addopts = "--mypy" - -[tool.pytest-enabler.cov] -addopts = "--cov" - -[tool.pytest-enabler.ruff] -addopts = "--ruff" diff --git a/setup.cfg b/setup.cfg index 75a50d4d..a9ca2a88 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,7 +37,7 @@ testing = pytest-mypy >= 0.9.1; \ # workaround for jaraco/skeleton#22 python_implementation != "PyPy" - pytest-enabler >= 1.3 + pytest-enabler >= 2.2 pytest-ruff # local From cca49a4481167049f6bdd0f8038e685e5b8e929f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 3 Jul 2023 15:18:58 -0400 Subject: [PATCH 230/480] Prefer 3.x for Python version (latest stable). --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7cc4fb82..f54dfbc6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -113,7 +113,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v4 with: - python-version: 3.11-dev + python-version: 3.x - name: Install tox run: | python -m pip install tox From c29955f9be8e44b2ea5fea12f86b7bd46a0b3958 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 4 Jul 2023 11:53:15 -0400 Subject: [PATCH 231/480] Collapse skeleton history. Workaround for jaraco/skeleton#87. --- .coveragerc | 9 ++ .editorconfig | 19 ++++ .github/dependabot.yml | 8 ++ .github/workflows/main.yml | 124 +++++++++++++++++++++++++++ .pre-commit-config.yaml | 5 ++ .readthedocs.yaml | 12 +++ LICENSE | 17 ++++ NEWS.rst | 0 README.rst | 22 +++++ docs/conf.py | 42 +++++++++ docs/history.rst | 8 ++ docs/index.rst | 22 +++++ mypy.ini | 5 ++ newsfragments/+drop-py37.feature.rst | 1 + pyproject.toml | 8 ++ pytest.ini | 27 ++++++ setup.cfg | 55 ++++++++++++ towncrier.toml | 2 + tox.ini | 49 +++++++++++ 19 files changed, 435 insertions(+) create mode 100644 .coveragerc create mode 100644 .editorconfig create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/main.yml create mode 100644 .pre-commit-config.yaml create mode 100644 .readthedocs.yaml create mode 100644 LICENSE create mode 100644 NEWS.rst create mode 100644 README.rst create mode 100644 docs/conf.py create mode 100644 docs/history.rst create mode 100644 docs/index.rst create mode 100644 mypy.ini create mode 100644 newsfragments/+drop-py37.feature.rst create mode 100644 pyproject.toml create mode 100644 pytest.ini create mode 100644 setup.cfg create mode 100644 towncrier.toml create mode 100644 tox.ini diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..02879483 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,9 @@ +[run] +omit = + # leading `*/` for pytest-dev/pytest-cov#456 + */.tox/* +disable_warnings = + couldnt-parse + +[report] +show_missing = True diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..304196f8 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +root = true + +[*] +charset = utf-8 +indent_style = tab +indent_size = 4 +insert_final_newline = true +end_of_line = lf + +[*.py] +indent_style = space +max_line_length = 88 + +[*.{yml,yaml}] +indent_style = space +indent_size = 2 + +[*.rst] +indent_style = space diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..89ff3396 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "daily" + allow: + - dependency-type: "all" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 00000000..f54dfbc6 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,124 @@ +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 + # 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. 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") + 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: + matrix: + python: + - "3.8" + - "3.11" + - "3.12" + platform: + - ubuntu-latest + - macos-latest + - windows-latest + include: + - python: "3.9" + platform: ubuntu-latest + - python: "3.10" + platform: ubuntu-latest + - 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 + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + allow-prereleases: true + - name: Install tox + run: | + python -m pip install tox + - 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 + - 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 + + steps: + - name: Decide whether the needed jobs succeeded or failed + uses: re-actors/alls-green@release/v1 + with: + jobs: ${{ toJSON(needs) }} + + release: + permissions: + contents: write + needs: + - check + if: github.event_name == 'push' && contains(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: 3.x + - name: Install tox + run: | + python -m pip install tox + - name: Release + run: tox -e release + env: + TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..af502010 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,5 @@ +repos: +- repo: https://github.com/psf/black + rev: 22.6.0 + hooks: + - id: black diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 00000000..053c7287 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,12 @@ +version: 2 +python: + install: + - path: . + extra_requirements: + - docs + +# required boilerplate readthedocs/readthedocs.org#10401 +build: + os: ubuntu-22.04 + tools: + python: "3" diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..1bb5a443 --- /dev/null +++ b/LICENSE @@ -0,0 +1,17 @@ +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to +deal in the Software without restriction, including without limitation the +rights to use, copy, modify, merge, publish, distribute, sublicense, and/or +sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS +IN THE SOFTWARE. diff --git a/NEWS.rst b/NEWS.rst new file mode 100644 index 00000000..e69de29b diff --git a/README.rst b/README.rst new file mode 100644 index 00000000..b703d490 --- /dev/null +++ b/README.rst @@ -0,0 +1,22 @@ +.. image:: https://img.shields.io/pypi/v/PROJECT.svg + :target: https://pypi.org/project/PROJECT + +.. image:: https://img.shields.io/pypi/pyversions/PROJECT.svg + +.. 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/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json + :target: https://github.com/astral-sh/ruff + :alt: Ruff + +.. 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/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/conf.py b/docs/conf.py new file mode 100644 index 00000000..32150488 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,42 @@ +extensions = [ + 'sphinx.ext.autodoc', + 'jaraco.packaging.sphinx', +] + +master_doc = "index" +html_theme = "furo" + +# Link dates and other references in the changelog +extensions += ['rst.linker'] +link_files = { + '../NEWS.rst': dict( + using=dict(GH='https://github.com'), + replace=[ + dict( + pattern=r'(Issue #|\B#)(?P\d+)', + url='{package_url}/issues/{issue}', + ), + dict( + pattern=r'(?m:^((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n)', + with_scm='{text}\n{rev[timestamp]:%d %b %Y}\n', + ), + dict( + pattern=r'PEP[- ](?P\d+)', + url='https://peps.python.org/pep-{pep_number:0>4}/', + ), + ], + ) +} + +# Be strict about any broken references +nitpicky = True + +# Include Python intersphinx mapping to prevent failures +# jaraco/skeleton#51 +extensions += ['sphinx.ext.intersphinx'] +intersphinx_mapping = { + 'python': ('https://docs.python.org/3', None), +} + +# Preserve authored syntax for defaults +autodoc_preserve_defaults = True diff --git a/docs/history.rst b/docs/history.rst new file mode 100644 index 00000000..5bdc2320 --- /dev/null +++ b/docs/history.rst @@ -0,0 +1,8 @@ +:tocdepth: 2 + +.. _changes: + +History +******* + +.. include:: ../NEWS (links).rst diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 00000000..53117d16 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,22 @@ +Welcome to |project| documentation! +=================================== + +.. toctree:: + :maxdepth: 1 + + history + + +.. automodule:: PROJECT + :members: + :undoc-members: + :show-inheritance: + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 00000000..b6f97276 --- /dev/null +++ b/mypy.ini @@ -0,0 +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/newsfragments/+drop-py37.feature.rst b/newsfragments/+drop-py37.feature.rst new file mode 100644 index 00000000..ccabdaa3 --- /dev/null +++ b/newsfragments/+drop-py37.feature.rst @@ -0,0 +1 @@ +Require Python 3.8 or later. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..dce944df --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,8 @@ +[build-system] +requires = ["setuptools>=56", "setuptools_scm[toml]>=3.4.1"] +build-backend = "setuptools.build_meta" + +[tool.black] +skip-string-normalization = true + +[tool.setuptools_scm] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..d9a15ed1 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,27 @@ +[pytest] +norecursedirs=dist build .tox .eggs +addopts=--doctest-modules +filterwarnings= + ## upstream + + # Ensure ResourceWarnings are emitted + default::ResourceWarning + + # 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 + + # shopkeep/pytest-black#67 + ignore:'encoding' argument not specified::pytest_black + + # realpython/pytest-mypy#152 + ignore:'encoding' argument not specified::pytest_mypy + + # python/cpython#100750 + ignore:'encoding' argument not specified::platform + + # pypa/build#615 + ignore:'encoding' argument not specified::build.env + + ## end upstream diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 00000000..a9ca2a88 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,55 @@ +[metadata] +name = PROJECT +author = Jason R. Coombs +author_email = jaraco@jaraco.com +description = PROJECT_DESCRIPTION +long_description = file:README.rst +url = https://github.com/PROJECT_PATH +classifiers = + Development Status :: 5 - Production/Stable + Intended Audience :: Developers + License :: OSI Approved :: MIT License + Programming Language :: Python :: 3 + Programming Language :: Python :: 3 :: Only + +[options] +packages = find_namespace: +include_package_data = true +python_requires = >=3.8 +install_requires = + +[options.packages.find] +exclude = + build* + dist* + docs* + tests* + +[options.extras_require] +testing = + # upstream + pytest >= 6 + pytest-checkdocs >= 2.4 + pytest-black >= 0.3.7; \ + # workaround for jaraco/skeleton#22 + python_implementation != "PyPy" + pytest-cov + pytest-mypy >= 0.9.1; \ + # workaround for jaraco/skeleton#22 + python_implementation != "PyPy" + pytest-enabler >= 2.2 + pytest-ruff + + # local + +docs = + # upstream + sphinx >= 3.5 + jaraco.packaging >= 9 + rst.linker >= 1.9 + furo + sphinx-lint + + # local + +[options.entry_points] diff --git a/towncrier.toml b/towncrier.toml new file mode 100644 index 00000000..6fa480e4 --- /dev/null +++ b/towncrier.toml @@ -0,0 +1,2 @@ +[tool.towncrier] +title_format = "{version}" diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..1093e028 --- /dev/null +++ b/tox.ini @@ -0,0 +1,49 @@ +[tox] +toxworkdir={env:TOX_WORK_DIR:.tox} + + +[testenv] +deps = +setenv = + PYTHONWARNDEFAULTENCODING = 1 +commands = + pytest {posargs} +usedevelop = True +extras = + testing + +[testenv:docs] +extras = + docs + testing +changedir = docs +commands = + python -m sphinx -W --keep-going . {toxinidir}/build/html + python -m sphinxlint + +[testenv:finalize] +skip_install = True +deps = + towncrier + jaraco.develop >= 7.23 +passenv = * +commands = + python -m jaraco.develop.finalize + + +[testenv:release] +skip_install = True +deps = + build + twine>=3 + jaraco.develop>=7.1 +passenv = + TWINE_PASSWORD + GITHUB_TOKEN +setenv = + TWINE_USERNAME = {env:TWINE_USERNAME:__token__} +commands = + python -c "import shutil; shutil.rmtree('dist', ignore_errors=True)" + python -m build + python -m twine upload dist/* + python -m jaraco.develop.create-github-release From 972d1b3033afba89ffa20e6c492c4d02742e8a9d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 5 Jul 2023 22:20:28 -0400 Subject: [PATCH 232/480] Add links to project home page and pypi. Fixes jaraco/skeleton#77. --- docs/index.rst | 4 ++++ setup.cfg | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/index.rst b/docs/index.rst index 53117d16..5a3c6770 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -1,6 +1,10 @@ Welcome to |project| documentation! =================================== +.. sidebar-links:: + :home: + :pypi: + .. toctree:: :maxdepth: 1 diff --git a/setup.cfg b/setup.cfg index a9ca2a88..46f7bdf7 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,7 +45,7 @@ testing = docs = # upstream sphinx >= 3.5 - jaraco.packaging >= 9 + jaraco.packaging >= 9.3 rst.linker >= 1.9 furo sphinx-lint From 747c2a36524f83b84a3d9497121313bb5751b877 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Jul 2023 08:59:49 -0400 Subject: [PATCH 233/480] Replace redundant step names with simple 'Run'. --- .github/workflows/main.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f54dfbc6..b8224099 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -68,7 +68,7 @@ jobs: - name: Install tox run: | python -m pip install tox - - name: Run tests + - name: Run run: tox docs: @@ -82,7 +82,7 @@ jobs: - name: Install tox run: | python -m pip install tox - - name: Run tests + - name: Run run: tox check: # This job does nothing and is only used for the branch protection @@ -117,7 +117,7 @@ jobs: - name: Install tox run: | python -m pip install tox - - name: Release + - name: Run run: tox -e release env: TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }} From ee4f84ace3bb6914f155d67aa4811e309f90b836 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 7 Jul 2023 10:06:59 -0400 Subject: [PATCH 234/480] Remove superfluous includes --- .github/workflows/main.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 303f5864..783bde8e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -56,10 +56,6 @@ jobs: platform: ubuntu-latest - python: pypy3.9 platform: ubuntu-latest - - platform: ubuntu-latest - python: "3.8" - - platform: ubuntu-latest - python: "3.9" runs-on: ${{ matrix.platform }} continue-on-error: ${{ matrix.python == '3.12' }} steps: From ec7bca0243886bdf25ec6f6d9f060b71d76ef4b5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 7 Jul 2023 10:07:52 -0400 Subject: [PATCH 235/480] Disable tests on pypy due to #463 --- .github/workflows/main.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 783bde8e..f7ddc25c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -54,8 +54,9 @@ jobs: platform: ubuntu-latest - python: "3.10" platform: ubuntu-latest - - python: pypy3.9 - platform: ubuntu-latest + # disabled for #463 + # - python: pypy3.9 + # platform: ubuntu-latest runs-on: ${{ matrix.platform }} continue-on-error: ${{ matrix.python == '3.12' }} steps: From 97084d87cab99d09555fdff2cf04ed9f3faaa913 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 7 Jul 2023 11:47:54 -0400 Subject: [PATCH 236/480] Remove reliance on typing-extensions, only required for Python 3.7 compatibility. --- importlib_metadata/_compat.py | 9 +-------- importlib_metadata/_meta.py | 2 +- tests/_path.py | 11 +++-------- 3 files changed, 5 insertions(+), 17 deletions(-) diff --git a/importlib_metadata/_compat.py b/importlib_metadata/_compat.py index b7abd09b..c0f15c78 100644 --- a/importlib_metadata/_compat.py +++ b/importlib_metadata/_compat.py @@ -5,14 +5,7 @@ from typing import Union -__all__ = ['install', 'NullFinder', 'Protocol'] - - -try: - from typing import Protocol -except ImportError: # pragma: no cover - # Python 3.7 compatibility - from typing_extensions import Protocol # type: ignore +__all__ = ['install', 'NullFinder'] def install(cls): diff --git a/importlib_metadata/_meta.py b/importlib_metadata/_meta.py index 0c7e8791..f670016d 100644 --- a/importlib_metadata/_meta.py +++ b/importlib_metadata/_meta.py @@ -1,4 +1,4 @@ -from ._compat import Protocol +from typing import Protocol from typing import Any, Dict, Iterator, List, Optional, TypeVar, Union, overload diff --git a/tests/_path.py b/tests/_path.py index 9762c5ec..25c799fa 100644 --- a/tests/_path.py +++ b/tests/_path.py @@ -1,14 +1,9 @@ -# from jaraco.path 3.6 +# from jaraco.path 3.7 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 +from typing import Dict, Protocol, Union +from typing import runtime_checkable class Symlink(str): From 34fd7365cad782511b7c824f65c054d654bb066c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 7 Jul 2023 12:07:03 -0400 Subject: [PATCH 237/480] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/+drop-py37.feature.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/+drop-py37.feature.rst diff --git a/NEWS.rst b/NEWS.rst index 255bbd32..18166afb 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v6.8.0 +====== + +Features +-------- + +- Require Python 3.8 or later. + + v6.7.0 ====== diff --git a/newsfragments/+drop-py37.feature.rst b/newsfragments/+drop-py37.feature.rst deleted file mode 100644 index ccabdaa3..00000000 --- a/newsfragments/+drop-py37.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Require Python 3.8 or later. From a7310562fad7a8834c9810c1edd8e00b03e1394b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 7 Jul 2023 21:48:23 -0400 Subject: [PATCH 238/480] Increase visibility of security policy. (#4) * Create SECURITY.md Signed-off-by: Joyce * Remove the security contact from the README, as it's now redundant. Closes jaraco/tidelift#3. --------- Signed-off-by: Joyce Co-authored-by: Joyce --- README.rst | 7 ------- SECURITY.md | 3 +++ 2 files changed, 3 insertions(+), 7 deletions(-) create mode 100644 SECURITY.md diff --git a/README.rst b/README.rst index 7b317c71..087365cd 100644 --- a/README.rst +++ b/README.rst @@ -9,10 +9,3 @@ Available as part of the Tidelift Subscription. This project and the maintainers of thousands of other packages are working with Tidelift to deliver one enterprise subscription that covers all of the open source you use. `Learn more `_. - -Security Contact -================ - -To report a security vulnerability, please use the -`Tidelift security contact `_. -Tidelift will coordinate the fix and disclosure. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..54f99acb --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,3 @@ +# Security Contact + +To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. From c43962adf34c28c22573093419e5e98b2e57cc07 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 10 Jul 2023 23:34:53 -0400 Subject: [PATCH 239/480] Remove TOX_WORK_DIR workaround, no longer necessary with tox 4. Ref tox-dev/tox#3050. --- tox.ini | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tox.ini b/tox.ini index 1093e028..e51d652d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,3 @@ -[tox] -toxworkdir={env:TOX_WORK_DIR:.tox} - - [testenv] deps = setenv = From ae9ca4393295d961fe6c1b0f1cbc957f07ab4afd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 8 Jun 2023 16:47:44 -0400 Subject: [PATCH 240/480] Add origin property. Ref #404. --- importlib_metadata/__init__.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 6ba414e5..79a5b17b 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -3,8 +3,10 @@ import abc import csv import sys +import json import zipp import email +import types import inspect import pathlib import operator @@ -618,6 +620,16 @@ def url_req_space(req): space = url_req_space(section.value) yield section.value + space + quoted_marker(section.name) + @property + def origin(self): + return self._load_json('direct_url.json') + + def _load_json(self, filename): + return pass_none(json.loads)( + self.read_text(filename), + object_hook=lambda data: types.SimpleNamespace(**data), + ) + class DistributionFinder(MetaPathFinder): """ From 0e2032c4754c598ba75e467c64009ba4490ddea9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 31 Aug 2023 18:42:14 -0400 Subject: [PATCH 241/480] Pin against sphinx 7.2.5 as workaround for sphinx/sphinx-doc#11662. Closes jaraco/skeleton#88. --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.cfg b/setup.cfg index 46f7bdf7..4f184c7e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -45,6 +45,8 @@ testing = docs = # upstream sphinx >= 3.5 + # workaround for sphinx/sphinx-doc#11662 + sphinx < 7.2.5 jaraco.packaging >= 9.3 rst.linker >= 1.9 furo From 92d2d8e1aff997f3877239230c9490ed9cdd1222 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 1 Sep 2023 18:46:27 -0400 Subject: [PATCH 242/480] Allow GITHUB_* settings to pass through to tests. --- .github/workflows/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b8224099..67d9d3bc 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -36,6 +36,10 @@ env: # Must be "1". TOX_PARALLEL_NO_SPINNER: 1 + # Ensure tests can sense settings about the environment + TOX_OVERRIDE: >- + testenv.pass_env+=GITHUB_* + jobs: test: From f3dc1f4776c94a9a4a7c0e8c5b49c532b0a7d411 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 1 Sep 2023 18:49:13 -0400 Subject: [PATCH 243/480] Remove spinner disablement. If it's not already fixed upstream, that's where it should be fixed. --- .github/workflows/main.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 67d9d3bc..30c9615d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,10 +32,6 @@ env: 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 - # Ensure tests can sense settings about the environment TOX_OVERRIDE: >- testenv.pass_env+=GITHUB_* From 0484daa8a6f72c9ad4e1784f9181c2488a191d8e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 1 Sep 2023 18:53:55 -0400 Subject: [PATCH 244/480] Clean up 'color' environment variables. The TOX_TESTENV_PASSENV hasn't been useful for some time and by its mere presence wasted a lot of time today under the assumption that it's doing something. Instead, just rely on one variable FORCE_COLOR. If it's not honored, then that should be the fix upstream. --- .github/workflows/main.yml | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 30c9615d..f3028549 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,26 +6,8 @@ permissions: contents: read 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. For tox, it must be one of - # , 0, 1, false, no, off, on, true, yes. The only enabling value - # in common is "1". + # Environment variable to support color support (jaraco/skeleton#66) 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") - 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' From b02bf32bae729d53bdb7c9649d6ec36afdb793ee Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 10 Sep 2023 13:27:03 -0400 Subject: [PATCH 245/480] Add diff-cover check to Github Actions CI. Closes jaraco/skeleton#90. --- .github/workflows/main.yml | 18 ++++++++++++++++++ tox.ini | 8 ++++++++ 2 files changed, 26 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f3028549..fa326a26 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -53,6 +53,24 @@ jobs: - name: Run run: tox + diffcov: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: 3.x + - name: Install tox + run: | + python -m pip install tox + - name: Evaluate coverage + run: tox + env: + TOXENV: diffcov + docs: runs-on: ubuntu-latest env: diff --git a/tox.ini b/tox.ini index e51d652d..3b4414b4 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,14 @@ usedevelop = True extras = testing +[testenv:diffcov] +deps = + diff-cover +commands = + pytest {posargs} --cov-report xml + diff-cover coverage.xml --compare-branch=origin/main --html-report diffcov.html + diff-cover coverage.xml --compare-branch=origin/main --fail-under=100 + [testenv:docs] extras = docs From a6256e2935468b72a61aa7fda1e036faef3bfb3d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 10 Sep 2023 13:59:47 -0400 Subject: [PATCH 246/480] Add descriptions to the tox environments. Closes jaraco/skeleton#91. --- tox.ini | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tox.ini b/tox.ini index 3b4414b4..1950b4ef 100644 --- a/tox.ini +++ b/tox.ini @@ -1,4 +1,5 @@ [testenv] +description = perform primary checks (tests, style, types, coverage) deps = setenv = PYTHONWARNDEFAULTENCODING = 1 @@ -9,6 +10,7 @@ extras = testing [testenv:diffcov] +description = run tests and check that diff from main is covered deps = diff-cover commands = @@ -17,6 +19,7 @@ commands = diff-cover coverage.xml --compare-branch=origin/main --fail-under=100 [testenv:docs] +description = build the documentation extras = docs testing @@ -26,6 +29,7 @@ commands = python -m sphinxlint [testenv:finalize] +description = assemble changelog and tag a release skip_install = True deps = towncrier @@ -36,6 +40,7 @@ commands = [testenv:release] +description = publish the package to PyPI and GitHub skip_install = True deps = build From 928e9a86d61d3a660948bcba7689f90216cc8243 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 10 Sep 2023 14:10:31 -0400 Subject: [PATCH 247/480] Add FORCE_COLOR to the TOX_OVERRIDE for GHA. Requires tox 4.11.1. Closes jaraco/skeleton#89. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fa326a26..28e36786 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -16,7 +16,7 @@ env: # Ensure tests can sense settings about the environment TOX_OVERRIDE: >- - testenv.pass_env+=GITHUB_* + testenv.pass_env+=GITHUB_*,FORCE_COLOR jobs: From ca1831c2148fe5ddbffd001de76ff5f6005f812c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 18 Sep 2023 11:05:36 -0400 Subject: [PATCH 248/480] Prefer ``pass_env`` in tox config. Preferred failure mode for tox-dev/tox#3127 and closes jaraco/skeleton#92. --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 1950b4ef..33da3deb 100644 --- a/tox.ini +++ b/tox.ini @@ -34,7 +34,7 @@ skip_install = True deps = towncrier jaraco.develop >= 7.23 -passenv = * +pass_env = * commands = python -m jaraco.develop.finalize @@ -46,7 +46,7 @@ deps = build twine>=3 jaraco.develop>=7.1 -passenv = +pass_env = TWINE_PASSWORD GITHUB_TOKEN setenv = From 9d6d8b5ee689769cf5a36475d4af3e9f65e3fca6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 21 Sep 2023 06:13:21 -0400 Subject: [PATCH 249/480] Update changelog to reflect backward-incompatible effect. Ref #459. --- NEWS.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/NEWS.rst b/NEWS.rst index 18166afb..92cc0da8 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -131,6 +131,10 @@ v4.11.4 duplicate entry points by packages varying only by non-normalized name are hidden. +Note (#459): This change had a backward-incompatible effect for +any installers that created metadata in the filesystem with dashes +in the package names (not replaced by underscores). + v4.11.3 ======= From 066e24d79994c69424ecdb51f7048d6e466fc392 Mon Sep 17 00:00:00 2001 From: Amund Hov Date: Wed, 11 Oct 2023 14:45:35 +0200 Subject: [PATCH 250/480] With commit 0c819641d314ac496eb32b55f2b15215fa6fa55f the behavior of Entrypoints gets a bit disorienting. Change __repr__ to reflect that Entrpoints is are longer indexable by integers like tuples, but signal our custom behavior. --- importlib_metadata/__init__.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 6ba414e5..bc8714b8 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -293,6 +293,14 @@ def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override] except StopIteration: raise KeyError(name) + def __repr__(self): + """ + Repr with classname and tuple constructor to + signal that we deviate from regular tuple behavior. + """ + return '%s(%r)' % (self.__class__.__name__, tuple(self)) + + def select(self, **params): """ Select entry points from self that match the From 03f03e7802b0842b41f70b2b1c17ab26551a7533 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 5 Nov 2023 09:43:46 -0500 Subject: [PATCH 251/480] Limit sphinxlint jobs to 1. Workaround for sphinx-contrib/sphinx-lint#83. --- tox.ini | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 33da3deb..331eeed9 100644 --- a/tox.ini +++ b/tox.ini @@ -26,7 +26,9 @@ extras = changedir = docs commands = python -m sphinx -W --keep-going . {toxinidir}/build/html - python -m sphinxlint + python -m sphinxlint \ + # workaround for sphinx-contrib/sphinx-lint#83 + --jobs 1 [testenv:finalize] description = assemble changelog and tag a release From 75d9cc1b7cb6f84e7a16a83ec3abb9a478fdb130 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 15 Nov 2023 19:57:45 +0600 Subject: [PATCH 252/480] Upgrade GitHub Actions checkout (jaraco/skeleton#94) Also, upgrade from `pypy3.9` to `pypy3.10` and remove the `continue-on-error` for Python 3.12. As recommended at jaraco/cssutils#41 --- .github/workflows/main.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 28e36786..10828667 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -36,12 +36,12 @@ jobs: platform: ubuntu-latest - python: "3.10" platform: ubuntu-latest - - python: pypy3.9 + - python: pypy3.10 platform: ubuntu-latest runs-on: ${{ matrix.platform }} - continue-on-error: ${{ matrix.python == '3.12' }} + continue-on-error: ${{ matrix.python == '3.13' }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v4 with: @@ -56,7 +56,7 @@ jobs: diffcov: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Setup Python @@ -76,7 +76,7 @@ jobs: env: TOXENV: docs steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v4 - name: Install tox @@ -109,7 +109,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v4 with: From 5732ebeeaa9480f8cd80c96a3183d7b247f27214 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Wed, 15 Nov 2023 20:08:10 +0600 Subject: [PATCH 253/480] GitHub Actions: Combine tox jobs diffcov and docs (jaraco/skeleton#95) Code reuse Co-authored-by: Jason R. Coombs --- .github/workflows/main.yml | 37 +++++++++++-------------------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 10828667..9682985c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -48,12 +48,15 @@ jobs: python-version: ${{ matrix.python }} allow-prereleases: true - name: Install tox - run: | - python -m pip install tox + run: python -m pip install tox - name: Run run: tox - diffcov: + collateral: + strategy: + fail-fast: false + matrix: + job: [diffcov, docs] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -64,33 +67,16 @@ jobs: with: python-version: 3.x - name: Install tox - run: | - python -m pip install tox - - name: Evaluate coverage - run: tox - env: - TOXENV: diffcov - - docs: - runs-on: ubuntu-latest - env: - TOXENV: docs - steps: - - uses: actions/checkout@v4 - - name: Setup Python - uses: actions/setup-python@v4 - - name: Install tox - run: | - python -m pip install tox - - name: Run - run: tox + run: python -m pip install tox + - name: Eval ${{ matrix.job }} + run: tox -e ${{ matrix.job }} check: # This job does nothing and is only used for the branch protection if: always() needs: - test - - docs + - collateral runs-on: ubuntu-latest @@ -115,8 +101,7 @@ jobs: with: python-version: 3.x - name: Install tox - run: | - python -m pip install tox + run: python -m pip install tox - name: Run run: tox -e release env: From 5f2e291ddf44fe5f5dc105d1d57dd2551f4f24b0 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Thu, 28 Sep 2023 10:16:20 +0200 Subject: [PATCH 254/480] Fix new typo found using codespell --- importlib_metadata/_adapters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/importlib_metadata/_adapters.py b/importlib_metadata/_adapters.py index e33cba5e..120e43a0 100644 --- a/importlib_metadata/_adapters.py +++ b/importlib_metadata/_adapters.py @@ -54,7 +54,7 @@ def __iter__(self): def __getitem__(self, item): """ Warn users that a ``KeyError`` can be expected when a - mising key is supplied. Ref python/importlib_metadata#371. + missing key is supplied. Ref python/importlib_metadata#371. """ res = super().__getitem__(item) if res is None: From 26f420a97e73a2ab695023f6cc21f5c786d2b289 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 28 Nov 2023 11:43:20 -0500 Subject: [PATCH 255/480] Remove news fragment after allowing time to be processed downstream. --- newsfragments/+drop-py37.feature.rst | 1 - 1 file changed, 1 deletion(-) delete mode 100644 newsfragments/+drop-py37.feature.rst diff --git a/newsfragments/+drop-py37.feature.rst b/newsfragments/+drop-py37.feature.rst deleted file mode 100644 index ccabdaa3..00000000 --- a/newsfragments/+drop-py37.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Require Python 3.8 or later. From 33dd01267b6a886217bae3ebd5df5b689e2ab722 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 29 Nov 2023 13:21:17 -0500 Subject: [PATCH 256/480] Suppress deprecation warning in dateutil. Workaround for dateutil/dateutil#1284. --- pytest.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pytest.ini b/pytest.ini index d9a15ed1..f9533b57 100644 --- a/pytest.ini +++ b/pytest.ini @@ -24,4 +24,7 @@ filterwarnings= # pypa/build#615 ignore:'encoding' argument not specified::build.env + # dateutil/dateutil#1284 + ignore:datetime.datetime.utcfromtimestamp:DeprecationWarning:dateutil.tz.tz + ## end upstream From c3b80ebe0fe37b5e192342f66ddc09f63ed8e325 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 1 Dec 2023 10:45:47 -0500 Subject: [PATCH 257/480] =?UTF-8?q?=E2=9A=AB=20Fade=20to=20black.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- importlib_metadata/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index bc8714b8..5e7e6ad7 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -299,8 +299,7 @@ def __repr__(self): signal that we deviate from regular tuple behavior. """ return '%s(%r)' % (self.__class__.__name__, tuple(self)) - - + def select(self, **params): """ Select entry points from self that match the From dfa7fede8223a69e22006a687f85009d8aecd81c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 1 Dec 2023 10:59:09 -0500 Subject: [PATCH 258/480] =?UTF-8?q?=F0=9F=A7=8E=E2=80=8D=E2=99=80=EF=B8=8F?= =?UTF-8?q?=20Genuflect=20to=20the=20types.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a redundant type definition to a test to avoid warnings about the inner types being unchecked. Closes #478. --- 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 79181bf4..a0be5bae 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -400,7 +400,7 @@ def test_packages_distributions_all_module_types(self): assert not any(name.endswith('.dist-info') for name in distributions) - def test_packages_distributions_symlinked_top_level(self): + def test_packages_distributions_symlinked_top_level(self) -> None: """ Distribution is resolvable from a simple top-level symlink in RECORD. See #452. From 177ec8cd8158a97c9e3df33bba991a079491778e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 1 Dec 2023 12:32:41 -0500 Subject: [PATCH 259/480] Add news fragment. Intended for #473. --- newsfragments/473.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/473.feature.rst diff --git a/newsfragments/473.feature.rst b/newsfragments/473.feature.rst new file mode 100644 index 00000000..a31a0c5c --- /dev/null +++ b/newsfragments/473.feature.rst @@ -0,0 +1 @@ +Added EntryPoints.__repr__ \ No newline at end of file From 4e118be4861fc0b2b15f856dacf52681f5be1b42 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 1 Dec 2023 12:32:48 -0500 Subject: [PATCH 260/480] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/473.feature.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/473.feature.rst diff --git a/NEWS.rst b/NEWS.rst index 92cc0da8..3eed2bad 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v6.9.0 +====== + +Features +-------- + +- Added EntryPoints.__repr__ (#473) + + v6.8.0 ====== diff --git a/newsfragments/473.feature.rst b/newsfragments/473.feature.rst deleted file mode 100644 index a31a0c5c..00000000 --- a/newsfragments/473.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Added EntryPoints.__repr__ \ No newline at end of file From b3918e751538bac84c87c6fe94d9ef5edbcd5e5e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 1 Dec 2023 16:06:52 -0500 Subject: [PATCH 261/480] Added diagnose script. Closes #461. --- importlib_metadata/diagnose.py | 21 +++++++++++++++++++++ newsfragments/461.feature.rst | 1 + 2 files changed, 22 insertions(+) create mode 100644 importlib_metadata/diagnose.py create mode 100644 newsfragments/461.feature.rst diff --git a/importlib_metadata/diagnose.py b/importlib_metadata/diagnose.py new file mode 100644 index 00000000..e405471a --- /dev/null +++ b/importlib_metadata/diagnose.py @@ -0,0 +1,21 @@ +import sys + +from . import Distribution + + +def inspect(path): + print("Inspecting", path) + dists = list(Distribution.discover(path=[path])) + if not dists: + return + print("Found", len(dists), "packages:", end=' ') + print(', '.join(dist.name for dist in dists)) + + +def run(): + for path in sys.path: + inspect(path) + + +if __name__ == '__main__': + run() diff --git a/newsfragments/461.feature.rst b/newsfragments/461.feature.rst new file mode 100644 index 00000000..bf07a91b --- /dev/null +++ b/newsfragments/461.feature.rst @@ -0,0 +1 @@ +Added diagnose script. \ No newline at end of file From 02bbfb0685ce826daa3e6d85d4002b90b70a5e30 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 1 Dec 2023 16:08:37 -0500 Subject: [PATCH 262/480] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/461.feature.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/461.feature.rst diff --git a/NEWS.rst b/NEWS.rst index 3eed2bad..6f3a7ff0 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v6.10.0 +======= + +Features +-------- + +- Added diagnose script. (#461) + + v6.9.0 ====== diff --git a/newsfragments/461.feature.rst b/newsfragments/461.feature.rst deleted file mode 100644 index bf07a91b..00000000 --- a/newsfragments/461.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Added diagnose script. \ No newline at end of file From 72383028a3ca5c7e6ab9a58e6e1e06d30079c905 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 3 Dec 2023 10:55:25 -0500 Subject: [PATCH 263/480] Restore pypy tests now that 3.10 is the standard. Bypasses issue in #463. --- .github/workflows/main.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0bbaaf25..9682985c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -36,9 +36,8 @@ jobs: platform: ubuntu-latest - python: "3.10" platform: ubuntu-latest - # disabled for #463 - # - python: pypy3.10 - # platform: ubuntu-latest + - python: pypy3.10 + platform: ubuntu-latest runs-on: ${{ matrix.platform }} continue-on-error: ${{ matrix.python == '3.13' }} steps: From 84399188dd9792364603b2a95149f7c11961bca7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 30 Jul 2023 15:35:14 -0400 Subject: [PATCH 264/480] Add changelog --- newsfragments/404.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/404.feature.rst diff --git a/newsfragments/404.feature.rst b/newsfragments/404.feature.rst new file mode 100644 index 00000000..47cf2447 --- /dev/null +++ b/newsfragments/404.feature.rst @@ -0,0 +1 @@ +Added ``Distribution.origin`` supplying the ``direct_url.json`` in a ``SimpleNamespace``. \ No newline at end of file From f480907325bcb79e614da9f826834c25b53839a7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 30 Jul 2023 16:01:38 -0400 Subject: [PATCH 265/480] Add test capturing expectation. Ref #404 --- tests/fixtures.py | 26 ++++++++++++++++++++++++++ tests/test_main.py | 7 +++++++ 2 files changed, 33 insertions(+) diff --git a/tests/fixtures.py b/tests/fixtures.py index c0b0fa32..e39fc071 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -1,6 +1,7 @@ import os import sys import copy +import json import shutil import pathlib import tempfile @@ -127,6 +128,31 @@ def make_uppercase(self): build_files(files, self.site_dir) +class DistInfoPkgEditable(DistInfoPkg): + """ + Package with a PEP 660 direct_url.json. + """ + + some_hash = '524127ce937f7cb65665130c695abd18ca386f60bb29687efb976faa1596fdcc' + files: FilesSpec = { + 'distinfo_pkg-1.0.0.dist-info': { + 'direct_url.json': json.dumps( + { + "archive_info": { + "hash": f"sha256={some_hash}", + "hashes": {"sha256": f"{some_hash}"}, + }, + "url": "file:///path/to/distinfo_pkg-1.0.0.editable-py3-none-any.whl", + } + ) + }, + } + + def setUp(self): + super().setUp() + build_files(DistInfoPkgEditable.files, self.site_dir) + + class DistInfoPkgWithDot(OnSysPath, SiteDir): files: FilesSpec = { "pkg_dot-1.0.0.dist-info": { diff --git a/tests/test_main.py b/tests/test_main.py index 79181bf4..38377788 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -457,3 +457,10 @@ def import_names_from_package(package_name): # 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'} + + +class EditableDistributionTest(fixtures.DistInfoPkgEditable, unittest.TestCase): + def test_origin(self): + dist = Distribution.from_name('distinfo-pkg') + assert dist.origin.url.endswith('.whl') + assert dist.origin.archive_info.hashes.sha256 From e886c996ee154c26861520b8440351f931470373 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 3 Dec 2023 12:13:50 -0500 Subject: [PATCH 266/480] Use a SiteBuilder class to build files in the site, traversing the class hierarchy explicitly and avoiding the need for separate setUp calls for each. --- tests/fixtures.py | 62 ++++++++++++++--------------------------------- 1 file changed, 18 insertions(+), 44 deletions(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index c0b0fa32..8df0860c 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -85,7 +85,15 @@ def setUp(self): self.fixtures.enter_context(self.add_sys_path(self.site_dir)) -class DistInfoPkg(OnSysPath, SiteDir): +class SiteBuilder(SiteDir): + def setUp(self): + super().setUp() + for cls in self.__class__.mro(): + with contextlib.suppress(AttributeError): + build_files(cls.files, prefix=self.site_dir) + + +class DistInfoPkg(OnSysPath, SiteBuilder): files: FilesSpec = { "distinfo_pkg-1.0.0.dist-info": { "METADATA": """ @@ -112,10 +120,6 @@ def main(): """, } - def setUp(self): - super().setUp() - build_files(DistInfoPkg.files, self.site_dir) - def make_uppercase(self): """ Rewrite metadata with everything uppercase. @@ -127,7 +131,7 @@ def make_uppercase(self): build_files(files, self.site_dir) -class DistInfoPkgWithDot(OnSysPath, SiteDir): +class DistInfoPkgWithDot(OnSysPath, SiteBuilder): files: FilesSpec = { "pkg_dot-1.0.0.dist-info": { "METADATA": """ @@ -137,12 +141,8 @@ class DistInfoPkgWithDot(OnSysPath, SiteDir): }, } - def setUp(self): - super().setUp() - build_files(DistInfoPkgWithDot.files, self.site_dir) - -class DistInfoPkgWithDotLegacy(OnSysPath, SiteDir): +class DistInfoPkgWithDotLegacy(OnSysPath, SiteBuilder): files: FilesSpec = { "pkg.dot-1.0.0.dist-info": { "METADATA": """ @@ -158,18 +158,12 @@ class DistInfoPkgWithDotLegacy(OnSysPath, SiteDir): }, } - def setUp(self): - super().setUp() - build_files(DistInfoPkgWithDotLegacy.files, self.site_dir) - -class DistInfoPkgOffPath(SiteDir): - def setUp(self): - super().setUp() - build_files(DistInfoPkg.files, self.site_dir) +class DistInfoPkgOffPath(SiteBuilder): + files = DistInfoPkg.files -class EggInfoPkg(OnSysPath, SiteDir): +class EggInfoPkg(OnSysPath, SiteBuilder): files: FilesSpec = { "egginfo_pkg.egg-info": { "PKG-INFO": """ @@ -204,12 +198,8 @@ def main(): """, } - def setUp(self): - super().setUp() - build_files(EggInfoPkg.files, prefix=self.site_dir) - -class EggInfoPkgPipInstalledNoToplevel(OnSysPath, SiteDir): +class EggInfoPkgPipInstalledNoToplevel(OnSysPath, SiteBuilder): files: FilesSpec = { "egg_with_module_pkg.egg-info": { "PKG-INFO": "Name: egg_with_module-pkg", @@ -239,12 +229,8 @@ def main(): """, } - def setUp(self): - super().setUp() - build_files(EggInfoPkgPipInstalledNoToplevel.files, prefix=self.site_dir) - -class EggInfoPkgPipInstalledNoModules(OnSysPath, SiteDir): +class EggInfoPkgPipInstalledNoModules(OnSysPath, SiteBuilder): files: FilesSpec = { "egg_with_no_modules_pkg.egg-info": { "PKG-INFO": "Name: egg_with_no_modules-pkg", @@ -269,12 +255,8 @@ class EggInfoPkgPipInstalledNoModules(OnSysPath, SiteDir): }, } - def setUp(self): - super().setUp() - build_files(EggInfoPkgPipInstalledNoModules.files, prefix=self.site_dir) - -class EggInfoPkgSourcesFallback(OnSysPath, SiteDir): +class EggInfoPkgSourcesFallback(OnSysPath, SiteBuilder): files: FilesSpec = { "sources_fallback_pkg.egg-info": { "PKG-INFO": "Name: sources_fallback-pkg", @@ -295,12 +277,8 @@ def main(): """, } - def setUp(self): - super().setUp() - build_files(EggInfoPkgSourcesFallback.files, prefix=self.site_dir) - -class EggInfoFile(OnSysPath, SiteDir): +class EggInfoFile(OnSysPath, SiteBuilder): files: FilesSpec = { "egginfo_file.egg-info": """ Metadata-Version: 1.0 @@ -316,10 +294,6 @@ class EggInfoFile(OnSysPath, SiteDir): """, } - def setUp(self): - super().setUp() - build_files(EggInfoFile.files, prefix=self.site_dir) - # dedent all text strings before writing orig = _path.create.registry[str] From 84418f85c38d2bba9925f576ef25dda1de9e4647 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 3 Dec 2023 12:25:45 -0500 Subject: [PATCH 267/480] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/404.feature.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/404.feature.rst diff --git a/NEWS.rst b/NEWS.rst index 6f3a7ff0..2a6cc7ba 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v6.11.0 +======= + +Features +-------- + +- Added ``Distribution.origin`` supplying the ``direct_url.json`` in a ``SimpleNamespace``. (#404) + + v6.10.0 ======= diff --git a/newsfragments/404.feature.rst b/newsfragments/404.feature.rst deleted file mode 100644 index 47cf2447..00000000 --- a/newsfragments/404.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Added ``Distribution.origin`` supplying the ``direct_url.json`` in a ``SimpleNamespace``. \ No newline at end of file From 37113c20d89b0646ffaf4922ba11dad81cd27e7f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 3 Dec 2023 12:23:20 -0500 Subject: [PATCH 268/480] Removed EntryPoint access by numeric index (tuple behavior). --- importlib_metadata/__init__.py | 30 +---------------------------- newsfragments/+9c754ffa.removal.rst | 1 + 2 files changed, 2 insertions(+), 29 deletions(-) create mode 100644 newsfragments/+9c754ffa.removal.rst diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index f9984697..312d6966 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -23,7 +23,6 @@ NullFinder, StrPath, install, - pypy_partial, ) from ._functools import method_cache, pass_none from ._itertools import always_iterable, unique_everseen @@ -128,34 +127,7 @@ def valid(line: str): return line and not line.startswith('#') -class DeprecatedTuple: - """ - Provide subscript item access for backward compatibility. - - >>> recwarn = getfixture('recwarn') - >>> ep = EntryPoint(name='name', value='value', group='group') - >>> ep[:] - ('name', 'value', 'group') - >>> ep[0] - 'name' - >>> len(recwarn) - 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.", - DeprecationWarning, - stacklevel=pypy_partial(2), - ) - - def __getitem__(self, item): - self._warn() - return self._key()[item] - - -class EntryPoint(DeprecatedTuple): +class EntryPoint: """An entry point as defined by Python packaging conventions. See `the packaging docs on entry points diff --git a/newsfragments/+9c754ffa.removal.rst b/newsfragments/+9c754ffa.removal.rst new file mode 100644 index 00000000..78e66eb9 --- /dev/null +++ b/newsfragments/+9c754ffa.removal.rst @@ -0,0 +1 @@ +Removed EntryPoint access by numeric index (tuple behavior). \ No newline at end of file From fb492e17faee9a6056ca31cf6feffec96c3b9d5a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 3 Dec 2023 12:31:24 -0500 Subject: [PATCH 269/480] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/+9c754ffa.removal.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/+9c754ffa.removal.rst diff --git a/NEWS.rst b/NEWS.rst index 2a6cc7ba..ae1e9cc8 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v7.0.0 +====== + +Deprecations and Removals +------------------------- + +- Removed EntryPoint access by numeric index (tuple behavior). + + v6.11.0 ======= diff --git a/newsfragments/+9c754ffa.removal.rst b/newsfragments/+9c754ffa.removal.rst deleted file mode 100644 index 78e66eb9..00000000 --- a/newsfragments/+9c754ffa.removal.rst +++ /dev/null @@ -1 +0,0 @@ -Removed EntryPoint access by numeric index (tuple behavior). \ No newline at end of file From 97a5f44787ac5a928534cdf724210c429621435c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 4 Dec 2023 15:53:37 -0500 Subject: [PATCH 270/480] Update Github Actions badge per actions/starter-workflows#1525. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index b703d490..41bcfbe8 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,7 @@ .. image:: https://img.shields.io/pypi/pyversions/PROJECT.svg -.. image:: https://github.com/PROJECT_PATH/workflows/tests/badge.svg +.. image:: https://github.com/PROJECT_PATH/actions/workflows/main.yml/badge.svg :target: https://github.com/PROJECT_PATH/actions?query=workflow%3A%22tests%22 :alt: tests From 8bff8b034a0bbf0273a38f0a0cc41e3a52b26864 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Tue, 5 Dec 2023 15:48:52 +0100 Subject: [PATCH 271/480] Enable testing merge queues @ GitHub Actions CI/CD (jaraco/skeleton#93) This allows org-hosted projects to start enabling merge queues in the repository settings. With that, GitHub would trigger a separate event against a merge commit derived from merging several pull requests with the target branch. --- .github/workflows/main.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9682985c..387d01aa 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,6 +1,11 @@ name: tests -on: [push, pull_request] +on: + merge_group: + push: + branches-ignore: + - gh-readonly-queue/** # Temporary merge queue-related GH-made branches + pull_request: permissions: contents: read From e4bd6091a1fbe26fe113051f0f47875d627c7ed2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 11 Dec 2023 10:46:32 -0500 Subject: [PATCH 272/480] Separate collateral jobs on different lines for easier override/extension. --- .github/workflows/main.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 387d01aa..a079bbfb 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -61,7 +61,9 @@ jobs: strategy: fail-fast: false matrix: - job: [diffcov, docs] + job: + - diffcov + - docs runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 From 3ef139e0640d0492d89d9978ab50502fc2b06ae2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 14 Dec 2023 16:06:07 -0500 Subject: [PATCH 273/480] Remove using documentation and instead point users to the stdlib docs. Closes #466. --- docs/index.rst | 8 +- docs/using.rst | 381 ------------------------------------------------- 2 files changed, 3 insertions(+), 386 deletions(-) delete mode 100644 docs/using.rst diff --git a/docs/index.rst b/docs/index.rst index 0c017d91..66755216 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -9,16 +9,14 @@ Welcome to |project| documentation! enabling early access to features of future Python versions and making functionality available for older Python versions. Users are encouraged to use the Python standard library where suitable and fall back to -this library for future compatibility. Developers looking for detailed API -descriptions should refer to the standard library documentation. - -The documentation here includes a general :ref:`usage ` guide. +this library for future compatibility. For general usage guidance, start +with :mod:`importlib.metadata` but substitute ``importlib_metadata`` +for ``importlib.metadata``. .. toctree:: :maxdepth: 1 - using api migration history diff --git a/docs/using.rst b/docs/using.rst deleted file mode 100644 index 17af34d3..00000000 --- a/docs/using.rst +++ /dev/null @@ -1,381 +0,0 @@ -.. _using: - -================================= - Using :mod:`!importlib_metadata` -================================= - -``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). -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 -:mod:`importlib.resources`, -this package can eliminate the need to use the older and less efficient -``pkg_resources`` package. - -``importlib_metadata`` operates on third-party *distribution packages* -installed into Python's ``site-packages`` directory via tools such as -`pip `_. -Specifically, it works with distributions with discoverable -``dist-info`` or ``egg-info`` directories, -and metadata defined by the :ref:`packaging:core-metadata`. - -.. important:: - - These are *not* necessarily equivalent to or correspond 1:1 with - the top-level *import package* names - that can be imported inside Python code. - One *distribution package* can contain multiple *import packages* - (and single modules), - and one top-level *import package* - may map to multiple *distribution packages* - if it is a namespace package. - You can use :ref:`package_distributions() ` - to get a mapping between them. - -By default, distribution metadata can live on the file system -or in zip archives on -:data:`sys.path`. Through an extension mechanism, the metadata can live almost -anywhere. - - -.. seealso:: - - https://importlib-metadata.readthedocs.io/ - The documentation for ``importlib_metadata``, which supplies a - backport of ``importlib.metadata``. - This includes an `API reference - `__ - for this module's classes and functions, - as well as a `migration guide - `__ - for existing users of ``pkg_resources``. - - -Overview -======== - -Let's say you wanted to get the version string for a -:term:`packaging:Distribution Package` you've installed -using ``pip``. We start by creating a virtual environment and installing -something into it:: - - $ python -m venv example - $ source example/bin/activate - (example) $ python -m pip install importlib_metadata - (example) $ python -m pip install wheel - -You can get the version string for ``wheel`` by running the following:: - - (example) $ python - >>> from importlib_metadata import version - >>> version('wheel') - '0.32.3' - -You can also get a collection of entry points selectable by properties of the EntryPoint (typically 'group' or 'name'), such as -``console_scripts``, ``distutils.commands`` and others. Each group contains a -collection of :ref:`EntryPoint ` objects. - -You can get the :ref:`metadata for a distribution `:: - - >>> list(metadata('wheel')) - ['Metadata-Version', 'Name', 'Version', 'Summary', 'Home-page', 'Author', 'Author-email', 'Maintainer', 'Maintainer-email', 'License', 'Project-URL', 'Project-URL', 'Project-URL', 'Keywords', 'Platform', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Requires-Python', 'Provides-Extra', 'Requires-Dist', 'Requires-Dist'] - -You can also get a :ref:`distribution's version number `, list its -:ref:`constituent files `, and get a list of the distribution's -:ref:`requirements`. - - -Functional API -============== - -This package provides the following functionality via its public API. - - -.. _entry-points: - -Entry points ------------- - -The ``entry_points()`` function returns a collection of entry points. -Entry points are represented by ``EntryPoint`` instances; -each ``EntryPoint`` has a ``.name``, ``.group``, and ``.value`` attributes and -a ``.load()`` method to resolve the value. There are also ``.module``, -``.attr``, and ``.extras`` attributes for getting the components of the -``.value`` attribute. - -Query all entry points:: - - >>> eps = entry_points() - -The ``entry_points()`` function returns an ``EntryPoints`` object, -a collection of all ``EntryPoint`` objects with ``names`` and ``groups`` -attributes for convenience:: - - >>> sorted(eps.groups) - ['console_scripts', 'distutils.commands', 'distutils.setup_keywords', 'egg_info.writers', 'setuptools.installation'] - -``EntryPoints`` has a ``select`` method to select entry points -matching specific properties. Select entry points in the -``console_scripts`` group:: - - >>> scripts = eps.select(group='console_scripts') - -Equivalently, since ``entry_points`` passes keyword arguments -through to select:: - - >>> scripts = entry_points(group='console_scripts') - -Pick out a specific script named "wheel" (found in the wheel project):: - - >>> 'wheel' in scripts.names - True - >>> wheel = scripts['wheel'] - -Equivalently, query for that entry point during selection:: - - >>> (wheel,) = entry_points(group='console_scripts', name='wheel') - >>> (wheel,) = entry_points().select(group='console_scripts', name='wheel') - -Inspect the resolved entry point:: - - >>> wheel - EntryPoint(name='wheel', value='wheel.cli:main', group='console_scripts') - >>> wheel.module - 'wheel.cli' - >>> wheel.attr - 'main' - >>> wheel.extras - [] - >>> main = wheel.load() - >>> main - - -The ``group`` and ``name`` are arbitrary values defined by the package author -and usually a client will wish to resolve all entry points for a particular -group. Read `the setuptools docs -`_ -for more information on entry points, their definition, and usage. - -*Compatibility Note* - -The "selectable" entry points were introduced in ``importlib_metadata`` -3.6 and Python 3.10. Prior to those changes, ``entry_points`` accepted -no parameters and always returned a dictionary of entry points, keyed -by group. With ``importlib_metadata`` 5.0 and Python 3.12, -``entry_points`` always returns an ``EntryPoints`` object. See -`backports.entry_points_selectable `_ -for compatibility options. - - -.. _metadata: - -Distribution metadata ---------------------- - -Every :term:`packaging:Distribution Package` includes some metadata, -which you can extract using the -``metadata()`` function:: - - >>> wheel_metadata = metadata('wheel') - -The keys of the returned data structure, a ``PackageMetadata``, -name the metadata keywords, and -the values are returned unparsed from the distribution metadata:: - - >>> wheel_metadata['Requires-Python'] - '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*' - -``PackageMetadata`` also presents a ``json`` attribute that returns -all the metadata in a JSON-compatible form per PEP 566:: - - >>> wheel_metadata.json['requires_python'] - '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*' - -.. note:: - - 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 `_. - - -.. _version: - -Distribution versions ---------------------- - -The ``version()`` function is the quickest way to get a -:term:`packaging:Distribution Package`'s version -number, as a string:: - - >>> version('wheel') - '0.32.3' - - -.. _files: - -Distribution files ------------------- - -You can also get the full set of files contained within a distribution. The -``files()`` function takes a :term:`packaging:Distribution Package` name -and returns all of the -files installed by this distribution. Each file object returned is a -``PackagePath``, a :class:`pathlib.PurePath` derived object with additional ``dist``, -``size``, and ``hash`` properties as indicated by the metadata. For example:: - - >>> util = [p for p in files('wheel') if 'util.py' in str(p)][0] - >>> util - PackagePath('wheel/util.py') - >>> util.size - 859 - >>> util.dist - - >>> util.hash - - -Once you have the file, you can also read its contents:: - - >>> print(util.read_text()) - import base64 - import sys - ... - def as_bytes(s): - if isinstance(s, text_type): - return s.encode('utf-8') - return s - -You can also use the ``locate`` method to get a the absolute path to the -file:: - - >>> util.locate() # doctest: +SKIP - PosixPath('/home/gustav/example/lib/site-packages/wheel/util.py') - -In the case where the metadata file listing files -(RECORD or SOURCES.txt) is missing, ``files()`` will -return ``None``. The caller may wish to wrap calls to -``files()`` in `always_iterable -`_ -or otherwise guard against this condition if the target -distribution is not known to have the metadata present. - -.. _requirements: - -Distribution requirements -------------------------- - -To get the full set of requirements for a :term:`packaging:Distribution Package`, -use the ``requires()`` -function:: - - >>> requires('wheel') - ["pytest (>=3.0.0) ; extra == 'test'", "pytest-cov ; extra == 'test'"] - - -.. _package-distributions: -.. _import-distribution-package-mapping: - -Mapping import to distribution packages ---------------------------------------- - -A convenience method to resolve the :term:`packaging:Distribution Package` -name (or names, in the case of a namespace package) -that provide each importable top-level -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 -============= - -While the above API is the most common and convenient usage, you can get all -of that information from the ``Distribution`` class. A ``Distribution`` is an -abstract object that represents the metadata for -a Python :term:`packaging:Distribution Package`. You can -get the ``Distribution`` instance:: - - >>> from importlib_metadata import distribution - >>> dist = distribution('wheel') - -Thus, an alternative way to get the version number is through the -``Distribution`` instance:: - - >>> dist.version - '0.32.3' - -There are all kinds of additional metadata available on the ``Distribution`` -instance:: - - >>> dist.metadata['Requires-Python'] - '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*' - >>> dist.metadata['License'] - 'MIT' - -The full set of available metadata is not described here. -See the :ref:`packaging:core-metadata` for additional details. - - -Distribution Discovery -====================== - -By default, this package provides built-in support for discovery of metadata -for file system and zip file :term:`packaging:Distribution Package`\s. -This metadata finder search defaults to ``sys.path``, but varies slightly in how it interprets those values from how other import machinery does. In particular: - -- ``importlib_metadata`` does not honor :class:`bytes` objects on ``sys.path``. -- ``importlib_metadata`` will incidentally honor :py:class:`pathlib.Path` objects on ``sys.path`` even though such values will be ignored for imports. - - -Extending the search algorithm -============================== - -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 :ref:`finders `. To find a distribution package's metadata, -``importlib.metadata`` queries the list of :term:`meta path finders ` on -:data:`sys.meta_path`. - -By default ``importlib_metadata`` installs a finder for distribution packages -found on the file system. -This finder doesn't actually find any *distributions*, -but it can find their metadata. - -The abstract class :py:class:`importlib.abc.MetaPathFinder` defines the -interface expected of finders by Python's import system. -``importlib_metadata`` extends this protocol by looking for an optional -``find_distributions`` callable on the finders from -:data:`sys.meta_path` and presents this extended interface as the -``DistributionFinder`` abstract base class, which defines this abstract -method:: - - @abc.abstractmethod - def find_distributions(context=DistributionFinder.Context()): - """Return an iterable of all Distribution instances capable of - loading the metadata for packages for the indicated ``context``. - """ - -The ``DistributionFinder.Context`` object provides ``.path`` and ``.name`` -properties indicating the path to search and name to match and may -supply other relevant context. - -What this means in practice is that to support finding distribution package -metadata in locations other than the file system, subclass -``Distribution`` and implement the abstract methods. Then from -a custom finder, return instances of this derived ``Distribution`` in the -``find_distributions()`` method. - - -.. _`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 From 3c4fd4e88350a44a82b4a3902e7527ef0d936727 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 15 Dec 2023 11:50:03 -0500 Subject: [PATCH 274/480] Fix typo in doc --- importlib_metadata/_compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/importlib_metadata/_compat.py b/importlib_metadata/_compat.py index c0f15c78..f444c341 100644 --- a/importlib_metadata/_compat.py +++ b/importlib_metadata/_compat.py @@ -41,7 +41,7 @@ def matches(finder): class NullFinder: """ - A "Finder" (aka "MetaClassFinder") that never finds any modules, + A "Finder" (aka "MetaPathFinder") that never finds any modules, but may find distributions. """ From d97e389fd59b766db0cd2c25e3dfb736adc98131 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 16 Dec 2023 11:52:29 -0500 Subject: [PATCH 275/480] Rely on keyword-only argument for context. --- importlib_metadata/__init__.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 312d6966..2cb6a6ad 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -336,6 +336,7 @@ def __repr__(self) -> str: class DeprecatedNonAbstract: + # Required until Python 3.14 def __new__(cls, *args, **kwargs): all_names = { name for subclass in inspect.getmro(cls) for name in vars(subclass) @@ -391,16 +392,18 @@ def from_name(cls, name: str) -> "Distribution": raise PackageNotFoundError(name) @classmethod - def discover(cls, **kwargs) -> Iterable["Distribution"]: + def discover( + cls, *, context: Optional['DistributionFinder.Context'] = None, **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: Iterable of Distribution objects for all packages. + :return: Iterable of Distribution objects for packages matching + the context. """ - context = kwargs.pop('context', None) if context and kwargs: raise ValueError("cannot accept context and kwargs") context = context or DistributionFinder.Context(**kwargs) From 62027870df2893312d18d3713052feb8b4855fdc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 16 Dec 2023 12:19:38 -0500 Subject: [PATCH 276/480] Expand the docstring for read_text --- importlib_metadata/__init__.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 2cb6a6ad..5df979fd 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -356,12 +356,34 @@ def __new__(cls, *args, **kwargs): class Distribution(DeprecatedNonAbstract): - """A Python distribution package.""" + """ + An abstract Python distribution package. + + Custom providers may derive from this class and define + the abstract methods to provide a concrete implementation + for their environment. + """ @abc.abstractmethod def read_text(self, filename) -> Optional[str]: """Attempt to load metadata file given by the name. + Python distribution metadata is organized by blobs of text + typically represented as "files" in the metadata directory + (e.g. package-1.0.dist-info). These files include things + like: + + - METADATA: The distribution metadata including fields + like Name and Version and Description. + - entry_points.txt: A series of entry points defined by + the Setuptools spec in an ini format with sections + representing the groups. + - RECORD: A record of files as installed by a typical + installer. + + A package may provide any set of files, including those + not listed here or none at all. + :param filename: The name of the file in the distribution info. :return: The text if found, otherwise None. """ @@ -413,7 +435,7 @@ def discover( @staticmethod def at(path: StrPath) -> "Distribution": - """Return a Distribution for the indicated metadata path + """Return a Distribution for the indicated metadata path. :param path: a string or path-like object :return: a concrete Distribution instance for the path @@ -422,7 +444,7 @@ def at(path: StrPath) -> "Distribution": @staticmethod def _discover_resolvers(): - """Search the meta_path for resolvers.""" + """Search the meta_path for resolvers (MetadataPathFinders).""" declared = ( getattr(finder, 'find_distributions', None) for finder in sys.meta_path ) From fc4df51a75de636adae1cbf20f6743bdd5bbb8a9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 16 Dec 2023 12:20:26 -0500 Subject: [PATCH 277/480] Rely on read_text and read_bytes from located paths. --- importlib_metadata/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 5df979fd..748f5e99 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -315,12 +315,10 @@ class PackagePath(pathlib.PurePosixPath): 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() + return self.locate().read_text(encoding=encoding) def read_binary(self) -> bytes: - with self.locate().open('rb') as stream: - return stream.read() + return self.locate().read_bytes() def locate(self) -> pathlib.Path: """Return a path-like object for this path""" From 1b3f272d7ce6b25ec5dd68d54d1a140abd159a3f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 16 Dec 2023 12:36:06 -0500 Subject: [PATCH 278/480] Corrected the interface for SimplePath to encompass the expectations of locate_file and PackagePath. --- importlib_metadata/__init__.py | 8 ++++---- importlib_metadata/_meta.py | 10 ++++++++-- newsfragments/+b15724f6.bugfix.rst | 1 + 3 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 newsfragments/+b15724f6.bugfix.rst diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 748f5e99..6108dbc3 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -320,7 +320,7 @@ def read_text(self, encoding: str = 'utf-8') -> str: # type: ignore[override] def read_binary(self) -> bytes: return self.locate().read_bytes() - def locate(self) -> pathlib.Path: + def locate(self) -> SimplePath: """Return a path-like object for this path""" return self.dist.locate_file(self) @@ -387,9 +387,9 @@ def read_text(self, filename) -> Optional[str]: """ @abc.abstractmethod - def locate_file(self, path: StrPath) -> pathlib.Path: + def locate_file(self, path: StrPath) -> SimplePath: """ - Given a path to a file in this distribution, return a path + Given a path to a file in this distribution, return a SimplePath to it. """ @@ -854,7 +854,7 @@ def read_text(self, filename: StrPath) -> Optional[str]: read_text.__doc__ = Distribution.read_text.__doc__ - def locate_file(self, path: StrPath) -> pathlib.Path: + def locate_file(self, path: StrPath) -> SimplePath: return self._path.parent / path @property diff --git a/importlib_metadata/_meta.py b/importlib_metadata/_meta.py index f670016d..be30c15e 100644 --- a/importlib_metadata/_meta.py +++ b/importlib_metadata/_meta.py @@ -46,7 +46,7 @@ def json(self) -> Dict[str, Union[str, List[str]]]: class SimplePath(Protocol[_T]): """ - A minimal subset of pathlib.Path required by PathDistribution. + A minimal subset of pathlib.Path required by Distribution. """ def joinpath(self, other: Union[str, _T]) -> _T: @@ -59,5 +59,11 @@ def __truediv__(self, other: Union[str, _T]) -> _T: def parent(self) -> _T: ... # pragma: no cover - def read_text(self) -> str: + def read_text(self, encoding=None) -> str: + ... # pragma: no cover + + def read_bytes(self) -> bytes: + ... # pragma: no cover + + def exists(self) -> bool: ... # pragma: no cover diff --git a/newsfragments/+b15724f6.bugfix.rst b/newsfragments/+b15724f6.bugfix.rst new file mode 100644 index 00000000..807be3ab --- /dev/null +++ b/newsfragments/+b15724f6.bugfix.rst @@ -0,0 +1 @@ +Corrected the interface for SimplePath to encompass the expectations of locate_file and PackagePath. \ No newline at end of file From ac243d3faa459be256df919aaa65c29d208f25b8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 16 Dec 2023 12:50:14 -0500 Subject: [PATCH 279/480] Include _meta in docs to fix doc build failures. --- docs/api.rst | 5 +++++ docs/conf.py | 2 ++ 2 files changed, 7 insertions(+) diff --git a/docs/api.rst b/docs/api.rst index 02b389ba..d22eecd5 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -9,3 +9,8 @@ API Reference :members: :undoc-members: :show-inheritance: + +.. automodule:: importlib_metadata._meta + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/conf.py b/docs/conf.py index 134d7534..70e5c2dd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -71,4 +71,6 @@ ('py:class', '_T'), # Other workarounds ('py:class', 'importlib_metadata.DeprecatedNonAbstract'), + # Workaround needed for #480 + ('py:obj', 'importlib_metadata._meta._T'), ] From b99c9d6680e549bb232bfe2ee9c6a4bd74874c32 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 16 Dec 2023 13:17:56 -0500 Subject: [PATCH 280/480] Refine SimplePath to allow for os.PathLike on input and SimplePath on output. --- docs/conf.py | 2 -- importlib_metadata/_meta.py | 11 +++++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 70e5c2dd..134d7534 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -71,6 +71,4 @@ ('py:class', '_T'), # Other workarounds ('py:class', 'importlib_metadata.DeprecatedNonAbstract'), - # Workaround needed for #480 - ('py:obj', 'importlib_metadata._meta._T'), ] diff --git a/importlib_metadata/_meta.py b/importlib_metadata/_meta.py index be30c15e..1342d839 100644 --- a/importlib_metadata/_meta.py +++ b/importlib_metadata/_meta.py @@ -1,3 +1,6 @@ +from __future__ import annotations + +import os from typing import Protocol from typing import Any, Dict, Iterator, List, Optional, TypeVar, Union, overload @@ -44,19 +47,19 @@ def json(self) -> Dict[str, Union[str, List[str]]]: """ -class SimplePath(Protocol[_T]): +class SimplePath(Protocol): """ A minimal subset of pathlib.Path required by Distribution. """ - def joinpath(self, other: Union[str, _T]) -> _T: + def joinpath(self, other: Union[str, os.PathLike[str]]) -> SimplePath: ... # pragma: no cover - def __truediv__(self, other: Union[str, _T]) -> _T: + def __truediv__(self, other: Union[str, os.PathLike[str]]) -> SimplePath: ... # pragma: no cover @property - def parent(self) -> _T: + def parent(self) -> SimplePath: ... # pragma: no cover def read_text(self, encoding=None) -> str: From 0c1d32e2656ac7ebc6fcc4377345f9909c9b7fbc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 16 Dec 2023 13:28:04 -0500 Subject: [PATCH 281/480] Inline os.PathLike using future annotations. --- importlib_metadata/__init__.py | 11 ++++++----- importlib_metadata/_compat.py | 10 ---------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 6108dbc3..02c799d1 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import re import abc @@ -21,7 +23,6 @@ from ._collections import FreezableDefaultDict, Pair from ._compat import ( NullFinder, - StrPath, install, ) from ._functools import method_cache, pass_none @@ -387,7 +388,7 @@ def read_text(self, filename) -> Optional[str]: """ @abc.abstractmethod - def locate_file(self, path: StrPath) -> SimplePath: + def locate_file(self, path: os.PathLike[str]) -> SimplePath: """ Given a path to a file in this distribution, return a SimplePath to it. @@ -432,7 +433,7 @@ def discover( ) @staticmethod - def at(path: StrPath) -> "Distribution": + def at(path: os.PathLike[str]) -> "Distribution": """Return a Distribution for the indicated metadata path. :param path: a string or path-like object @@ -840,7 +841,7 @@ def __init__(self, path: SimplePath) -> None: """ self._path = path - def read_text(self, filename: StrPath) -> Optional[str]: + def read_text(self, filename: os.PathLike[str]) -> Optional[str]: with suppress( FileNotFoundError, IsADirectoryError, @@ -854,7 +855,7 @@ def read_text(self, filename: StrPath) -> Optional[str]: read_text.__doc__ = Distribution.read_text.__doc__ - def locate_file(self, path: StrPath) -> SimplePath: + def locate_file(self, path: os.PathLike[str]) -> SimplePath: return self._path.parent / path @property diff --git a/importlib_metadata/_compat.py b/importlib_metadata/_compat.py index f444c341..df312b1c 100644 --- a/importlib_metadata/_compat.py +++ b/importlib_metadata/_compat.py @@ -1,9 +1,6 @@ -import os import sys import platform -from typing import Union - __all__ = ['install', 'NullFinder'] @@ -58,10 +55,3 @@ 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 f38e051467f446fc332e1135fb68bdcec95e6e27 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 21 Dec 2023 15:06:20 -0500 Subject: [PATCH 282/480] Add Python 3.13 to compatibility matrix. Ref python/cpython#113174. --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 426364b1..6788a7e3 100644 --- a/README.rst +++ b/README.rst @@ -43,6 +43,8 @@ were contributed to different versions in the standard library: * - importlib_metadata - stdlib + * - 7.0 + - 3.13 * - 6.5 - 3.12 * - 4.13 From 98196a7fe04d74d7af46e1b052f8124a3fe91dd3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 22 Dec 2023 09:28:26 -0500 Subject: [PATCH 283/480] Fixed type annotations to allow strings. --- importlib_metadata/__init__.py | 8 ++++---- newsfragments/+e563b690.bugfix.rst | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 newsfragments/+e563b690.bugfix.rst diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 02c799d1..0ce535a1 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -388,7 +388,7 @@ def read_text(self, filename) -> Optional[str]: """ @abc.abstractmethod - def locate_file(self, path: os.PathLike[str]) -> SimplePath: + def locate_file(self, path: str | os.PathLike[str]) -> SimplePath: """ Given a path to a file in this distribution, return a SimplePath to it. @@ -433,7 +433,7 @@ def discover( ) @staticmethod - def at(path: os.PathLike[str]) -> "Distribution": + def at(path: str | os.PathLike[str]) -> "Distribution": """Return a Distribution for the indicated metadata path. :param path: a string or path-like object @@ -841,7 +841,7 @@ def __init__(self, path: SimplePath) -> None: """ self._path = path - def read_text(self, filename: os.PathLike[str]) -> Optional[str]: + def read_text(self, filename: str | os.PathLike[str]) -> Optional[str]: with suppress( FileNotFoundError, IsADirectoryError, @@ -855,7 +855,7 @@ def read_text(self, filename: os.PathLike[str]) -> Optional[str]: read_text.__doc__ = Distribution.read_text.__doc__ - def locate_file(self, path: os.PathLike[str]) -> SimplePath: + def locate_file(self, path: str | os.PathLike[str]) -> SimplePath: return self._path.parent / path @property diff --git a/newsfragments/+e563b690.bugfix.rst b/newsfragments/+e563b690.bugfix.rst new file mode 100644 index 00000000..7a1bcc66 --- /dev/null +++ b/newsfragments/+e563b690.bugfix.rst @@ -0,0 +1 @@ +Fixed type annotations to allow strings. \ No newline at end of file From f2e84e3fb5a240b054e8669e5162914bb4a4e68b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 22 Dec 2023 12:11:38 -0500 Subject: [PATCH 284/480] Finalize --- NEWS.rst | 10 ++++++++++ newsfragments/+b15724f6.bugfix.rst | 1 - newsfragments/+e563b690.bugfix.rst | 1 - 3 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 newsfragments/+b15724f6.bugfix.rst delete mode 100644 newsfragments/+e563b690.bugfix.rst diff --git a/NEWS.rst b/NEWS.rst index ae1e9cc8..5f9b2765 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,13 @@ +v7.0.1 +====== + +Bugfixes +-------- + +- Corrected the interface for SimplePath to encompass the expectations of locate_file and PackagePath. +- Fixed type annotations to allow strings. + + v7.0.0 ====== diff --git a/newsfragments/+b15724f6.bugfix.rst b/newsfragments/+b15724f6.bugfix.rst deleted file mode 100644 index 807be3ab..00000000 --- a/newsfragments/+b15724f6.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Corrected the interface for SimplePath to encompass the expectations of locate_file and PackagePath. \ No newline at end of file diff --git a/newsfragments/+e563b690.bugfix.rst b/newsfragments/+e563b690.bugfix.rst deleted file mode 100644 index 7a1bcc66..00000000 --- a/newsfragments/+e563b690.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed type annotations to allow strings. \ No newline at end of file From 596e6834c8a037c935338afe92e0b9c5ffa1768f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 19 Dec 2023 18:29:16 -0500 Subject: [PATCH 285/480] Drop minimum requirement on pytest-mypy as most environments are already running much later. Closes jaraco/skeleton#96. --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 4f184c7e..20c5dd76 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,7 +34,7 @@ testing = # workaround for jaraco/skeleton#22 python_implementation != "PyPy" pytest-cov - pytest-mypy >= 0.9.1; \ + pytest-mypy; \ # workaround for jaraco/skeleton#22 python_implementation != "PyPy" pytest-enabler >= 2.2 From b8c6c1530ef937521b60aabb0ecd98a8b5dca761 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Sat, 23 Dec 2023 00:25:02 +0100 Subject: [PATCH 286/480] Use the ruff formatter (jaraco/skeleton#99) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use the ruff formatter, instead of black Based on: - ruff-pre-commit README.md | Using Ruff with pre-commit https://github.com/astral-sh/ruff-pre-commit/blob/main/README.md - The Ruff Formatter | Conflicting lint rules https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules Support for the ruff formatter was added to pytest-ruff by commits from October 2023, released the same day as versions 0.2 and 0.2.1. Hence, it makes sense to require pytest-ruff ≥ 0.2.1 now. Support for `quote-style = "preserve"` was added to ruff in the last couple of weeks, therefore require the latest version, ruff ≥ 0.1.8. This option is equivalent to `skip-string-normalization` in black. Closes jaraco/skeleton#101. --------- Co-authored-by: Jason R. Coombs --- .pre-commit-config.yaml | 7 ++++--- README.rst | 4 ---- pyproject.toml | 3 --- pytest.ini | 8 -------- ruff.toml | 22 ++++++++++++++++++++++ setup.cfg | 5 +---- 6 files changed, 27 insertions(+), 22 deletions(-) create mode 100644 ruff.toml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index af502010..5a4a7e91 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,6 @@ repos: -- repo: https://github.com/psf/black - rev: 22.6.0 +- repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.8 hooks: - - id: black + - id: ruff + - id: ruff-format diff --git a/README.rst b/README.rst index 41bcfbe8..2fabcf33 100644 --- a/README.rst +++ b/README.rst @@ -11,10 +11,6 @@ :target: https://github.com/astral-sh/ruff :alt: Ruff -.. 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/PROJECT_RTD/badge/?version=latest .. :target: https://PROJECT_RTD.readthedocs.io/en/latest/?badge=latest diff --git a/pyproject.toml b/pyproject.toml index dce944df..a853c578 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,4 @@ requires = ["setuptools>=56", "setuptools_scm[toml]>=3.4.1"] build-backend = "setuptools.build_meta" -[tool.black] -skip-string-normalization = true - [tool.setuptools_scm] diff --git a/pytest.ini b/pytest.ini index f9533b57..022a723e 100644 --- a/pytest.ini +++ b/pytest.ini @@ -7,14 +7,6 @@ filterwarnings= # Ensure ResourceWarnings are emitted default::ResourceWarning - # 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 - - # shopkeep/pytest-black#67 - ignore:'encoding' argument not specified::pytest_black - # realpython/pytest-mypy#152 ignore:'encoding' argument not specified::pytest_mypy diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 00000000..7ed133b7 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,22 @@ +[lint] +extend-ignore = [ + # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules + "W191", + "E111", + "E114", + "E117", + "D206", + "D300", + "Q000", + "Q001", + "Q002", + "Q003", + "COM812", + "COM819", + "ISC001", + "ISC002", +] + +[format] +# https://docs.astral.sh/ruff/settings/#format-quote-style +quote-style = "preserve" diff --git a/setup.cfg b/setup.cfg index 20c5dd76..1d2729be 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,15 +30,12 @@ testing = # upstream pytest >= 6 pytest-checkdocs >= 2.4 - pytest-black >= 0.3.7; \ - # workaround for jaraco/skeleton#22 - python_implementation != "PyPy" pytest-cov pytest-mypy; \ # workaround for jaraco/skeleton#22 python_implementation != "PyPy" pytest-enabler >= 2.2 - pytest-ruff + pytest-ruff >= 0.2.1 # local From a9c5dd5a4eab9f4132d62344cdbad24e077c650e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 24 Dec 2023 12:08:46 -0500 Subject: [PATCH 287/480] Remove sole entry for branches-ignore. Workaround for and closes jaraco/skeleton#103. --- .github/workflows/main.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a079bbfb..cf94f7d8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,7 +4,8 @@ on: merge_group: push: branches-ignore: - - gh-readonly-queue/** # Temporary merge queue-related GH-made branches + # disabled for jaraco/skeleton#103 + # - gh-readonly-queue/** # Temporary merge queue-related GH-made branches pull_request: permissions: From db0d581685d4fc2a16d392d4dedffe622e9a355c Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Tue, 26 Dec 2023 15:58:23 +0100 Subject: [PATCH 288/480] =?UTF-8?q?ruff:=20extended-ignore=20=E2=86=92=20i?= =?UTF-8?q?gnore=20(jaraco/skeleton#105)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Applies Repo-Review suggestion: RF201: Avoid using deprecated config settings extend-ignore deprecated, use ignore instead (identical) --- ruff.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruff.toml b/ruff.toml index 7ed133b7..795cca16 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,5 +1,5 @@ [lint] -extend-ignore = [ +ignore = [ # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules "W191", "E111", From 9ff082440919c8528ce41197c697c432537655f2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 26 Dec 2023 10:37:38 -0500 Subject: [PATCH 289/480] Remove string types now that future annotations are in place. --- importlib_metadata/__init__.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 0ce535a1..f399f1bd 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -170,7 +170,7 @@ class EntryPoint: value: str group: str - dist: Optional['Distribution'] = None + dist: Optional[Distribution] = None def __init__(self, name: str, value: str, group: str) -> None: vars(self).update(name=name, value=value, group=group) @@ -311,9 +311,9 @@ def _from_text(text): class PackagePath(pathlib.PurePosixPath): """A reference to a path in a package""" - hash: Optional["FileHash"] + hash: Optional[FileHash] size: int - dist: "Distribution" + dist: Distribution def read_text(self, encoding: str = 'utf-8') -> str: # type: ignore[override] return self.locate().read_text(encoding=encoding) @@ -395,7 +395,7 @@ def locate_file(self, path: str | os.PathLike[str]) -> SimplePath: """ @classmethod - def from_name(cls, name: str) -> "Distribution": + 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. @@ -414,8 +414,8 @@ def from_name(cls, name: str) -> "Distribution": @classmethod def discover( - cls, *, context: Optional['DistributionFinder.Context'] = None, **kwargs - ) -> Iterable["Distribution"]: + cls, *, context: Optional[DistributionFinder.Context] = None, **kwargs + ) -> Iterable[Distribution]: """Return an iterable of Distribution objects for all packages. Pass a ``context`` or pass keyword arguments for constructing @@ -433,7 +433,7 @@ def discover( ) @staticmethod - def at(path: str | os.PathLike[str]) -> "Distribution": + def at(path: str | os.PathLike[str]) -> Distribution: """Return a Distribution for the indicated metadata path. :param path: a string or path-like object @@ -809,7 +809,7 @@ class MetadataPathFinder(NullFinder, DistributionFinder): def find_distributions( self, context=DistributionFinder.Context() - ) -> Iterable["PathDistribution"]: + ) -> Iterable[PathDistribution]: """ Find distributions. From 86352408cbee5f445b012cda81bab66d82a2f0d4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 26 Dec 2023 11:46:00 -0500 Subject: [PATCH 290/480] Expand docstrings to elaborate on the purpose and details of the interfaces. --- importlib_metadata/__init__.py | 80 ++++++++++++++++++++++++++++------ 1 file changed, 67 insertions(+), 13 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index f399f1bd..bbd92a7d 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -360,7 +360,9 @@ class Distribution(DeprecatedNonAbstract): Custom providers may derive from this class and define the abstract methods to provide a concrete implementation - for their environment. + for their environment. Some providers may opt to override + the default implementation of some properties to bypass + the file-reading mechanism. """ @abc.abstractmethod @@ -374,11 +376,10 @@ def read_text(self, filename) -> Optional[str]: - METADATA: The distribution metadata including fields like Name and Version and Description. - - entry_points.txt: A series of entry points defined by - the Setuptools spec in an ini format with sections - representing the groups. - - RECORD: A record of files as installed by a typical - installer. + - entry_points.txt: A series of entry points as defined in + `this spec `_. + - RECORD: A record of files according to + `this spec `_. A package may provide any set of files, including those not listed here or none at all. @@ -454,7 +455,11 @@ def metadata(self) -> _meta.PackageMetadata: """Return the parsed metadata for this Distribution. The returned object will have keys that name the various bits of - metadata. See PEP 566 for details. + metadata per the + `Core metadata specifications `_. + + Custom providers may provide the METADATA file or override this + property. """ opt_text = ( self.read_text('METADATA') @@ -484,6 +489,12 @@ def version(self) -> str: @property def entry_points(self) -> EntryPoints: + """ + Return EntryPoints for this distribution. + + Custom providers may provide the ``entry_points.txt`` file + or override this property. + """ return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self) @property @@ -496,6 +507,10 @@ def files(self) -> Optional[List[PackagePath]]: (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. + + Custom providers are recommended to provide a "RECORD" file (in + ``read_text``) or override this property to allow for callers to be + able to resolve filenames provided by the package. """ def make_file(name, hash=None, size_str=None): @@ -523,7 +538,7 @@ def skip_missing_files(package_paths): def _read_files_distinfo(self): """ - Read the lines of RECORD + Read the lines of RECORD. """ text = self.read_text('RECORD') return text and text.splitlines() @@ -637,6 +652,9 @@ def _load_json(self, filename): class DistributionFinder(MetaPathFinder): """ A MetaPathFinder capable of discovering installed distributions. + + Custom providers should implement this interface in order to + supply metadata. """ class Context: @@ -684,11 +702,18 @@ def find_distributions(self, context=Context()) -> Iterable[Distribution]: class FastPath: """ - Micro-optimized class for searching a path for - children. + Micro-optimized class for searching a root for children. + + Root is a path on the file system that may contain metadata + directories either as natural directories or within a zip file. >>> FastPath('').children() ['...'] + + FastPath objects are cached and recycled for any given root. + + >>> FastPath('foobar') is FastPath('foobar') + True """ @functools.lru_cache() # type: ignore @@ -730,7 +755,18 @@ def lookup(self, mtime): class Lookup: + """ + A micro-optimized class for searching a (fast) path for metadata. + """ def __init__(self, path: FastPath): + """ + Calculate all of the children representing metadata. + + From the children in the path, calculate early all of the + children that appear to represent metadata (infos) or legacy + metadata (eggs). + """ + base = os.path.basename(path.root).lower() base_is_egg = base.endswith(".egg") self.infos = FreezableDefaultDict(list) @@ -751,7 +787,10 @@ def __init__(self, path: FastPath): self.infos.freeze() self.eggs.freeze() - def search(self, prepared): + def search(self, prepared: Prepared): + """ + Yield all infos and eggs matching the Prepared query. + """ infos = ( self.infos[prepared.normalized] if prepared @@ -767,13 +806,28 @@ def search(self, prepared): class Prepared: """ - A prepared search for metadata on a possibly-named package. + A prepared search query for metadata on a possibly-named package. + + Pre-calculates the normalization to prevent repeated operations. + + >>> none = Prepared(None) + >>> none.normalized + >>> none.legacy_normalized + >>> bool(none) + False + >>> sample = Prepared('Sample__Pkg-name.foo') + >>> sample.normalized + 'sample_pkg_name_foo' + >>> sample.legacy_normalized + 'sample__pkg_name.foo' + >>> bool(sample) + True """ normalized = None legacy_normalized = None - def __init__(self, name): + def __init__(self, name: Optional[str]): self.name = name if name is None: return From f6d9e107365ca270ec843898c05bb8e43dc6987a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 2 Jan 2024 17:56:53 -0500 Subject: [PATCH 291/480] Bump year on badge --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 2fabcf33..efabeee4 100644 --- a/README.rst +++ b/README.rst @@ -14,5 +14,5 @@ .. .. 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 +.. image:: https://img.shields.io/badge/skeleton-2024-informational :target: https://blog.jaraco.com/skeleton From a246813bf5c7e35d03458bc4c04c0c32ae13db77 Mon Sep 17 00:00:00 2001 From: Dan Blanchard Date: Fri, 5 Jan 2024 14:54:36 -0500 Subject: [PATCH 292/480] Add support for egg packages with files outside site-packages --- importlib_metadata/__init__.py | 24 ++++++++++++++++++++---- importlib_metadata/_meta.py | 3 +++ tests/fixtures.py | 34 ++++++++++++++++++++++++++++++++++ tests/test_api.py | 1 + 4 files changed, 58 insertions(+), 4 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index bbd92a7d..42230385 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -562,15 +562,31 @@ def _read_files_egginfo_installed(self): if not text or not subdir: return + site_path = self.locate_file('').resolve() paths = ( - (subdir / name) - .resolve() - .relative_to(self.locate_file('').resolve()) - .as_posix() + self._relative_to( + (subdir / name).resolve(), + site_path, + ).as_posix() for name in text.splitlines() ) return map('"{}"'.format, paths) + def _relative_to(self, path, root): + """ + Workaround for https://bugs.python.org/issue23082 where ".." + isn't added by pathlib.Path.relative_to() when path is not + a subpath of root. + + One example of such a package is dask-labextension, which uses + jupyter-packaging to install JupyterLab javascript files outside + of site-packages. + """ + try: + return path.relative_to(root) + except ValueError: + return pathlib.Path(os.path.relpath(path, root)) + def _read_files_egginfo_sources(self): """ Read SOURCES.txt and return lines in a similar CSV-parsable diff --git a/importlib_metadata/_meta.py b/importlib_metadata/_meta.py index 1342d839..e3892da4 100644 --- a/importlib_metadata/_meta.py +++ b/importlib_metadata/_meta.py @@ -70,3 +70,6 @@ def read_bytes(self) -> bytes: def exists(self) -> bool: ... # pragma: no cover + + def resolve(self) -> bool: + ... # pragma: no cover diff --git a/tests/fixtures.py b/tests/fixtures.py index b419d81c..b194864a 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -252,6 +252,40 @@ def main(): } +class EggInfoPkgPipInstalledExternalDataFiles(OnSysPath, SiteBuilder): + 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 + # (setup.py) that are not present after installation. + "SOURCES.txt": """ + egg_with_module.py + setup.py + egg_with_module.json + 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": """ + ../../../etc/jupyter/jupyter_notebook_config.d/egg_with_module.json + ../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") + """, + } + + + class EggInfoPkgPipInstalledNoModules(OnSysPath, SiteBuilder): files: FilesSpec = { "egg_with_no_modules_pkg.egg-info": { diff --git a/tests/test_api.py b/tests/test_api.py index a85c62ad..1280c532 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -29,6 +29,7 @@ class APITests( fixtures.EggInfoPkg, fixtures.EggInfoPkgPipInstalledNoToplevel, fixtures.EggInfoPkgPipInstalledNoModules, + fixtures.EggInfoPkgPipInstalledExternalDataFiles, fixtures.EggInfoPkgSourcesFallback, fixtures.DistInfoPkg, fixtures.DistInfoPkgWithDot, From a6038c3f2bddad2a6113deb5358e911071ec71d1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Jan 2024 11:40:22 -0500 Subject: [PATCH 293/480] Add an example of how a custom provider might leverage the DistributionFinder.Context. --- importlib_metadata/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index bbd92a7d..09a02a4e 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -667,6 +667,17 @@ class Context: Each DistributionFinder may expect any parameters and should attempt to honor the canonical parameters defined below when appropriate. + + This mechanism gives a custom provider a means to + solicit additional details from the caller beyond + "name" and "path" when searching distributions. + For example, imagine a provider that exposes suites + of packages in either a "public" or "private" ``realm``. + A caller may wish to query only for distributions in + a particular realm and could call + ``distributions(realm="private")`` to signal to the + custom provider to only include distributions from that + realm. """ name = None From dbcb0747110d074112f27e2699856acfc4ba8ea3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 6 Jan 2024 20:09:59 -0500 Subject: [PATCH 294/480] Remove build and dist from excludes. It appears they are not needed and their presence blocks the names of packages like 'builder' and 'distutils'. Ref pypa/distutils#224. --- setup.cfg | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 1d2729be..c2e82875 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,8 +20,6 @@ install_requires = [options.packages.find] exclude = - build* - dist* docs* tests* From d27890573088a6a0292139c5e30466debd7dc1dd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 7 Jan 2024 12:26:16 -0500 Subject: [PATCH 295/480] Exclude docs and tests directories properly per Setuptools behavior. --- setup.cfg | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index c2e82875..c5aa1af9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,8 +20,11 @@ install_requires = [options.packages.find] exclude = - docs* - tests* + # duplicate exclusions for pypa/setuptools#2688 + docs + docs.* + tests + tests.* [options.extras_require] testing = From 63535c6efd3516a7ef35c862c24ef5b6d43c8494 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 7 Jan 2024 12:49:05 -0500 Subject: [PATCH 296/480] Rely on default discovery for good heuristics for finding packages. --- setup.cfg | 9 --------- 1 file changed, 9 deletions(-) diff --git a/setup.cfg b/setup.cfg index c5aa1af9..fe99eaf6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,19 +13,10 @@ classifiers = Programming Language :: Python :: 3 :: Only [options] -packages = find_namespace: include_package_data = true python_requires = >=3.8 install_requires = -[options.packages.find] -exclude = - # duplicate exclusions for pypa/setuptools#2688 - docs - docs.* - tests - tests.* - [options.extras_require] testing = # upstream From d3a1333ce57379af65b75a13b7a4981e1d4d0c42 Mon Sep 17 00:00:00 2001 From: Dan Blanchard Date: Wed, 10 Jan 2024 10:35:13 -0500 Subject: [PATCH 297/480] Tweak test --- tests/fixtures.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/fixtures.py b/tests/fixtures.py index b194864a..263d5ffe 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -270,7 +270,8 @@ class EggInfoPkgPipInstalledExternalDataFiles(OnSysPath, SiteBuilder): # accurate source than SOURCES.txt as to the installed contents of # the package. "installed-files.txt": """ - ../../../etc/jupyter/jupyter_notebook_config.d/egg_with_module.json + ../../../etc/jupyter/jupyter_notebook_config.d/relative.json + /etc/jupyter/jupyter_notebook_config.d/absolute.json ../egg_with_module.py PKG-INFO SOURCES.txt From 68af09493b1d7ef829eb3c0813542c5e21e63ec4 Mon Sep 17 00:00:00 2001 From: Danyal-Faheem Date: Wed, 17 Jan 2024 20:02:21 +0500 Subject: [PATCH 298/480] refactor: add return type for select method --- 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 09a02a4e..a3aae725 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -275,7 +275,7 @@ def __repr__(self): """ return '%s(%r)' % (self.__class__.__name__, tuple(self)) - def select(self, **params): + def select(self, **params) -> EntryPoints: """ Select entry points from self that match the given parameters (typically group and/or name). From 72b30eca10d73b0f793aac5239e11710077c26cf Mon Sep 17 00:00:00 2001 From: Danyal-Faheem Date: Tue, 30 Jan 2024 15:30:36 +0500 Subject: [PATCH 299/480] refactor: add return type for load method --- 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 a3aae725..d196ab79 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -33,7 +33,7 @@ from importlib import import_module from importlib.abc import MetaPathFinder from itertools import starmap -from typing import Iterable, List, Mapping, Optional, Set, cast +from typing import Iterable, List, Mapping, Optional, Set, cast, Any __all__ = [ 'Distribution', @@ -175,7 +175,7 @@ class EntryPoint: def __init__(self, name: str, value: str, group: str) -> None: vars(self).update(name=name, value=value, group=group) - def load(self): + def load(self) -> Any: """Load the entry point from its definition. If only a module is indicated by the value, return that module. Otherwise, return the named object. From 29e5d34af962e59e92c501ebb988dcaf192b114e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 4 Feb 2024 10:15:04 -0500 Subject: [PATCH 300/480] Enable preview to enable preserving quotes. --- ruff.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ruff.toml b/ruff.toml index 795cca16..e61ca8b0 100644 --- a/ruff.toml +++ b/ruff.toml @@ -18,5 +18,7 @@ ignore = [ ] [format] +# Enable preview, required for quote-style = "preserve" +preview = true # https://docs.astral.sh/ruff/settings/#format-quote-style quote-style = "preserve" From 2a402a39f154d9a6cf4621e8c5d22bace749b55e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20S=C5=82awecki?= Date: Tue, 6 Feb 2024 23:01:32 +0100 Subject: [PATCH 301/480] Tweak coverage configuration for type checking (jaraco/skeleton#97) * Tweak coverage configuration for type checking * Use `exclude_also` instead of `exclude_lines` Co-authored-by: Sviatoslav Sydorenko * Add reference to the issue. --------- Co-authored-by: Sviatoslav Sydorenko Co-authored-by: Jason R. Coombs --- .coveragerc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.coveragerc b/.coveragerc index 02879483..35b98b1d 100644 --- a/.coveragerc +++ b/.coveragerc @@ -7,3 +7,7 @@ disable_warnings = [report] show_missing = True +exclude_also = + # jaraco/skeleton#97 + @overload + if TYPE_CHECKING: From 68ac292eb37ce92e992e6fab05a44ad86f32e8f1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 6 Feb 2024 16:53:46 -0500 Subject: [PATCH 302/480] Use latest versions in RTD boilerplate. --- .readthedocs.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 053c7287..68489063 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -7,6 +7,6 @@ python: # required boilerplate readthedocs/readthedocs.org#10401 build: - os: ubuntu-22.04 + os: ubuntu-lts-latest tools: - python: "3" + python: latest From 178d254379ed260eb537f48722703f819eaa8235 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 12 Feb 2024 16:02:29 -0500 Subject: [PATCH 303/480] Remove Sphinx pin. Ref sphinx-doc/sphinx#11662. --- setup.cfg | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index fe99eaf6..400a72a5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -34,8 +34,6 @@ testing = docs = # upstream sphinx >= 3.5 - # workaround for sphinx/sphinx-doc#11662 - sphinx < 7.2.5 jaraco.packaging >= 9.3 rst.linker >= 1.9 furo From 779219ce3ecbf4477da062658a1d0b2d5bf4f77f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 18 Feb 2024 10:38:06 -0500 Subject: [PATCH 304/480] Include deps from the base config in diffcov. --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 331eeed9..4c39a5b1 100644 --- a/tox.ini +++ b/tox.ini @@ -12,6 +12,7 @@ extras = [testenv:diffcov] description = run tests and check that diff from main is covered deps = + {[testenv]deps} diff-cover commands = pytest {posargs} --cov-report xml From d1c5444126aeacefee3949b30136446ab99979d8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 10:33:21 -0500 Subject: [PATCH 305/480] Enable complexity check and pycodestyle warnings. Closes jaraco/skeleton#110. --- ruff.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ruff.toml b/ruff.toml index e61ca8b0..6c5b0009 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,4 +1,8 @@ [lint] +select = [ + "C901", + "W", +] ignore = [ # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules "W191", From b434f69238b4ee517ae20978afa19f3cd1ed8f1f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 2 Mar 2024 14:05:46 -0500 Subject: [PATCH 306/480] Use 'extend-select' to avoid disabling the default config. Ref jaraco/skeleton#110. --- ruff.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ruff.toml b/ruff.toml index 6c5b0009..70612985 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,5 +1,5 @@ [lint] -select = [ +extend-select = [ "C901", "W", ] From 9f2d682d24df57c047651deba4a5c37eac029c02 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 6 Mar 2024 10:09:09 -0500 Subject: [PATCH 307/480] =?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 + importlib_metadata/_meta.py | 47 ++++++++++++++----------------- importlib_metadata/_py39compat.py | 1 + tests/_path.py | 15 ++++------ tests/fixtures.py | 16 +++++------ tests/test_main.py | 32 +++++++++------------ 6 files changed, 49 insertions(+), 63 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 09a02a4e..1e5be7a2 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -769,6 +769,7 @@ class Lookup: """ A micro-optimized class for searching a (fast) path for metadata. """ + def __init__(self, path: FastPath): """ Calculate all of the children representing metadata. diff --git a/importlib_metadata/_meta.py b/importlib_metadata/_meta.py index 1342d839..1927d0f6 100644 --- a/importlib_metadata/_meta.py +++ b/importlib_metadata/_meta.py @@ -9,30 +9,27 @@ class PackageMetadata(Protocol): - def __len__(self) -> int: - ... # pragma: no cover + def __len__(self) -> int: ... # pragma: no cover - def __contains__(self, item: str) -> bool: - ... # pragma: no cover + def __contains__(self, item: str) -> bool: ... # pragma: no cover - def __getitem__(self, key: str) -> str: - ... # pragma: no cover + def __getitem__(self, key: str) -> str: ... # pragma: no cover - def __iter__(self) -> Iterator[str]: - ... # pragma: no cover + def __iter__(self) -> Iterator[str]: ... # pragma: no cover @overload - def get(self, name: str, failobj: None = None) -> Optional[str]: - ... # pragma: no cover + 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 + 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]]: - ... # pragma: no cover + 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]: @@ -52,21 +49,19 @@ class SimplePath(Protocol): A minimal subset of pathlib.Path required by Distribution. """ - def joinpath(self, other: Union[str, os.PathLike[str]]) -> SimplePath: - ... # pragma: no cover + def joinpath( + self, other: Union[str, os.PathLike[str]] + ) -> SimplePath: ... # pragma: no cover - def __truediv__(self, other: Union[str, os.PathLike[str]]) -> SimplePath: - ... # pragma: no cover + def __truediv__( + self, other: Union[str, os.PathLike[str]] + ) -> SimplePath: ... # pragma: no cover @property - def parent(self) -> SimplePath: - ... # pragma: no cover + def parent(self) -> SimplePath: ... # pragma: no cover - def read_text(self, encoding=None) -> str: - ... # pragma: no cover + def read_text(self, encoding=None) -> str: ... # pragma: no cover - def read_bytes(self) -> bytes: - ... # pragma: no cover + def read_bytes(self) -> bytes: ... # pragma: no cover - def exists(self) -> bool: - ... # pragma: no cover + def exists(self) -> bool: ... # pragma: no cover diff --git a/importlib_metadata/_py39compat.py b/importlib_metadata/_py39compat.py index cde4558f..fc6b8221 100644 --- a/importlib_metadata/_py39compat.py +++ b/importlib_metadata/_py39compat.py @@ -1,6 +1,7 @@ """ Compatibility layer with Python 3.8/3.9 """ + from typing import TYPE_CHECKING, Any, Optional if TYPE_CHECKING: # pragma: no cover diff --git a/tests/_path.py b/tests/_path.py index 25c799fa..b3cfb9cd 100644 --- a/tests/_path.py +++ b/tests/_path.py @@ -17,20 +17,15 @@ class Symlink(str): @runtime_checkable class TreeMaker(Protocol): - def __truediv__(self, *args, **kwargs): - ... # pragma: no cover + def __truediv__(self, *args, **kwargs): ... # pragma: no cover - def mkdir(self, **kwargs): - ... # pragma: no cover + def mkdir(self, **kwargs): ... # pragma: no cover - def write_text(self, content, **kwargs): - ... # pragma: no cover + def write_text(self, content, **kwargs): ... # pragma: no cover - def write_bytes(self, content): - ... # pragma: no cover + def write_bytes(self, content): ... # pragma: no cover - def symlink_to(self, target): - ... # pragma: no cover + def symlink_to(self, target): ... # pragma: no cover def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker: diff --git a/tests/fixtures.py b/tests/fixtures.py index b419d81c..7daae16a 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -140,15 +140,13 @@ class DistInfoPkgEditable(DistInfoPkg): some_hash = '524127ce937f7cb65665130c695abd18ca386f60bb29687efb976faa1596fdcc' files: FilesSpec = { 'distinfo_pkg-1.0.0.dist-info': { - 'direct_url.json': json.dumps( - { - "archive_info": { - "hash": f"sha256={some_hash}", - "hashes": {"sha256": f"{some_hash}"}, - }, - "url": "file:///path/to/distinfo_pkg-1.0.0.editable-py3-none-any.whl", - } - ) + 'direct_url.json': json.dumps({ + "archive_info": { + "hash": f"sha256={some_hash}", + "hashes": {"sha256": f"{some_hash}"}, + }, + "url": "file:///path/to/distinfo_pkg-1.0.0.editable-py3-none-any.whl", + }) }, } diff --git a/tests/test_main.py b/tests/test_main.py index dd6b51fa..6e0b1da7 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -304,12 +304,10 @@ def test_sortable(self): """ EntryPoint objects are sortable, but result is undefined. """ - sorted( - [ - EntryPoint(name='b', value='val', group='group'), - EntryPoint(name='a', value='val', group='group'), - ] - ) + sorted([ + EntryPoint(name='b', value='val', group='group'), + EntryPoint(name='a', value='val', group='group'), + ]) class FileSystem( @@ -376,18 +374,16 @@ def test_packages_distributions_all_module_types(self): 'all_distributions-1.0.0.dist-info': metadata, } for i, suffix in enumerate(suffixes): - files.update( - { - f'importable-name {i}{suffix}': '', - f'in_namespace_{i}': { - f'mod{suffix}': '', - }, - f'in_package_{i}': { - '__init__.py': '', - f'mod{suffix}': '', - }, - } - ) + files.update({ + f'importable-name {i}{suffix}': '', + f'in_namespace_{i}': { + f'mod{suffix}': '', + }, + f'in_package_{i}': { + '__init__.py': '', + f'mod{suffix}': '', + }, + }) metadata.update(RECORD=fixtures.build_record(files)) fixtures.build_files(files, prefix=self.site_dir) From 79521e175bc3ababd40f31ca3e3dfdab001118d5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 6 Mar 2024 10:16:39 -0500 Subject: [PATCH 308/480] Moved example sources to the tests/data directory, avoiding build issues. --- conftest.py | 2 +- setup.cfg | 4 ---- {prepare => tests/data/sources}/example/example/__init__.py | 0 {prepare => tests/data/sources}/example/setup.py | 0 {prepare => tests/data/sources}/example2/example2/__init__.py | 0 {prepare => tests/data/sources}/example2/pyproject.toml | 0 6 files changed, 1 insertion(+), 5 deletions(-) rename {prepare => tests/data/sources}/example/example/__init__.py (100%) rename {prepare => tests/data/sources}/example/setup.py (100%) rename {prepare => tests/data/sources}/example2/example2/__init__.py (100%) rename {prepare => tests/data/sources}/example2/pyproject.toml (100%) diff --git a/conftest.py b/conftest.py index ab6c8cae..779ac24b 100644 --- a/conftest.py +++ b/conftest.py @@ -3,7 +3,7 @@ collect_ignore = [ # this module fails mypy tests because 'setup.py' matches './setup.py' - 'prepare/example/setup.py', + 'tests/data/sources/example/setup.py', ] diff --git a/setup.cfg b/setup.cfg index 27eabc57..3ea2ca08 100644 --- a/setup.cfg +++ b/setup.cfg @@ -19,10 +19,6 @@ install_requires = zipp>=0.5 typing-extensions>=3.6.4; python_version < "3.8" -[options.packages.find] -exclude = - prepare* - [options.extras_require] testing = # upstream diff --git a/prepare/example/example/__init__.py b/tests/data/sources/example/example/__init__.py similarity index 100% rename from prepare/example/example/__init__.py rename to tests/data/sources/example/example/__init__.py diff --git a/prepare/example/setup.py b/tests/data/sources/example/setup.py similarity index 100% rename from prepare/example/setup.py rename to tests/data/sources/example/setup.py diff --git a/prepare/example2/example2/__init__.py b/tests/data/sources/example2/example2/__init__.py similarity index 100% rename from prepare/example2/example2/__init__.py rename to tests/data/sources/example2/example2/__init__.py diff --git a/prepare/example2/pyproject.toml b/tests/data/sources/example2/pyproject.toml similarity index 100% rename from prepare/example2/pyproject.toml rename to tests/data/sources/example2/pyproject.toml From 55a55ea046dddc4e714777031952769b52eaa929 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 6 Mar 2024 20:51:51 -0500 Subject: [PATCH 309/480] Fix docs error due to duplicate explicit target name. --- 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 1e5be7a2..6704f2b6 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -377,9 +377,9 @@ def read_text(self, filename) -> Optional[str]: - METADATA: The distribution metadata including fields like Name and Version and Description. - entry_points.txt: A series of entry points as defined in - `this spec `_. + `the entry points spec `_. - RECORD: A record of files according to - `this spec `_. + `this recording spec `_. A package may provide any set of files, including those not listed here or none at all. From fd3a0abc127bbd8986958e5baa54f9304daf2fa4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 6 Mar 2024 21:50:33 -0500 Subject: [PATCH 310/480] Re-order imports for consistency. --- 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 d196ab79..3f14faa2 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -33,7 +33,7 @@ from importlib import import_module from importlib.abc import MetaPathFinder from itertools import starmap -from typing import Iterable, List, Mapping, Optional, Set, cast, Any +from typing import Any, Iterable, List, Mapping, Optional, Set, cast __all__ = [ 'Distribution', From 9d4908e77692d915872b24c040d3a1c89ebba1be Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 6 Mar 2024 21:56:41 -0500 Subject: [PATCH 311/480] =?UTF-8?q?=F0=9F=A7=8E=E2=80=8D=E2=99=80=EF=B8=8F?= =?UTF-8?q?=20Genuflect=20to=20the=20types.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- importlib_metadata/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 3f14faa2..5593f74e 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -33,7 +33,7 @@ from importlib import import_module from importlib.abc import MetaPathFinder from itertools import starmap -from typing import Any, Iterable, List, Mapping, Optional, Set, cast +from typing import Any, Iterable, List, Mapping, Match, Optional, Set, cast __all__ = [ 'Distribution', @@ -180,7 +180,7 @@ def load(self) -> Any: is indicated by the value, return that module. Otherwise, return the named object. """ - match = self.pattern.match(self.value) + match = cast(Match, self.pattern.match(self.value)) module = import_module(match.group('module')) attrs = filter(None, (match.group('attr') or '').split('.')) return functools.reduce(getattr, attrs, module) @@ -769,6 +769,7 @@ class Lookup: """ A micro-optimized class for searching a (fast) path for metadata. """ + def __init__(self, path: FastPath): """ Calculate all of the children representing metadata. From 913352a8765662f1569123b27ecb9ca7bc4d649e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 7 Mar 2024 08:09:53 -0500 Subject: [PATCH 312/480] Finalize --- NEWS.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/NEWS.rst b/NEWS.rst index 5f9b2765..085a5305 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,9 @@ +v7.0.2 +====== + +No significant changes. + + v7.0.1 ====== From 608b319e33e170f746b32061de5b8862e0a943f7 Mon Sep 17 00:00:00 2001 From: Dan Blanchard Date: Thu, 7 Mar 2024 09:28:51 -0500 Subject: [PATCH 313/480] Update importlib_metadata/__init__.py Co-authored-by: Jason R. Coombs --- 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 4bd9093c..dc23148f 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -574,7 +574,7 @@ def _read_files_egginfo_installed(self): def _relative_to(self, path, root): """ - Workaround for https://bugs.python.org/issue23082 where ".." + Workaround for https://github.com/python/cpython/issues/67271 where ".." isn't added by pathlib.Path.relative_to() when path is not a subpath of root. From 4a3782d11895c22fd443c67175ab7352db2a769b Mon Sep 17 00:00:00 2001 From: Dan Blanchard Date: Thu, 7 Mar 2024 10:22:30 -0500 Subject: [PATCH 314/480] Fix mypy failure that has nothing to do with this PR --- 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 dc23148f..61325221 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -440,7 +440,7 @@ def at(path: str | os.PathLike[str]) -> Distribution: :param path: a string or path-like object :return: a concrete Distribution instance for the path """ - return PathDistribution(pathlib.Path(path)) + return PathDistribution(cast(SimplePath, pathlib.Path(path))) @staticmethod def _discover_resolvers(): From b4ce0ff90ec730866885a08134ece217b69949d2 Mon Sep 17 00:00:00 2001 From: Shantanu <12621235+hauntsaninja@users.noreply.github.com> Date: Mon, 29 Jan 2024 01:30:22 -0800 Subject: [PATCH 315/480] gh-109653: Improve import time of importlib.metadata / email.utils (python/cpython#114664) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit My criterion for delayed imports is that they're only worth it if the majority of users of the module would benefit from it, otherwise you're just moving latency around unpredictably. mktime_tz is not used anywhere in the standard library and grep.app indicates it's not got much use in the ecosystem either. Distribution.files is not nearly as widely used as other importlib.metadata APIs, so we defer the csv import. Before: ``` λ hyperfine -w 8 './python -c "import importlib.metadata"' Benchmark 1: ./python -c "import importlib.metadata" Time (mean ± σ): 65.1 ms ± 0.5 ms [User: 55.3 ms, System: 9.8 ms] Range (min … max): 64.4 ms … 66.4 ms 44 runs ``` After: ``` λ hyperfine -w 8 './python -c "import importlib.metadata"' Benchmark 1: ./python -c "import importlib.metadata" Time (mean ± σ): 62.0 ms ± 0.3 ms [User: 52.5 ms, System: 9.6 ms] Range (min … max): 61.3 ms … 62.8 ms 46 runs ``` for about a 3ms saving with warm disk cache, maybe 7-11ms with cold disk cache. --- importlib_metadata/__init__.py | 5 ++++- newsfragments/+.feature.rst | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 newsfragments/+.feature.rst diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index c68d8ad8..5cd15ab5 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -3,7 +3,6 @@ import os import re import abc -import csv import sys import json import zipp @@ -522,6 +521,10 @@ def make_file(name, hash=None, size_str=None): @pass_none def make_files(lines): + # Delay csv import, since Distribution.files is not as widely used + # as other parts of importlib.metadata + import csv + return starmap(make_file, csv.reader(lines)) @pass_none diff --git a/newsfragments/+.feature.rst b/newsfragments/+.feature.rst new file mode 100644 index 00000000..865acfc1 --- /dev/null +++ b/newsfragments/+.feature.rst @@ -0,0 +1 @@ +Improve import time (python/cpython#114664). From fb7465c6f81847574b736e6dfa1bb93187e025cc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 14 Mar 2024 17:59:00 -0400 Subject: [PATCH 316/480] gh-116811: Ensure MetadataPathFinder.invalidate_caches is reachable when delegated through PathFinder. (python/cpython#116812) * Make MetadataPathFinder a proper classmethod. --- importlib_metadata/__init__.py | 1 + newsfragments/+.bugfix.rst | 1 + 2 files changed, 2 insertions(+) create mode 100644 newsfragments/+.bugfix.rst diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 5cd15ab5..86e68a23 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -898,6 +898,7 @@ def _search_paths(cls, name, paths): path.search(prepared) for path in map(FastPath, paths) ) + @classmethod def invalidate_caches(cls) -> None: FastPath.__new__.cache_clear() diff --git a/newsfragments/+.bugfix.rst b/newsfragments/+.bugfix.rst new file mode 100644 index 00000000..a8dd3dde --- /dev/null +++ b/newsfragments/+.bugfix.rst @@ -0,0 +1 @@ +Allow ``MetadataPathFinder.invalidate_caches`` to be called as a classmethod. From 9a878d6b263b7e9dedc10fc0df1a7323e9ee7b99 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Tue, 16 Jan 2024 16:10:03 +0100 Subject: [PATCH 317/480] gh-114107: Fix symlink test if symlinks aren't supported (python/cpython#114108) --- tests/test_main.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_main.py b/tests/test_main.py index 6e0b1da7..45d74534 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -5,6 +5,8 @@ import importlib import importlib_metadata import contextlib +from test.support import os_helper + import pyfakefs.fake_filesystem_unittest as ffs from . import fixtures @@ -396,6 +398,7 @@ def test_packages_distributions_all_module_types(self): assert not any(name.endswith('.dist-info') for name in distributions) + @os_helper.skip_unless_symlink def test_packages_distributions_symlinked_top_level(self) -> None: """ Distribution is resolvable from a simple top-level symlink in RECORD. From f16e114956ea2970d4b391ca031dfe7f2d65d46e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 20 Mar 2024 04:53:22 -0400 Subject: [PATCH 318/480] Add support for python/cpython references --- docs/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 134d7534..90a1da2a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,7 +25,7 @@ url='https://peps.python.org/pep-{pep_number:0>4}/', ), dict( - pattern=r'(Python #|py-)(?P\d+)', + pattern=r'(python/cpython#|Python #|py-)(?P\d+)', url='https://github.com/python/cpython/issues/{python}', ), ], From 3531507c7a275cfcbdefe02e0aed4f92ff589b09 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 20 Mar 2024 05:11:25 -0400 Subject: [PATCH 319/480] Fix test failures on older Pythons with os_helper shim. Copied 'from_test_support' from importlib_resources. --- setup.cfg | 1 + tests/fixtures.py | 6 ++++-- tests/py39compat.py | 18 ++++++++++++++++-- tests/test_main.py | 2 +- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/setup.cfg b/setup.cfg index 71b66b39..f67df574 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,6 +37,7 @@ testing = pyfakefs flufl.flake8 pytest-perf >= 0.9.2 + jaraco.collections docs = # upstream diff --git a/tests/fixtures.py b/tests/fixtures.py index 7daae16a..88478050 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -9,7 +9,7 @@ import functools import contextlib -from .py39compat import FS_NONASCII +from .py39compat import os_helper from . import _path from ._path import FilesSpec @@ -335,7 +335,9 @@ def record_names(file_defs): class FileBuilder: def unicode_filename(self): - return FS_NONASCII or self.skip("File system does not support non-ascii.") + return os_helper.FS_NONASCII or self.skip( + "File system does not support non-ascii." + ) def DALS(str): diff --git a/tests/py39compat.py b/tests/py39compat.py index 926dcad9..b01ecad8 100644 --- a/tests/py39compat.py +++ b/tests/py39compat.py @@ -1,4 +1,18 @@ +import types + +from jaraco.collections import Projection + + +def from_test_support(*names): + """ + Return a SimpleNamespace of names from test.support. + """ + import test.support + + return types.SimpleNamespace(**Projection(names, vars(test.support))) + + try: - from test.support.os_helper import FS_NONASCII + from test.support import os_helper # type: ignore except ImportError: - from test.support import FS_NONASCII # noqa + os_helper = from_test_support('FS_NONASCII', 'skip_unless_symlink') diff --git a/tests/test_main.py b/tests/test_main.py index 45d74534..c94ab0f4 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -5,7 +5,7 @@ import importlib import importlib_metadata import contextlib -from test.support import os_helper +from .py39compat import os_helper import pyfakefs.fake_filesystem_unittest as ffs From 856541b5f26c099ef7629b95533ca285fe6c102c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 20 Mar 2024 05:12:18 -0400 Subject: [PATCH 320/480] Moved compatibility module to compat package. --- importlib_metadata/__init__.py | 7 ++++--- importlib_metadata/compat/__init__.py | 0 importlib_metadata/{_py39compat.py => compat/py39.py} | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) create mode 100644 importlib_metadata/compat/__init__.py rename importlib_metadata/{_py39compat.py => compat/py39.py} (82%) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 86e68a23..c85ffa76 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -18,7 +18,8 @@ import posixpath import collections -from . import _adapters, _meta, _py39compat +from . import _adapters, _meta +from .compat import py39 from ._collections import FreezableDefaultDict, Pair from ._compat import ( NullFinder, @@ -279,7 +280,7 @@ def select(self, **params) -> EntryPoints: Select entry points from self that match the given parameters (typically group and/or name). """ - return EntryPoints(ep for ep in self if _py39compat.ep_matches(ep, **params)) + return EntryPoints(ep for ep in self if py39.ep_matches(ep, **params)) @property def names(self) -> Set[str]: @@ -996,7 +997,7 @@ def version(distribution_name: str) -> str: _unique = functools.partial( unique_everseen, - key=_py39compat.normalized_name, + key=py39.normalized_name, ) """ Wrapper for ``distributions`` to return unique distributions by name. diff --git a/importlib_metadata/compat/__init__.py b/importlib_metadata/compat/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/importlib_metadata/_py39compat.py b/importlib_metadata/compat/py39.py similarity index 82% rename from importlib_metadata/_py39compat.py rename to importlib_metadata/compat/py39.py index fc6b8221..1f15bd97 100644 --- a/importlib_metadata/_py39compat.py +++ b/importlib_metadata/compat/py39.py @@ -6,7 +6,7 @@ if TYPE_CHECKING: # pragma: no cover # Prevent circular imports on runtime. - from . import Distribution, EntryPoint + from .. import Distribution, EntryPoint else: Distribution = EntryPoint = Any @@ -18,7 +18,7 @@ def normalized_name(dist: Distribution) -> Optional[str]: try: return dist._normalized_name except AttributeError: - from . import Prepared # -> delay to prevent circular imports. + from .. import Prepared # -> delay to prevent circular imports. return Prepared.normalize(getattr(dist, "name", None) or dist.metadata['Name']) @@ -30,7 +30,7 @@ def ep_matches(ep: EntryPoint, **params) -> bool: try: return ep.matches(**params) except AttributeError: - from . import EntryPoint # -> delay to prevent circular imports. + from .. import EntryPoint # -> delay to prevent circular imports. # Reconstruct the EntryPoint object to make sure it is compatible. return EntryPoint(ep.name, ep.value, ep.group).matches(**params) From ffa719bbd8876b43964b704af34b6bce50ac7271 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 20 Mar 2024 05:14:31 -0400 Subject: [PATCH 321/480] Moved compatibility module to compat package. --- tests/compat/__init__.py | 0 tests/{py39compat.py => compat/py39.py} | 0 tests/fixtures.py | 2 +- tests/test_main.py | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 tests/compat/__init__.py rename tests/{py39compat.py => compat/py39.py} (100%) diff --git a/tests/compat/__init__.py b/tests/compat/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/py39compat.py b/tests/compat/py39.py similarity index 100% rename from tests/py39compat.py rename to tests/compat/py39.py diff --git a/tests/fixtures.py b/tests/fixtures.py index 88478050..07f10963 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -9,7 +9,7 @@ import functools import contextlib -from .py39compat import os_helper +from .compat.py39 import os_helper from . import _path from ._path import FilesSpec diff --git a/tests/test_main.py b/tests/test_main.py index c94ab0f4..af79e698 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -5,7 +5,7 @@ import importlib import importlib_metadata import contextlib -from .py39compat import os_helper +from .compat.py39 import os_helper import pyfakefs.fake_filesystem_unittest as ffs From 5950f43b8f44a1b700342ffd4633c147309b1c7c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 20 Mar 2024 05:17:34 -0400 Subject: [PATCH 322/480] Remove legacy logic for Python 3.7. --- tests/test_py39compat.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_py39compat.py b/tests/test_py39compat.py index 7e6235e4..34745a4b 100644 --- a/tests/test_py39compat.py +++ b/tests/test_py39compat.py @@ -14,8 +14,7 @@ class OldStdlibFinderTests(fixtures.DistInfoPkgOffPath, unittest.TestCase): def setUp(self): - python_version = sys.version_info[:2] - if python_version < (3, 8) or python_version > (3, 9): + if sys.version_info >= (3, 10): self.skipTest("Tests specific for Python 3.8/3.9") super().setUp() From 41ca0390dbb52543104f12c0629f0bbb882e48ea Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 20 Mar 2024 05:22:24 -0400 Subject: [PATCH 323/480] Moved compatibility tests to the compat package, as they're not included in CPython. --- tests/{test_py39compat.py => compat/test_py39_compat.py} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/{test_py39compat.py => compat/test_py39_compat.py} (99%) diff --git a/tests/test_py39compat.py b/tests/compat/test_py39_compat.py similarity index 99% rename from tests/test_py39compat.py rename to tests/compat/test_py39_compat.py index 34745a4b..549e518a 100644 --- a/tests/test_py39compat.py +++ b/tests/compat/test_py39_compat.py @@ -2,7 +2,7 @@ import pathlib import unittest -from . import fixtures +from .. import fixtures from importlib_metadata import ( distribution, distributions, From e30a16d471f62555db5605d5652bede9a1234b1a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 20 Mar 2024 06:20:46 -0400 Subject: [PATCH 324/480] Consolidated test support logic in jaraco.test.cpython. --- setup.cfg | 2 +- tests/compat/py39.py | 20 ++++---------------- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/setup.cfg b/setup.cfg index f67df574..02d3c7e8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,7 +37,7 @@ testing = pyfakefs flufl.flake8 pytest-perf >= 0.9.2 - jaraco.collections + jaraco.test >= 5.4 docs = # upstream diff --git a/tests/compat/py39.py b/tests/compat/py39.py index b01ecad8..0f6f9c3a 100644 --- a/tests/compat/py39.py +++ b/tests/compat/py39.py @@ -1,18 +1,6 @@ -import types +from jaraco.test.cpython import from_test_support, try_import -from jaraco.collections import Projection - -def from_test_support(*names): - """ - Return a SimpleNamespace of names from test.support. - """ - import test.support - - return types.SimpleNamespace(**Projection(names, vars(test.support))) - - -try: - from test.support import os_helper # type: ignore -except ImportError: - os_helper = from_test_support('FS_NONASCII', 'skip_unless_symlink') +os_helper = try_import('os_helper') or from_test_support( + 'FS_NONASCII', 'skip_unless_symlink' +) From 07d894d96777e77f9dac3ec671f2dce4c584a26d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 20 Mar 2024 13:49:52 -0400 Subject: [PATCH 325/480] Copy backport of isolated_modules from importlib_resources. --- tests/compat/py312.py | 18 ++++++++++++++++++ tests/compat/py39.py | 1 + 2 files changed, 19 insertions(+) create mode 100644 tests/compat/py312.py diff --git a/tests/compat/py312.py b/tests/compat/py312.py new file mode 100644 index 00000000..ea9a58ba --- /dev/null +++ b/tests/compat/py312.py @@ -0,0 +1,18 @@ +import contextlib + +from .py39 import import_helper + + +@contextlib.contextmanager +def isolated_modules(): + """ + Save modules on entry and cleanup on exit. + """ + (saved,) = import_helper.modules_setup() + try: + yield + finally: + import_helper.modules_cleanup(saved) + + +vars(import_helper).setdefault('isolated_modules', isolated_modules) diff --git a/tests/compat/py39.py b/tests/compat/py39.py index 0f6f9c3a..5fe61c39 100644 --- a/tests/compat/py39.py +++ b/tests/compat/py39.py @@ -4,3 +4,4 @@ os_helper = try_import('os_helper') or from_test_support( 'FS_NONASCII', 'skip_unless_symlink' ) +import_helper = try_import('import_helper') or from_test_support() From adc4b124fc57cc3864bf68c61f7fa046757ffa02 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 20 Mar 2024 13:55:54 -0400 Subject: [PATCH 326/480] Ensure tests do not leak references in sys.modules. --- tests/fixtures.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/fixtures.py b/tests/fixtures.py index 07f10963..f8082df0 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -9,6 +9,7 @@ import functools import contextlib +from .compat.py312 import import_helper from .compat.py39 import os_helper from . import _path @@ -84,6 +85,7 @@ def add_sys_path(dir): def setUp(self): super().setUp() self.fixtures.enter_context(self.add_sys_path(self.site_dir)) + self.fixtures.enter_context(import_helper.isolated_modules()) class SiteBuilder(SiteDir): From 47b14acde7b15472b02a14c7abdd7e5545af37f5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 20 Mar 2024 15:17:03 -0400 Subject: [PATCH 327/480] Make MetadataPathFinder.find_distributions a classmethod for consistency with CPython. Closes #484. --- importlib_metadata/__init__.py | 5 +++-- newsfragments/484.bugfix.rst | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 newsfragments/484.bugfix.rst diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index c85ffa76..32ee3b4d 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -877,8 +877,9 @@ class MetadataPathFinder(NullFinder, DistributionFinder): of Python that do not have a PathFinder find_distributions(). """ + @classmethod def find_distributions( - self, context=DistributionFinder.Context() + cls, context=DistributionFinder.Context() ) -> Iterable[PathDistribution]: """ Find distributions. @@ -888,7 +889,7 @@ def find_distributions( (or all names if ``None`` indicated) along the paths in the list of directories ``context.path``. """ - found = self._search_paths(context.name, context.path) + found = cls._search_paths(context.name, context.path) return map(PathDistribution, found) @classmethod diff --git a/newsfragments/484.bugfix.rst b/newsfragments/484.bugfix.rst new file mode 100644 index 00000000..4274419b --- /dev/null +++ b/newsfragments/484.bugfix.rst @@ -0,0 +1 @@ +Make MetadataPathFinder.find_distributions a classmethod for consistency with CPython. Closes #484. \ No newline at end of file From 1711b2c1984b2f871fe12c9c14fd7a0b42d32987 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 20 Mar 2024 15:26:59 -0400 Subject: [PATCH 328/480] Need to include names from test.support for py312 compat. --- tests/compat/py39.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/compat/py39.py b/tests/compat/py39.py index 5fe61c39..16c8b574 100644 --- a/tests/compat/py39.py +++ b/tests/compat/py39.py @@ -4,4 +4,6 @@ os_helper = try_import('os_helper') or from_test_support( 'FS_NONASCII', 'skip_unless_symlink' ) -import_helper = try_import('import_helper') or from_test_support() +import_helper = try_import('import_helper') or from_test_support( + 'modules_setup', 'modules_cleanup' +) From f5d6b5f3f3f6fffe01b340c5a19562433db148a9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 20 Mar 2024 15:38:57 -0400 Subject: [PATCH 329/480] Finalize --- NEWS.rst | 16 ++++++++++++++++ newsfragments/+.bugfix.rst | 1 - newsfragments/+.feature.rst | 1 - newsfragments/484.bugfix.rst | 1 - 4 files changed, 16 insertions(+), 3 deletions(-) delete mode 100644 newsfragments/+.bugfix.rst delete mode 100644 newsfragments/+.feature.rst delete mode 100644 newsfragments/484.bugfix.rst diff --git a/NEWS.rst b/NEWS.rst index 085a5305..850e8f00 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,19 @@ +v7.1.0 +====== + +Features +-------- + +- Improve import time (python/cpython#114664). + + +Bugfixes +-------- + +- Make MetadataPathFinder.find_distributions a classmethod for consistency with CPython. Closes #484. (#484) +- Allow ``MetadataPathFinder.invalidate_caches`` to be called as a classmethod. + + v7.0.2 ====== diff --git a/newsfragments/+.bugfix.rst b/newsfragments/+.bugfix.rst deleted file mode 100644 index a8dd3dde..00000000 --- a/newsfragments/+.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Allow ``MetadataPathFinder.invalidate_caches`` to be called as a classmethod. diff --git a/newsfragments/+.feature.rst b/newsfragments/+.feature.rst deleted file mode 100644 index 865acfc1..00000000 --- a/newsfragments/+.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Improve import time (python/cpython#114664). diff --git a/newsfragments/484.bugfix.rst b/newsfragments/484.bugfix.rst deleted file mode 100644 index 4274419b..00000000 --- a/newsfragments/484.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Make MetadataPathFinder.find_distributions a classmethod for consistency with CPython. Closes #484. \ No newline at end of file From 6673723baa487e447135815e8aaf1bb41aa963bd Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Date: Thu, 21 Mar 2024 03:49:10 +0000 Subject: [PATCH 330/480] GH-109653: Defer import of ``importlib.metadata._adapters`` (python/cpython#109829) --------- Co-authored-by: Jason R. Coombs --- importlib_metadata/__init__.py | 5 ++++- newsfragments/+.feature.rst | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 newsfragments/+.feature.rst diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 32ee3b4d..f4c75941 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -18,7 +18,7 @@ import posixpath import collections -from . import _adapters, _meta +from . import _meta from .compat import py39 from ._collections import FreezableDefaultDict, Pair from ._compat import ( @@ -461,6 +461,9 @@ def metadata(self) -> _meta.PackageMetadata: Custom providers may provide the METADATA file or override this property. """ + # deferred for performance (python/cpython#109829) + from . import _adapters + opt_text = ( self.read_text('METADATA') or self.read_text('PKG-INFO') diff --git a/newsfragments/+.feature.rst b/newsfragments/+.feature.rst new file mode 100644 index 00000000..26618684 --- /dev/null +++ b/newsfragments/+.feature.rst @@ -0,0 +1 @@ +Deferred select imports in for speedup (python/cpython#109829). From a0d0c4b7e87fbfd04cee2546ba452858587516fd Mon Sep 17 00:00:00 2001 From: Avasam Date: Thu, 21 Mar 2024 15:34:23 -0400 Subject: [PATCH 331/480] Allow mypy on PyPy (jaraco/skeleton#111) https://github.com/pypa/setuptools/pull/4257 shows that mypy now works with PyPy --- setup.cfg | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 400a72a5..6fa73b6a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,9 +23,7 @@ testing = pytest >= 6 pytest-checkdocs >= 2.4 pytest-cov - pytest-mypy; \ - # workaround for jaraco/skeleton#22 - python_implementation != "PyPy" + pytest-mypy pytest-enabler >= 2.2 pytest-ruff >= 0.2.1 From c9a7f97ba83be124e173713f5c24564c2b6dd49e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 21 Mar 2024 15:49:52 -0400 Subject: [PATCH 332/480] Re-enable ignoring of temporary merge queue branches. Closes jaraco/skeleton#103. --- .github/workflows/main.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cf94f7d8..143b0984 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,8 +4,11 @@ on: merge_group: push: branches-ignore: - # disabled for jaraco/skeleton#103 - # - gh-readonly-queue/** # Temporary merge queue-related GH-made branches + # temporary GH branches relating to merge queues (jaraco/skeleton#93) + - gh-readonly-queue/** + tags: + # required if branches-ignore is supplied (jaraco/skeleton#103) + - '**' pull_request: permissions: From d72c6a081b67ce18eae654bf3c8d2d627af6939e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 23 Mar 2024 13:46:21 -0400 Subject: [PATCH 333/480] Fetch unshallow clones in readthedocs. Closes jaraco/skeleton#114. --- .readthedocs.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 68489063..85dfea9d 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -10,3 +10,7 @@ build: os: ubuntu-lts-latest tools: python: latest + # post-checkout job to ensure the clone isn't shallow jaraco/skeleton#114 + jobs: + post_checkout: + - git fetch --unshallow || true From 3fc7a935dfc0e5c8e330a29efc5518c464795cf8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 29 Mar 2024 21:11:46 -0400 Subject: [PATCH 334/480] Move Python 3.11 out of the test matrix. Probably should have done this when moving continue-on-error to Python 3.13. --- .github/workflows/main.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 143b0984..a15c74a6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -34,7 +34,6 @@ jobs: matrix: python: - "3.8" - - "3.11" - "3.12" platform: - ubuntu-latest @@ -45,6 +44,8 @@ jobs: platform: ubuntu-latest - python: "3.10" platform: ubuntu-latest + - python: "3.11" + platform: ubuntu-latest - python: pypy3.10 platform: ubuntu-latest runs-on: ${{ matrix.platform }} From 6ff02e0eefcd90e271cefd326b460ecfa0e3eb9e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 31 Mar 2024 04:27:11 -0400 Subject: [PATCH 335/480] Configure pytest to support namespace packages. Ref pytest-dev/pytest#12112. --- pytest.ini | 5 ++++- setup.cfg | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pytest.ini b/pytest.ini index 022a723e..9a0f3bce 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,9 @@ [pytest] norecursedirs=dist build .tox .eggs -addopts=--doctest-modules +addopts= + --doctest-modules + --import-mode importlib +consider_namespace_packages=true filterwarnings= ## upstream diff --git a/setup.cfg b/setup.cfg index 6fa73b6a..f46b6cbf 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,7 +20,7 @@ install_requires = [options.extras_require] testing = # upstream - pytest >= 6 + pytest >= 6, != 8.1.1 pytest-checkdocs >= 2.4 pytest-cov pytest-mypy From 34ba6b2ec0650c8c70d9285a0c7ee1a126406807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20S=C5=82awecki?= Date: Mon, 1 Apr 2024 17:47:04 +0200 Subject: [PATCH 336/480] Add link to blog entry from jaraco/skeleton#115 above CI build matrix. --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a15c74a6..ac0ff69e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -31,6 +31,7 @@ env: jobs: test: strategy: + # https://blog.jaraco.com/efficient-use-of-ci-resources/ matrix: python: - "3.8" From 7ad4f2fa9fb2b030d3ecc231fc24de181705622d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 16 Apr 2024 10:31:45 -0400 Subject: [PATCH 337/480] Pin against pytest 8.1.x due to pytest-dev/pytest#12194. --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index f46b6cbf..05ac4c76 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,7 +20,7 @@ install_requires = [options.extras_require] testing = # upstream - pytest >= 6, != 8.1.1 + pytest >= 6, != 8.1.* pytest-checkdocs >= 2.4 pytest-cov pytest-mypy From f4529af6a66e34d423860566be7882d665e10569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartosz=20S=C5=82awecki?= Date: Tue, 16 Apr 2024 22:37:50 +0200 Subject: [PATCH 338/480] Move project metadata to `pyproject.toml` (jaraco/skeleton#122) Intentionally omitted specifying `tool.setuptools.include-package-data`: it's true by default in `pyproject.toml` according to https://setuptools.pypa.io/en/latest/userguide/datafiles.html#include-package-data. Closes jaraco/skeleton#121 --- pyproject.toml | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- setup.cfg | 42 ------------------------------------------ 2 files changed, 48 insertions(+), 43 deletions(-) delete mode 100644 setup.cfg diff --git a/pyproject.toml b/pyproject.toml index a853c578..869fe7e5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,52 @@ [build-system] -requires = ["setuptools>=56", "setuptools_scm[toml]>=3.4.1"] +requires = ["setuptools>=61.2", "setuptools_scm[toml]>=3.4.1"] build-backend = "setuptools.build_meta" +[project] +name = "PROJECT" +authors = [ + { name = "Jason R. Coombs", email = "jaraco@jaraco.com" }, +] +description = "PROJECT_DESCRIPTION" +readme = "README.rst" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", +] +requires-python = ">=3.8" +dependencies = [ +] +dynamic = ["version"] + +[project.optional-dependencies] +testing = [ + # upstream + "pytest >= 6, != 8.1.*", + "pytest-checkdocs >= 2.4", + "pytest-cov", + "pytest-mypy", + "pytest-enabler >= 2.2", + "pytest-ruff >= 0.2.1", + + # local +] +docs = [ + # upstream + "sphinx >= 3.5", + "jaraco.packaging >= 9.3", + "rst.linker >= 1.9", + "furo", + "sphinx-lint", + + # local +] + +[project.urls] +Homepage = "https://github.com/PROJECT_PATH" + +[project.scripts] + [tool.setuptools_scm] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 05ac4c76..00000000 --- a/setup.cfg +++ /dev/null @@ -1,42 +0,0 @@ -[metadata] -name = PROJECT -author = Jason R. Coombs -author_email = jaraco@jaraco.com -description = PROJECT_DESCRIPTION -long_description = file:README.rst -url = https://github.com/PROJECT_PATH -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - License :: OSI Approved :: MIT License - Programming Language :: Python :: 3 - Programming Language :: Python :: 3 :: Only - -[options] -include_package_data = true -python_requires = >=3.8 -install_requires = - -[options.extras_require] -testing = - # upstream - pytest >= 6, != 8.1.* - pytest-checkdocs >= 2.4 - pytest-cov - pytest-mypy - pytest-enabler >= 2.2 - pytest-ruff >= 0.2.1 - - # local - -docs = - # upstream - sphinx >= 3.5 - jaraco.packaging >= 9.3 - rst.linker >= 1.9 - furo - sphinx-lint - - # local - -[options.entry_points] From d34801bd169637d95d262fca9e0dbd658ae1feef Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 18 Apr 2024 16:38:18 -0400 Subject: [PATCH 339/480] Migrated config to pyproject.toml using jaraco.develop.migrate-config and ini2toml. --- pyproject.toml | 59 +++++++++++++++++++++++++++++++++++++++++++++++++- setup.cfg | 58 ------------------------------------------------- 2 files changed, 58 insertions(+), 59 deletions(-) delete mode 100644 setup.cfg diff --git a/pyproject.toml b/pyproject.toml index a853c578..4931fcbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,62 @@ [build-system] -requires = ["setuptools>=56", "setuptools_scm[toml]>=3.4.1"] +requires = ["setuptools>=61.2", "setuptools_scm[toml]>=3.4.1"] build-backend = "setuptools.build_meta" +[project] +name = "importlib_metadata" +authors = [ + { name = "Jason R. Coombs", email = "jaraco@jaraco.com" }, +] +description = "Read metadata from Python packages" +readme = "README.rst" +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", +] +requires-python = ">=3.8" +dependencies = [ + "zipp>=0.5", + 'typing-extensions>=3.6.4; python_version < "3.8"', +] +dynamic = ["version"] + +[project.urls] +Homepage = "https://github.com/python/importlib_metadata" + +[project.optional-dependencies] +testing = [ + # upstream + "pytest >= 6", + "pytest-checkdocs >= 2.4", + "pytest-cov", + 'pytest-mypy; python_implementation != "PyPy"', # workaround for jaraco/skeleton#22 + "pytest-enabler >= 2.2", + "pytest-ruff >= 0.2.1", + + # local + 'importlib_resources>=1.3; python_version < "3.9"', + "packaging", + "pyfakefs", + "flufl.flake8", + "pytest-perf >= 0.9.2", + "jaraco.test >= 5.4", +] +docs = [ + # upstream + "sphinx >= 3.5", + "jaraco.packaging >= 9.3", + "rst.linker >= 1.9", + "furo", + "sphinx-lint", + + # tidelift + "jaraco.tidelift >= 1.4", + + # local +] +perf = ["ipython"] + [tool.setuptools_scm] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 02d3c7e8..00000000 --- a/setup.cfg +++ /dev/null @@ -1,58 +0,0 @@ -[metadata] -name = importlib_metadata -author = Jason R. Coombs -author_email = jaraco@jaraco.com -description = Read metadata from Python packages -long_description = file:README.rst -url = https://github.com/python/importlib_metadata -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Programming Language :: Python :: 3 - Programming Language :: Python :: 3 :: Only - -[options] -include_package_data = true -python_requires = >=3.8 -install_requires = - zipp>=0.5 - typing-extensions>=3.6.4; python_version < "3.8" - -[options.extras_require] -testing = - # upstream - pytest >= 6 - pytest-checkdocs >= 2.4 - pytest-cov - pytest-mypy; \ - # workaround for jaraco/skeleton#22 - python_implementation != "PyPy" - pytest-enabler >= 2.2 - pytest-ruff >= 0.2.1 - - # local - importlib_resources>=1.3; python_version < "3.9" - packaging - pyfakefs - flufl.flake8 - pytest-perf >= 0.9.2 - jaraco.test >= 5.4 - -docs = - # upstream - sphinx >= 3.5 - jaraco.packaging >= 9.3 - rst.linker >= 1.9 - furo - sphinx-lint - - # tidelift - jaraco.tidelift >= 1.4 - - # local - -perf = - ipython - -[options.entry_points] From 744cf2a2befb6a616657c105e5c9be9f3f921224 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 24 Apr 2024 10:48:06 -0400 Subject: [PATCH 340/480] Allow macos on Python 3.8 to fail as GitHub CI has dropped support. Closes jaraco/skeleton#124. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ac0ff69e..5ace4c50 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -50,7 +50,7 @@ jobs: - python: pypy3.10 platform: ubuntu-latest runs-on: ${{ matrix.platform }} - continue-on-error: ${{ matrix.python == '3.13' }} + continue-on-error: ${{ matrix.python == '3.13' || (matrix.python == '3.8' || matrix.python == '3.9') && matrix.platform == 'macos-latest' }} steps: - uses: actions/checkout@v4 - name: Setup Python From bcf8f079eb729e7bcd50c10cf4da522620b00635 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 24 Apr 2024 11:06:12 -0400 Subject: [PATCH 341/480] Move project.urls to appear in the order that ini2toml generates it. Remove project.scripts. --- pyproject.toml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 869fe7e5..04b14cbc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,9 @@ dependencies = [ ] dynamic = ["version"] +[project.urls] +Homepage = "https://github.com/PROJECT_PATH" + [project.optional-dependencies] testing = [ # upstream @@ -44,9 +47,4 @@ docs = [ # local ] -[project.urls] -Homepage = "https://github.com/PROJECT_PATH" - -[project.scripts] - [tool.setuptools_scm] From 67aab1554c7c9cbb19bb546a5b6476267030c5b5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 2 May 2024 15:36:22 -0400 Subject: [PATCH 342/480] Revert "Allow macos on Python 3.8 to fail as GitHub CI has dropped support." This reverts commit 744cf2a2befb6a616657c105e5c9be9f3f921224. --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5ace4c50..ac0ff69e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -50,7 +50,7 @@ jobs: - python: pypy3.10 platform: ubuntu-latest runs-on: ${{ matrix.platform }} - continue-on-error: ${{ matrix.python == '3.13' || (matrix.python == '3.8' || matrix.python == '3.9') && matrix.platform == 'macos-latest' }} + continue-on-error: ${{ matrix.python == '3.13' }} steps: - uses: actions/checkout@v4 - name: Setup Python From a595a0fad054cd20b69d3e954c99174e3a548938 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 31 May 2024 03:53:48 -0400 Subject: [PATCH 343/480] Rename extras to align with core metadata spec. Closes jaraco/skeleton#125. --- .readthedocs.yaml | 2 +- pyproject.toml | 4 ++-- tox.ini | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 85dfea9d..dc8516ac 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -3,7 +3,7 @@ python: install: - path: . extra_requirements: - - docs + - doc # required boilerplate readthedocs/readthedocs.org#10401 build: diff --git a/pyproject.toml b/pyproject.toml index 04b14cbc..50845ee3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ dynamic = ["version"] Homepage = "https://github.com/PROJECT_PATH" [project.optional-dependencies] -testing = [ +test = [ # upstream "pytest >= 6, != 8.1.*", "pytest-checkdocs >= 2.4", @@ -36,7 +36,7 @@ testing = [ # local ] -docs = [ +doc = [ # upstream "sphinx >= 3.5", "jaraco.packaging >= 9.3", diff --git a/tox.ini b/tox.ini index 4c39a5b1..cc4db36e 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,7 @@ commands = pytest {posargs} usedevelop = True extras = - testing + test [testenv:diffcov] description = run tests and check that diff from main is covered @@ -22,8 +22,8 @@ commands = [testenv:docs] description = build the documentation extras = - docs - testing + doc + test changedir = docs commands = python -m sphinx -W --keep-going . {toxinidir}/build/html From 963f643b1f05effc980bb6df0196c64f6576dfc6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 20 Jun 2024 12:39:10 -0400 Subject: [PATCH 344/480] gh-120801: Update fixtures. Removed unused fixtures relating to cwd. Rely on os_helper.temp_dir where relevant. Renamed to tmp_path to reflect pathlib behavior. --- newsfragments/+76d2542b.feature.rst | 1 + tests/compat/py39.py | 2 +- tests/fixtures.py | 33 ++++++----------------------- tests/test_api.py | 2 +- tests/test_main.py | 2 +- 5 files changed, 11 insertions(+), 29 deletions(-) create mode 100644 newsfragments/+76d2542b.feature.rst diff --git a/newsfragments/+76d2542b.feature.rst b/newsfragments/+76d2542b.feature.rst new file mode 100644 index 00000000..43b0e391 --- /dev/null +++ b/newsfragments/+76d2542b.feature.rst @@ -0,0 +1 @@ +Updated fixtures for python/cpython#120801. diff --git a/tests/compat/py39.py b/tests/compat/py39.py index 16c8b574..9476eb35 100644 --- a/tests/compat/py39.py +++ b/tests/compat/py39.py @@ -2,7 +2,7 @@ os_helper = try_import('os_helper') or from_test_support( - 'FS_NONASCII', 'skip_unless_symlink' + 'FS_NONASCII', 'skip_unless_symlink', 'temp_dir' ) import_helper = try_import('import_helper') or from_test_support( 'modules_setup', 'modules_cleanup' diff --git a/tests/fixtures.py b/tests/fixtures.py index f8082df0..1bf9a803 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -1,10 +1,8 @@ -import os import sys import copy import json import shutil import pathlib -import tempfile import textwrap import functools import contextlib @@ -26,29 +24,12 @@ @contextlib.contextmanager -def tempdir(): - tmpdir = tempfile.mkdtemp() - try: - yield pathlib.Path(tmpdir) - finally: - shutil.rmtree(tmpdir) - - -@contextlib.contextmanager -def save_cwd(): - orig = os.getcwd() - try: - yield - finally: - os.chdir(orig) - - -@contextlib.contextmanager -def tempdir_as_cwd(): - with tempdir() as tmp: - with save_cwd(): - os.chdir(str(tmp)) - yield tmp +def tmp_path(): + """ + Like os_helper.temp_dir, but yields a pathlib.Path. + """ + with os_helper.temp_dir() as path: + yield pathlib.Path(path) @contextlib.contextmanager @@ -69,7 +50,7 @@ def setUp(self): class SiteDir(Fixtures): def setUp(self): super().setUp() - self.site_dir = self.fixtures.enter_context(tempdir()) + self.site_dir = self.fixtures.enter_context(tmp_path()) class OnSysPath(Fixtures): diff --git a/tests/test_api.py b/tests/test_api.py index a85c62ad..a93065cb 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -109,7 +109,7 @@ def test_entry_points_unique_packages_normalized(self): Entry points should only be exposed for the first package on sys.path with a given name (even when normalized). """ - alt_site_dir = self.fixtures.enter_context(fixtures.tempdir()) + alt_site_dir = self.fixtures.enter_context(fixtures.tmp_path()) self.fixtures.enter_context(self.add_sys_path(alt_site_dir)) alt_pkg = { "DistInfo_pkg-1.1.0.dist-info": { diff --git a/tests/test_main.py b/tests/test_main.py index af79e698..32241c00 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -135,7 +135,7 @@ def test_unique_distributions(self): fixtures.build_files(self.make_pkg('abc'), self.site_dir) before = list(_unique(distributions())) - alt_site_dir = self.fixtures.enter_context(fixtures.tempdir()) + alt_site_dir = self.fixtures.enter_context(fixtures.tmp_path()) self.fixtures.enter_context(self.add_sys_path(alt_site_dir)) fixtures.build_files(self.make_pkg('ABC'), alt_site_dir) after = list(_unique(distributions())) From 311cef4ab03cfd122bc4895094ab85b67f565406 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 20 Jun 2024 12:40:33 -0400 Subject: [PATCH 345/480] Finalize --- NEWS.rst | 10 ++++++++++ newsfragments/+.feature.rst | 1 - newsfragments/+76d2542b.feature.rst | 1 - 3 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 newsfragments/+.feature.rst delete mode 100644 newsfragments/+76d2542b.feature.rst diff --git a/NEWS.rst b/NEWS.rst index 850e8f00..70f1eb38 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,13 @@ +v7.2.0 +====== + +Features +-------- + +- Deferred select imports in for speedup (python/cpython#109829). +- Updated fixtures for python/cpython#120801. + + v7.1.0 ====== diff --git a/newsfragments/+.feature.rst b/newsfragments/+.feature.rst deleted file mode 100644 index 26618684..00000000 --- a/newsfragments/+.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Deferred select imports in for speedup (python/cpython#109829). diff --git a/newsfragments/+76d2542b.feature.rst b/newsfragments/+76d2542b.feature.rst deleted file mode 100644 index 43b0e391..00000000 --- a/newsfragments/+76d2542b.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Updated fixtures for python/cpython#120801. From c9729e1a0f66b7adad70c629518b7dab82ccd8c6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 21 Jun 2024 13:09:07 -0400 Subject: [PATCH 346/480] Prefer "Source" to "Homepage" for the repository label. Closes jaraco/skeleton#129 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 50845ee3..ad67d3b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,7 +22,7 @@ dependencies = [ dynamic = ["version"] [project.urls] -Homepage = "https://github.com/PROJECT_PATH" +Source = "https://github.com/PROJECT_PATH" [project.optional-dependencies] test = [ From 08d74cc165094bda158e108747fa39ae10674bbf Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 22 Jun 2024 21:42:03 -0400 Subject: [PATCH 347/480] Move _relative_to compatibility to a compat module. --- importlib_metadata/__init__.py | 24 ++++-------------------- importlib_metadata/compat/py38.py | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 20 deletions(-) create mode 100644 importlib_metadata/compat/py38.py diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 61325221..c2c4b1e9 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -19,6 +19,7 @@ import posixpath import collections +from .compat.py38 import relative_fix from . import _adapters, _meta, _py39compat from ._collections import FreezableDefaultDict, Pair from ._compat import ( @@ -562,31 +563,14 @@ def _read_files_egginfo_installed(self): if not text or not subdir: return - site_path = self.locate_file('').resolve() paths = ( - self._relative_to( - (subdir / name).resolve(), - site_path, - ).as_posix() + relative_fix((subdir / name).resolve()) + .relative_to(self.locate_file('').resolve(), walk_up=True) + .as_posix() for name in text.splitlines() ) return map('"{}"'.format, paths) - def _relative_to(self, path, root): - """ - Workaround for https://github.com/python/cpython/issues/67271 where ".." - isn't added by pathlib.Path.relative_to() when path is not - a subpath of root. - - One example of such a package is dask-labextension, which uses - jupyter-packaging to install JupyterLab javascript files outside - of site-packages. - """ - try: - return path.relative_to(root) - except ValueError: - return pathlib.Path(os.path.relpath(path, root)) - def _read_files_egginfo_sources(self): """ Read SOURCES.txt and return lines in a similar CSV-parsable diff --git a/importlib_metadata/compat/py38.py b/importlib_metadata/compat/py38.py new file mode 100644 index 00000000..2499c3f6 --- /dev/null +++ b/importlib_metadata/compat/py38.py @@ -0,0 +1,23 @@ +import os +import pathlib +import sys +import types + + +def wrap(path): + """ + Workaround for https://github.com/python/cpython/issues/67271 where ".." + isn't added by pathlib.Path.relative_to() when path is not + a subpath of root. + One example of such a package is dask-labextension, which uses + jupyter-packaging to install JupyterLab javascript files outside + of site-packages. + """ + + def relative_to(root, *, walk_up=False): + return pathlib.Path(os.path.relpath(path, root)) + + return types.SimpleNamespace(relative_to=relative_to) + + +relative_fix = wrap if sys.version_info < (3, 9) else lambda x: x From 1584b96f50317f7cbf55192706a9a1c4ef869a3b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 22 Jun 2024 21:56:20 -0400 Subject: [PATCH 348/480] Update wrapper to rely on 'relative_to(walk_up=True)' on Python 3.12 and compatibility wrapper on Python 3.11 and earlier. --- importlib_metadata/__init__.py | 2 +- importlib_metadata/compat/{py38.py => py311.py} | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) rename importlib_metadata/compat/{py38.py => py311.py} (53%) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index c2c4b1e9..94818f62 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -19,7 +19,7 @@ import posixpath import collections -from .compat.py38 import relative_fix +from .compat.py311 import relative_fix from . import _adapters, _meta, _py39compat from ._collections import FreezableDefaultDict, Pair from ._compat import ( diff --git a/importlib_metadata/compat/py38.py b/importlib_metadata/compat/py311.py similarity index 53% rename from importlib_metadata/compat/py38.py rename to importlib_metadata/compat/py311.py index 2499c3f6..d4b6f359 100644 --- a/importlib_metadata/compat/py38.py +++ b/importlib_metadata/compat/py311.py @@ -6,10 +6,9 @@ def wrap(path): """ - Workaround for https://github.com/python/cpython/issues/67271 where ".." - isn't added by pathlib.Path.relative_to() when path is not - a subpath of root. - One example of such a package is dask-labextension, which uses + Workaround for https://github.com/python/cpython/issues/84538 + to add backward compatibility for walk_up=True. + An example affected package is dask-labextension, which uses jupyter-packaging to install JupyterLab javascript files outside of site-packages. """ @@ -20,4 +19,4 @@ def relative_to(root, *, walk_up=False): return types.SimpleNamespace(relative_to=relative_to) -relative_fix = wrap if sys.version_info < (3, 9) else lambda x: x +relative_fix = wrap if sys.version_info < (3, 12) else lambda x: x From b815aee5352ed728f6f90ba7362f3dddf46ab418 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 22 Jun 2024 22:18:37 -0400 Subject: [PATCH 349/480] Mark compat code as uncovered. --- importlib_metadata/compat/py311.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/importlib_metadata/compat/py311.py b/importlib_metadata/compat/py311.py index d4b6f359..3a532743 100644 --- a/importlib_metadata/compat/py311.py +++ b/importlib_metadata/compat/py311.py @@ -4,7 +4,7 @@ import types -def wrap(path): +def wrap(path): # pragma: no cover """ Workaround for https://github.com/python/cpython/issues/84538 to add backward compatibility for walk_up=True. From 07a2a4402fb39f03facea611fa9da8d9b927602e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 23 Jun 2024 10:34:14 -0400 Subject: [PATCH 350/480] Revert "Fix mypy failure that has nothing to do with this PR" This reverts commit 4a3782d11895c22fd443c67175ab7352db2a769b. --- 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 f79376e9..eab73ffe 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -440,7 +440,7 @@ def at(path: str | os.PathLike[str]) -> Distribution: :param path: a string or path-like object :return: a concrete Distribution instance for the path """ - return PathDistribution(cast(SimplePath, pathlib.Path(path))) + return PathDistribution(pathlib.Path(path)) @staticmethod def _discover_resolvers(): From e4d1dcca7244c0d890c57eb24b3b8a6a76f4910e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 23 Jun 2024 11:02:40 -0400 Subject: [PATCH 351/480] Remove additional method in SimplePath. --- importlib_metadata/_meta.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/importlib_metadata/_meta.py b/importlib_metadata/_meta.py index 13ab8d90..1927d0f6 100644 --- a/importlib_metadata/_meta.py +++ b/importlib_metadata/_meta.py @@ -65,5 +65,3 @@ def read_text(self, encoding=None) -> str: ... # pragma: no cover def read_bytes(self) -> bytes: ... # pragma: no cover def exists(self) -> bool: ... # pragma: no cover - - def resolve(self) -> bool: ... # pragma: no cover From b94b42ef3103250a0f509f68170037199dc86583 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 23 Jun 2024 11:10:58 -0400 Subject: [PATCH 352/480] Add news fragment --- newsfragments/455.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/455.bugfix.rst diff --git a/newsfragments/455.bugfix.rst b/newsfragments/455.bugfix.rst new file mode 100644 index 00000000..72bc9917 --- /dev/null +++ b/newsfragments/455.bugfix.rst @@ -0,0 +1 @@ +When reading installed files from an egg, use ``relative_to(walk_up=True)`` to honor files installed outside of the installation root. \ No newline at end of file From b76931df96cb577bedbfac086d507a731a74b4b3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 23 Jun 2024 11:12:45 -0400 Subject: [PATCH 353/480] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/455.bugfix.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/455.bugfix.rst diff --git a/NEWS.rst b/NEWS.rst index 70f1eb38..e337db4d 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v7.2.1 +====== + +Bugfixes +-------- + +- When reading installed files from an egg, use ``relative_to(walk_up=True)`` to honor files installed outside of the installation root. (#455) + + v7.2.0 ====== diff --git a/newsfragments/455.bugfix.rst b/newsfragments/455.bugfix.rst deleted file mode 100644 index 72bc9917..00000000 --- a/newsfragments/455.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -When reading installed files from an egg, use ``relative_to(walk_up=True)`` to honor files installed outside of the installation root. \ No newline at end of file From 32c14aa622cd6d8c41449f5163370dc60af1e5dc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 23 Jun 2024 14:15:53 -0400 Subject: [PATCH 354/480] Removed deprecated support for Distribution subclasses not implementing abstract methods. --- docs/conf.py | 2 -- importlib_metadata/__init__.py | 23 +---------------------- newsfragments/+8256a9d7.removal.rst | 1 + tests/test_main.py | 13 ------------- 4 files changed, 2 insertions(+), 37 deletions(-) create mode 100644 newsfragments/+8256a9d7.removal.rst diff --git a/docs/conf.py b/docs/conf.py index 90a1da2a..2cd8fb0c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -69,6 +69,4 @@ ('py:class', 'importlib_metadata._meta._T'), # Workaround for #435 ('py:class', '_T'), - # Other workarounds - ('py:class', 'importlib_metadata.DeprecatedNonAbstract'), ] diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index eab73ffe..ed481355 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -12,7 +12,6 @@ import pathlib import operator import textwrap -import warnings import functools import itertools import posixpath @@ -334,27 +333,7 @@ def __repr__(self) -> str: return f'' -class DeprecatedNonAbstract: - # Required until Python 3.14 - 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): +class Distribution(metaclass=abc.ABCMeta): """ An abstract Python distribution package. diff --git a/newsfragments/+8256a9d7.removal.rst b/newsfragments/+8256a9d7.removal.rst new file mode 100644 index 00000000..86f61c42 --- /dev/null +++ b/newsfragments/+8256a9d7.removal.rst @@ -0,0 +1 @@ +Removed deprecated support for Distribution subclasses not implementing abstract methods. \ No newline at end of file diff --git a/tests/test_main.py b/tests/test_main.py index 32241c00..f1c12855 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,16 +1,13 @@ import re import pickle import unittest -import warnings import importlib import importlib_metadata -import contextlib from .compat.py39 import os_helper import pyfakefs.fake_filesystem_unittest as ffs from . import fixtures -from ._context import suppress from ._path import Symlink from importlib_metadata import ( Distribution, @@ -25,13 +22,6 @@ ) -@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)?' @@ -56,9 +46,6 @@ 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 a970a491b56a3bf529821ce21867af4573cf2e0d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 23 Jun 2024 14:24:28 -0400 Subject: [PATCH 355/480] Message.__getitem__ now raises a KeyError on missing keys. Ref #371 --- importlib_metadata/_adapters.py | 23 ++++++++--------------- newsfragments/371.removal.rst | 1 + tests/test_api.py | 17 ++++------------- 3 files changed, 13 insertions(+), 28 deletions(-) create mode 100644 newsfragments/371.removal.rst diff --git a/importlib_metadata/_adapters.py b/importlib_metadata/_adapters.py index 120e43a0..6223263e 100644 --- a/importlib_metadata/_adapters.py +++ b/importlib_metadata/_adapters.py @@ -1,20 +1,8 @@ -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): @@ -53,12 +41,17 @@ def __iter__(self): def __getitem__(self, item): """ - Warn users that a ``KeyError`` can be expected when a - missing key is supplied. Ref python/importlib_metadata#371. + Override parent behavior to typical dict behavior. + + ``email.message.Message`` will emit None values for missing + keys. Typical mappings, including this ``Message``, will raise + a key error for missing keys. + + Ref python/importlib_metadata#371. """ res = super().__getitem__(item) if res is None: - _warn() + raise KeyError(item) return res def _repair_headers(self): diff --git a/newsfragments/371.removal.rst b/newsfragments/371.removal.rst new file mode 100644 index 00000000..7aca2d4f --- /dev/null +++ b/newsfragments/371.removal.rst @@ -0,0 +1 @@ +Message.__getitem__ now raises a KeyError on missing keys. \ No newline at end of file diff --git a/tests/test_api.py b/tests/test_api.py index fc20987e..7ce0cd64 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,9 +1,7 @@ import re import textwrap import unittest -import warnings import importlib -import contextlib from . import fixtures from importlib_metadata import ( @@ -18,13 +16,6 @@ ) -@contextlib.contextmanager -def suppress_known_deprecation(): - with warnings.catch_warnings(record=True) as ctx: - warnings.simplefilter('default', category=DeprecationWarning) - yield ctx - - class APITests( fixtures.EggInfoPkg, fixtures.EggInfoPkgPipInstalledNoToplevel, @@ -157,13 +148,13 @@ def test_importlib_metadata_version(self): resolved = version('importlib-metadata') assert re.match(self.version_pattern, resolved) - def test_missing_key_legacy(self): + def test_missing_key(self): """ - Requesting a missing key will still return None, but warn. + Requesting a missing key raises KeyError. """ md = metadata('distinfo-pkg') - with suppress_known_deprecation(): - assert md['does-not-exist'] is None + with self.assertRaises(KeyError): + md['does-not-exist'] def test_get_key(self): """ From f3901686abc47853523f3b211873fc2b9e0c5ab5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 23 Jun 2024 15:57:21 -0400 Subject: [PATCH 356/480] Finalize --- NEWS.rst | 10 ++++++++++ newsfragments/+8256a9d7.removal.rst | 1 - newsfragments/371.removal.rst | 1 - 3 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 newsfragments/+8256a9d7.removal.rst delete mode 100644 newsfragments/371.removal.rst diff --git a/NEWS.rst b/NEWS.rst index e337db4d..a49f5ec9 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,13 @@ +v8.0.0 +====== + +Deprecations and Removals +------------------------- + +- Message.__getitem__ now raises a KeyError on missing keys. (#371) +- Removed deprecated support for Distribution subclasses not implementing abstract methods. + + v7.2.1 ====== diff --git a/newsfragments/+8256a9d7.removal.rst b/newsfragments/+8256a9d7.removal.rst deleted file mode 100644 index 86f61c42..00000000 --- a/newsfragments/+8256a9d7.removal.rst +++ /dev/null @@ -1 +0,0 @@ -Removed deprecated support for Distribution subclasses not implementing abstract methods. \ No newline at end of file diff --git a/newsfragments/371.removal.rst b/newsfragments/371.removal.rst deleted file mode 100644 index 7aca2d4f..00000000 --- a/newsfragments/371.removal.rst +++ /dev/null @@ -1 +0,0 @@ -Message.__getitem__ now raises a KeyError on missing keys. \ No newline at end of file From 33c4896dbaeda2fd7a5fef701431dea05bb83bab Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 10 Jul 2024 15:34:09 -0400 Subject: [PATCH 357/480] Exclude pytest-ruff (and thus ruff), which cannot build on cygwin. Ref pypa/setuptools#3921 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ad67d3b1..1307e1fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,7 +32,7 @@ test = [ "pytest-cov", "pytest-mypy", "pytest-enabler >= 2.2", - "pytest-ruff >= 0.2.1", + "pytest-ruff >= 0.2.1; sys_platform != 'cygwin'", # local ] From f087fb4ca05eb08c46abdd2cd67b18a3f33e3c79 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Fri, 19 Jul 2024 16:32:46 +0200 Subject: [PATCH 358/480] "preserve" does not require preview any more (jaraco/skeleton#133) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * "preserve" does not require preview any more * Update URL in ruff.toml comment --------- Co-authored-by: Bartosz Sławecki --- ruff.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ruff.toml b/ruff.toml index 70612985..7da4bee7 100644 --- a/ruff.toml +++ b/ruff.toml @@ -22,7 +22,5 @@ ignore = [ ] [format] -# Enable preview, required for quote-style = "preserve" -preview = true -# https://docs.astral.sh/ruff/settings/#format-quote-style +# https://docs.astral.sh/ruff/settings/#format_quote-style quote-style = "preserve" From 30f940e74b599400347d1162b7096f184cc46d31 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Fri, 19 Jul 2024 16:34:53 +0200 Subject: [PATCH 359/480] Enforce ruff/Perflint rule PERF401 (jaraco/skeleton#132) --- ruff.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/ruff.toml b/ruff.toml index 7da4bee7..f1d03f83 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,6 +1,7 @@ [lint] extend-select = [ "C901", + "PERF401", "W", ] ignore = [ From ab34814ca3ffe511ad63bb9589da06fd76758db8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 19 Jul 2024 12:33:01 -0400 Subject: [PATCH 360/480] Re-enable preview, this time not for one specific feature, but for all features in preview. Ref jaraco/skeleton#133 --- ruff.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ruff.toml b/ruff.toml index f1d03f83..922aa1f1 100644 --- a/ruff.toml +++ b/ruff.toml @@ -23,5 +23,8 @@ ignore = [ ] [format] +# Enable preview to get hugged parenthesis unwrapping and other nice surprises +# See https://github.com/jaraco/skeleton/pull/133#issuecomment-2239538373 +preview = true # https://docs.astral.sh/ruff/settings/#format_quote-style quote-style = "preserve" From 48f6b143419941be07dc53330a4702ff8c9398c4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 23 Jul 2024 08:56:52 -0400 Subject: [PATCH 361/480] Add test capturing failed expectation. Ref #489. --- tests/test_main.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/test_main.py b/tests/test_main.py index f1c12855..a18c2d17 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -130,6 +130,32 @@ def test_unique_distributions(self): assert len(after) == len(before) +class InvalidMetadataTests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase): + @staticmethod + def make_pkg(name, files=dict(METADATA="VERSION: 1.0")): + """ + Create metadata for a dist-info package with name and files. + """ + return { + f'{name}.dist-info': files, + } + + @__import__('pytest').mark.xfail(reason="#489") + def test_valid_dists_preferred(self): + """ + Dists with metadata should be preferred when discovered by name. + + Ref python/importlib_metadata#489. + """ + # create three dists with the valid one in the middle (lexicographically) + # such that on most file systems, the valid one is never naturally first. + fixtures.build_files(self.make_pkg('foo-4.0', files={}), self.site_dir) + fixtures.build_files(self.make_pkg('foo-4.1'), self.site_dir) + fixtures.build_files(self.make_pkg('foo-4.2', files={}), self.site_dir) + dist = Distribution.from_name('foo') + assert dist.version == "1.0" + + class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase): @staticmethod def pkg_with_non_ascii_description(site_dir): From a65c29adc027b3615154cab73aaedd58a6aa23da Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 23 Jul 2024 08:36:16 -0400 Subject: [PATCH 362/480] Prioritize valid dists to invalid dists when retrieving by name. Closes python/importlib_metadata#489 --- importlib_metadata/__init__.py | 14 ++++- importlib_metadata/_itertools.py | 98 ++++++++++++++++++++++++++++++++ newsfragments/489.feature.rst | 1 + tests/test_main.py | 1 - 4 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 newsfragments/489.feature.rst diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index ed481355..b9fc04f1 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -25,7 +25,7 @@ install, ) from ._functools import method_cache, pass_none -from ._itertools import always_iterable, unique_everseen +from ._itertools import always_iterable, bucket, unique_everseen from ._meta import PackageMetadata, SimplePath from contextlib import suppress @@ -388,7 +388,7 @@ def from_name(cls, name: str) -> Distribution: if not name: raise ValueError("A distribution name is required.") try: - return next(iter(cls.discover(name=name))) + return next(iter(cls._prefer_valid(cls.discover(name=name)))) except StopIteration: raise PackageNotFoundError(name) @@ -412,6 +412,16 @@ def discover( resolver(context) for resolver in cls._discover_resolvers() ) + @staticmethod + def _prefer_valid(dists: Iterable[Distribution]) -> Iterable[Distribution]: + """ + Prefer (move to the front) distributions that have metadata. + + Ref python/importlib_resources#489. + """ + buckets = bucket(dists, lambda dist: bool(dist.metadata)) + return itertools.chain(buckets[True], buckets[False]) + @staticmethod def at(path: str | os.PathLike[str]) -> Distribution: """Return a Distribution for the indicated metadata path. diff --git a/importlib_metadata/_itertools.py b/importlib_metadata/_itertools.py index d4ca9b91..79d37198 100644 --- a/importlib_metadata/_itertools.py +++ b/importlib_metadata/_itertools.py @@ -1,3 +1,4 @@ +from collections import defaultdict, deque from itertools import filterfalse @@ -71,3 +72,100 @@ def always_iterable(obj, base_type=(str, bytes)): return iter(obj) except TypeError: return iter((obj,)) + + +# Copied from more_itertools 10.3 +class bucket: + """Wrap *iterable* and return an object that buckets the iterable into + child iterables based on a *key* function. + + >>> iterable = ['a1', 'b1', 'c1', 'a2', 'b2', 'c2', 'b3'] + >>> s = bucket(iterable, key=lambda x: x[0]) # Bucket by 1st character + >>> sorted(list(s)) # Get the keys + ['a', 'b', 'c'] + >>> a_iterable = s['a'] + >>> next(a_iterable) + 'a1' + >>> next(a_iterable) + 'a2' + >>> list(s['b']) + ['b1', 'b2', 'b3'] + + The original iterable will be advanced and its items will be cached until + they are used by the child iterables. This may require significant storage. + + By default, attempting to select a bucket to which no items belong will + exhaust the iterable and cache all values. + If you specify a *validator* function, selected buckets will instead be + checked against it. + + >>> from itertools import count + >>> it = count(1, 2) # Infinite sequence of odd numbers + >>> key = lambda x: x % 10 # Bucket by last digit + >>> validator = lambda x: x in {1, 3, 5, 7, 9} # Odd digits only + >>> s = bucket(it, key=key, validator=validator) + >>> 2 in s + False + >>> list(s[2]) + [] + + """ + + def __init__(self, iterable, key, validator=None): + self._it = iter(iterable) + self._key = key + self._cache = defaultdict(deque) + self._validator = validator or (lambda x: True) + + def __contains__(self, value): + if not self._validator(value): + return False + + try: + item = next(self[value]) + except StopIteration: + return False + else: + self._cache[value].appendleft(item) + + return True + + def _get_values(self, value): + """ + Helper to yield items from the parent iterator that match *value*. + Items that don't match are stored in the local cache as they + are encountered. + """ + while True: + # If we've cached some items that match the target value, emit + # the first one and evict it from the cache. + if self._cache[value]: + yield self._cache[value].popleft() + # Otherwise we need to advance the parent iterator to search for + # a matching item, caching the rest. + else: + while True: + try: + item = next(self._it) + except StopIteration: + return + item_value = self._key(item) + if item_value == value: + yield item + break + elif self._validator(item_value): + self._cache[item_value].append(item) + + def __iter__(self): + for item in self._it: + item_value = self._key(item) + if self._validator(item_value): + self._cache[item_value].append(item) + + yield from self._cache.keys() + + def __getitem__(self, value): + if not self._validator(value): + return iter(()) + + return self._get_values(value) diff --git a/newsfragments/489.feature.rst b/newsfragments/489.feature.rst new file mode 100644 index 00000000..f0e1b091 --- /dev/null +++ b/newsfragments/489.feature.rst @@ -0,0 +1 @@ +Prioritize valid dists to invalid dists when retrieving by name. \ No newline at end of file diff --git a/tests/test_main.py b/tests/test_main.py index a18c2d17..dc248492 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -140,7 +140,6 @@ def make_pkg(name, files=dict(METADATA="VERSION: 1.0")): f'{name}.dist-info': files, } - @__import__('pytest').mark.xfail(reason="#489") def test_valid_dists_preferred(self): """ Dists with metadata should be preferred when discovered by name. From 4bcda08ca2b1bf91437619d4f99810ecbf83b2b2 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 23 Jul 2024 09:03:10 -0400 Subject: [PATCH 363/480] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/489.feature.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/489.feature.rst diff --git a/NEWS.rst b/NEWS.rst index a49f5ec9..0f1b63a4 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v8.1.0 +====== + +Features +-------- + +- Prioritize valid dists to invalid dists when retrieving by name. (#489) + + v8.0.0 ====== diff --git a/newsfragments/489.feature.rst b/newsfragments/489.feature.rst deleted file mode 100644 index f0e1b091..00000000 --- a/newsfragments/489.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Prioritize valid dists to invalid dists when retrieving by name. \ No newline at end of file From d18ae21b54f32cfc813119545f3f2701468f8284 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 24 Jul 2024 11:15:52 -0400 Subject: [PATCH 364/480] Add SimplePath to importlib_metadata.__all__. Closes #494 --- importlib_metadata/__init__.py | 1 + newsfragments/494.feature.rst | 1 + 2 files changed, 2 insertions(+) create mode 100644 newsfragments/494.feature.rst diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index b9fc04f1..2c71d33c 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -39,6 +39,7 @@ 'DistributionFinder', 'PackageMetadata', 'PackageNotFoundError', + 'SimplePath', 'distribution', 'distributions', 'entry_points', diff --git a/newsfragments/494.feature.rst b/newsfragments/494.feature.rst new file mode 100644 index 00000000..d19e12e3 --- /dev/null +++ b/newsfragments/494.feature.rst @@ -0,0 +1 @@ +Add SimplePath to importlib_metadata.__all__. \ No newline at end of file From 3f6acea6081d28501bc76888c0676611a70fce10 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 24 Jul 2024 11:16:34 -0400 Subject: [PATCH 365/480] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/494.feature.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/494.feature.rst diff --git a/NEWS.rst b/NEWS.rst index 0f1b63a4..2e22a335 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v8.2.0 +====== + +Features +-------- + +- Add SimplePath to importlib_metadata.__all__. (#494) + + v8.1.0 ====== diff --git a/newsfragments/494.feature.rst b/newsfragments/494.feature.rst deleted file mode 100644 index d19e12e3..00000000 --- a/newsfragments/494.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Add SimplePath to importlib_metadata.__all__. \ No newline at end of file From 5ddd1228a1c96b803409159d2acd2eccb064ed48 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 25 Jul 2024 16:56:48 -0400 Subject: [PATCH 366/480] Expand the documentation of Distribution.locate_file to explain why 'locate_file' does what it does and what the consequences are of not implementing it. Ref pypa/pip#11684 --- importlib_metadata/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 2c71d33c..2eefb1d6 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -373,6 +373,17 @@ def locate_file(self, path: str | os.PathLike[str]) -> SimplePath: """ Given a path to a file in this distribution, return a SimplePath to it. + + This method is used by callers of ``Distribution.files()`` to + locate files within the distribution. If it's possible for a + Distribution to represent files in the distribution as + ``SimplePath`` objects, it should implement this method + to resolve such objects. + + Some Distribution providers may elect not to resolve SimplePath + objects within the distribution by raising a + NotImplementedError, but consumers of such a Distribution would + be unable to invoke ``Distribution.files()``. """ @classmethod From 875003a735fdd6334782250d15baa8142896a9a5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 1 Aug 2024 15:48:57 -0400 Subject: [PATCH 367/480] Disallow passing of 'dist' to EntryPoints.select. Closes python/cpython#107220. --- importlib_metadata/__init__.py | 17 +++++++++++++++++ newsfragments/+29a322e3.feature.rst | 1 + 2 files changed, 18 insertions(+) create mode 100644 newsfragments/+29a322e3.feature.rst diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 2eefb1d6..9a96d061 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -227,9 +227,26 @@ def matches(self, **params): >>> ep.matches(attr='bong') True """ + self._disallow_dist(params) attrs = (getattr(self, param) for param in params) return all(map(operator.eq, params.values(), attrs)) + @staticmethod + def _disallow_dist(params): + """ + Querying by dist is not allowed (dist objects are not comparable). + >>> EntryPoint(name='fan', value='fav', group='fag').matches(dist='foo') + Traceback (most recent call last): + ... + ValueError: "dist" is not suitable for matching... + """ + if "dist" in params: + raise ValueError( + '"dist" is not suitable for matching. ' + "Instead, use Distribution.entry_points.select() on a " + "located distribution." + ) + def _key(self): return self.name, self.value, self.group diff --git a/newsfragments/+29a322e3.feature.rst b/newsfragments/+29a322e3.feature.rst new file mode 100644 index 00000000..0e80b746 --- /dev/null +++ b/newsfragments/+29a322e3.feature.rst @@ -0,0 +1 @@ +Disallow passing of 'dist' to EntryPoints.select. \ No newline at end of file From 8e66fbc444727f75a54c055bfcc5504c49f6418c Mon Sep 17 00:00:00 2001 From: Daniel Hollas Date: Mon, 5 Aug 2024 19:40:35 +0100 Subject: [PATCH 368/480] Defer import inspect --- importlib_metadata/__init__.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 2eefb1d6..a8dead9b 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -8,7 +8,6 @@ import zipp import email import types -import inspect import pathlib import operator import textwrap @@ -1071,6 +1070,9 @@ def _topmost(name: PackagePath) -> Optional[str]: return top if rest else None +inspect = None + + def _get_toplevel_name(name: PackagePath) -> str: """ Infer a possibly importable module name from a name presumed on @@ -1089,11 +1091,14 @@ def _get_toplevel_name(name: PackagePath) -> str: >>> _get_toplevel_name(PackagePath('foo.dist-info')) 'foo.dist-info' """ - return _topmost(name) or ( - # python/typeshed#10328 - inspect.getmodulename(name) # type: ignore - or str(name) - ) + n = _topmost(name) + if n: + return n + + global inspect + if inspect is None: + import inspect + return inspect.getmodulename(name) or str(name) def _top_level_inferred(dist): From 06acfd262258d809242c74179477af324389e1c7 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Thu, 8 Aug 2024 23:14:35 +0200 Subject: [PATCH 369/480] Update to the latest ruff version (jaraco/skeleton#137) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5a4a7e91..ff54405e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.8 + rev: v0.5.6 hooks: - id: ruff - id: ruff-format From dd30b7600f33ce06a479a73002b950f4a3947759 Mon Sep 17 00:00:00 2001 From: Avasam Date: Thu, 8 Aug 2024 17:19:17 -0400 Subject: [PATCH 370/480] Add Protocols, remove @overload, from `.coveragerc` `exclude_also` (jaraco/skeleton#135) Co-authored-by: Jason R. Coombs --- .coveragerc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.coveragerc b/.coveragerc index 35b98b1d..2e3f4dd7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -8,6 +8,8 @@ disable_warnings = [report] show_missing = True exclude_also = - # jaraco/skeleton#97 - @overload + # Exclude common false positives per + # https://coverage.readthedocs.io/en/latest/excluding.html#advanced-exclusion + # Ref jaraco/skeleton#97 and jaraco/skeleton#135 + class .*\bProtocol\): if TYPE_CHECKING: From 3841656c61bad87f922fcba50445b503209b69c2 Mon Sep 17 00:00:00 2001 From: Avasam Date: Mon, 12 Aug 2024 12:13:19 -0400 Subject: [PATCH 371/480] Loosen restrictions on mypy (jaraco/skeleton#136) Based on changes downstream in setuptools. --- mypy.ini | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/mypy.ini b/mypy.ini index b6f97276..83b0d15c 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,5 +1,14 @@ [mypy] -ignore_missing_imports = True -# required to support namespace packages -# https://github.com/python/mypy/issues/14057 +# Is the project well-typed? +strict = False + +# Early opt-in even when strict = False +warn_unused_ignores = True +warn_redundant_casts = True +enable_error_code = ignore-without-code + +# Support namespace packages per https://github.com/python/mypy/issues/14057 explicit_package_bases = True + +# Disable overload-overlap due to many false-positives +disable_error_code = overload-overlap From 1a27fd5b8815e65571e6c028d6bef2c1daf61688 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 12 Aug 2024 12:16:15 -0400 Subject: [PATCH 372/480] Split the test dependencies into four classes (test, cover, type, check). (jaraco/skeleton#139) --- pyproject.toml | 25 ++++++++++++++++++++----- tox.ini | 4 ++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1307e1fa..31057d85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,14 +28,10 @@ Source = "https://github.com/PROJECT_PATH" test = [ # upstream "pytest >= 6, != 8.1.*", - "pytest-checkdocs >= 2.4", - "pytest-cov", - "pytest-mypy", - "pytest-enabler >= 2.2", - "pytest-ruff >= 0.2.1; sys_platform != 'cygwin'", # local ] + doc = [ # upstream "sphinx >= 3.5", @@ -47,4 +43,23 @@ doc = [ # local ] +check = [ + "pytest-checkdocs >= 2.4", + "pytest-ruff >= 0.2.1; sys_platform != 'cygwin'", +] + +cover = [ + "pytest-cov", +] + +enabler = [ + "pytest-enabler >= 2.2", +] + +type = [ + "pytest-mypy", +] + + + [tool.setuptools_scm] diff --git a/tox.ini b/tox.ini index cc4db36e..01f0975f 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,10 @@ commands = usedevelop = True extras = test + check + cover + enabler + type [testenv:diffcov] description = run tests and check that diff from main is covered From 6d9b766099dbac1c97a220badde7e14304e03291 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 19 Aug 2024 13:47:26 -0400 Subject: [PATCH 373/480] Remove MetadataPathFinder regardless of its position. Closes #500 --- conftest.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/conftest.py b/conftest.py index 779ac24b..762e66f3 100644 --- a/conftest.py +++ b/conftest.py @@ -13,13 +13,18 @@ def pytest_configure(): def remove_importlib_metadata(): """ - Because pytest imports importlib_metadata, the coverage - reports are broken (#322). So work around the issue by - undoing the changes made by pytest's import of - importlib_metadata (if any). + Ensure importlib_metadata is not imported yet. + + Because pytest or other modules might import + importlib_metadata, the coverage reports are broken (#322). + Work around the issue by undoing the changes made by a + previous import of importlib_metadata (if any). """ - if sys.meta_path[-1].__class__.__name__ == 'MetadataPathFinder': - del sys.meta_path[-1] + sys.meta_path[:] = [ + item + for item in sys.meta_path + if item.__class__.__name__ != 'MetadataPathFinder' + ] for mod in list(sys.modules): if mod.startswith('importlib_metadata'): del sys.modules[mod] From 3c8e1ec4e34c11dcff086be7fbd0d1981bf32480 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 19 Aug 2024 15:35:22 -0400 Subject: [PATCH 374/480] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/+29a322e3.feature.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/+29a322e3.feature.rst diff --git a/NEWS.rst b/NEWS.rst index 2e22a335..018825be 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v8.3.0 +====== + +Features +-------- + +- Disallow passing of 'dist' to EntryPoints.select. + + v8.2.0 ====== diff --git a/newsfragments/+29a322e3.feature.rst b/newsfragments/+29a322e3.feature.rst deleted file mode 100644 index 0e80b746..00000000 --- a/newsfragments/+29a322e3.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Disallow passing of 'dist' to EntryPoints.select. \ No newline at end of file From debb5165a88b1a4433150b265e155c21b497d154 Mon Sep 17 00:00:00 2001 From: Daniel Hollas Date: Tue, 20 Aug 2024 12:23:17 +0100 Subject: [PATCH 375/480] Don't use global var - wallrusify - add a note about deffered import --- importlib_metadata/__init__.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index a8dead9b..f76ef2cc 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -1070,9 +1070,6 @@ def _topmost(name: PackagePath) -> Optional[str]: return top if rest else None -inspect = None - - def _get_toplevel_name(name: PackagePath) -> str: """ Infer a possibly importable module name from a name presumed on @@ -1091,13 +1088,12 @@ def _get_toplevel_name(name: PackagePath) -> str: >>> _get_toplevel_name(PackagePath('foo.dist-info')) 'foo.dist-info' """ - n = _topmost(name) - if n: + if n := _topmost(name): return n - global inspect - if inspect is None: - import inspect + # We're deffering import of inspect to speed up overall import time + import inspect + return inspect.getmodulename(name) or str(name) From e99c10510d48e840b0550bd05d1167633dcfaea7 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 20 Aug 2024 13:00:48 -0400 Subject: [PATCH 376/480] Restore single-expression logic. --- importlib_metadata/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index f76ef2cc..b1a15350 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -1088,13 +1088,14 @@ def _get_toplevel_name(name: PackagePath) -> str: >>> _get_toplevel_name(PackagePath('foo.dist-info')) 'foo.dist-info' """ - if n := _topmost(name): - return n - # We're deffering import of inspect to speed up overall import time import inspect - return inspect.getmodulename(name) or str(name) + return _topmost(name) or ( + # python/typeshed#10328 + inspect.getmodulename(name) # type: ignore + or str(name) + ) def _top_level_inferred(dist): From a7aaf72702b3a49ea3e33c9cf7f223839067c883 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 20 Aug 2024 13:01:47 -0400 Subject: [PATCH 377/480] Use third-person imperative voice and link to issue in comment. --- 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 b1a15350..c2b76dd2 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -1088,7 +1088,7 @@ def _get_toplevel_name(name: PackagePath) -> str: >>> _get_toplevel_name(PackagePath('foo.dist-info')) 'foo.dist-info' """ - # We're deffering import of inspect to speed up overall import time + # Defer import of inspect for performance (python/cpython#118761) import inspect return _topmost(name) or ( From ebcdcfdd18d427498f11b74e245b3f8a7ef5df9c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 20 Aug 2024 13:04:10 -0400 Subject: [PATCH 378/480] Remove workaround for python/typeshed#10328. --- importlib_metadata/__init__.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 72670f44..24587e68 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -1108,11 +1108,7 @@ def _get_toplevel_name(name: PackagePath) -> str: # Defer import of inspect for performance (python/cpython#118761) import inspect - return _topmost(name) or ( - # python/typeshed#10328 - inspect.getmodulename(name) # type: ignore - or str(name) - ) + return _topmost(name) or (inspect.getmodulename(name) or str(name)) def _top_level_inferred(dist): From 71b467843258873048eb944545ba1235866523e6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 20 Aug 2024 13:05:25 -0400 Subject: [PATCH 379/480] Add news fragment. --- newsfragments/499.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/499.feature.rst diff --git a/newsfragments/499.feature.rst b/newsfragments/499.feature.rst new file mode 100644 index 00000000..1aa347ed --- /dev/null +++ b/newsfragments/499.feature.rst @@ -0,0 +1 @@ +Deferred import of inspect for import performance. \ No newline at end of file From 1616cb3a82c33c3603ff984b6ff417e68068aa6e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 20 Aug 2024 13:06:05 -0400 Subject: [PATCH 380/480] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/499.feature.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/499.feature.rst diff --git a/NEWS.rst b/NEWS.rst index 018825be..f4a37fdf 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v8.4.0 +====== + +Features +-------- + +- Deferred import of inspect for import performance. (#499) + + v8.3.0 ====== diff --git a/newsfragments/499.feature.rst b/newsfragments/499.feature.rst deleted file mode 100644 index 1aa347ed..00000000 --- a/newsfragments/499.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Deferred import of inspect for import performance. \ No newline at end of file From 46c6127405cba0cf19b96ce6e533c28890eb1ea3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 20 Aug 2024 13:39:18 -0400 Subject: [PATCH 381/480] =?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 --- conftest.py | 1 - exercises.py | 4 +++- importlib_metadata/__init__.py | 34 ++++++++++++++++---------------- importlib_metadata/_adapters.py | 2 +- importlib_metadata/_compat.py | 3 +-- importlib_metadata/_functools.py | 2 +- importlib_metadata/_meta.py | 14 ++++++++++--- tests/_path.py | 3 +-- tests/compat/py39.py | 1 - tests/compat/test_py39_compat.py | 5 +++-- tests/fixtures.py | 14 ++++++------- tests/test_api.py | 5 +++-- tests/test_integration.py | 4 +++- tests/test_main.py | 13 ++++++------ tests/test_zip.py | 3 ++- 15 files changed, 59 insertions(+), 49 deletions(-) diff --git a/conftest.py b/conftest.py index 762e66f3..6d3402d6 100644 --- a/conftest.py +++ b/conftest.py @@ -1,6 +1,5 @@ import sys - collect_ignore = [ # this module fails mypy tests because 'setup.py' matches './setup.py' 'tests/data/sources/example/setup.py', diff --git a/exercises.py b/exercises.py index c88fa983..adccf03c 100644 --- a/exercises.py +++ b/exercises.py @@ -29,6 +29,7 @@ def cached_distribution_perf(): def uncached_distribution_perf(): "uncached distribution" import importlib + import importlib_metadata # end warmup @@ -37,9 +38,10 @@ def uncached_distribution_perf(): def entrypoint_regexp_perf(): - import importlib_metadata import re + import importlib_metadata + input = '0' + ' ' * 2**10 + '0' # end warmup re.match(importlib_metadata.EntryPoint.pattern, input) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 24587e68..84d16508 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -1,23 +1,28 @@ from __future__ import annotations -import os -import re import abc -import sys -import json -import zipp +import collections import email -import types -import pathlib -import operator -import textwrap import functools import itertools +import json +import operator +import os +import pathlib import posixpath -import collections +import re +import sys +import textwrap +import types +from contextlib import suppress +from importlib import import_module +from importlib.abc import MetaPathFinder +from itertools import starmap +from typing import Any, Iterable, List, Mapping, Match, Optional, Set, cast + +import zipp from . import _meta -from .compat import py39, py311 from ._collections import FreezableDefaultDict, Pair from ._compat import ( NullFinder, @@ -26,12 +31,7 @@ from ._functools import method_cache, pass_none from ._itertools import always_iterable, bucket, unique_everseen from ._meta import PackageMetadata, SimplePath - -from contextlib import suppress -from importlib import import_module -from importlib.abc import MetaPathFinder -from itertools import starmap -from typing import Any, Iterable, List, Mapping, Match, Optional, Set, cast +from .compat import py39, py311 __all__ = [ 'Distribution', diff --git a/importlib_metadata/_adapters.py b/importlib_metadata/_adapters.py index 6223263e..3b516a2d 100644 --- a/importlib_metadata/_adapters.py +++ b/importlib_metadata/_adapters.py @@ -1,6 +1,6 @@ +import email.message import re import textwrap -import email.message from ._text import FoldedCase diff --git a/importlib_metadata/_compat.py b/importlib_metadata/_compat.py index df312b1c..01356d69 100644 --- a/importlib_metadata/_compat.py +++ b/importlib_metadata/_compat.py @@ -1,6 +1,5 @@ -import sys import platform - +import sys __all__ = ['install', 'NullFinder'] diff --git a/importlib_metadata/_functools.py b/importlib_metadata/_functools.py index 71f66bd0..5dda6a21 100644 --- a/importlib_metadata/_functools.py +++ b/importlib_metadata/_functools.py @@ -1,5 +1,5 @@ -import types import functools +import types # from jaraco.functools 3.3 diff --git a/importlib_metadata/_meta.py b/importlib_metadata/_meta.py index 1927d0f6..0942bbd9 100644 --- a/importlib_metadata/_meta.py +++ b/importlib_metadata/_meta.py @@ -1,9 +1,17 @@ from __future__ import annotations import os -from typing import Protocol -from typing import Any, Dict, Iterator, List, Optional, TypeVar, Union, overload - +from typing import ( + Any, + Dict, + Iterator, + List, + Optional, + Protocol, + TypeVar, + Union, + overload, +) _T = TypeVar("_T") diff --git a/tests/_path.py b/tests/_path.py index b3cfb9cd..7e4f7ee0 100644 --- a/tests/_path.py +++ b/tests/_path.py @@ -2,8 +2,7 @@ import functools import pathlib -from typing import Dict, Protocol, Union -from typing import runtime_checkable +from typing import Dict, Protocol, Union, runtime_checkable class Symlink(str): diff --git a/tests/compat/py39.py b/tests/compat/py39.py index 9476eb35..4e45d7cc 100644 --- a/tests/compat/py39.py +++ b/tests/compat/py39.py @@ -1,6 +1,5 @@ from jaraco.test.cpython import from_test_support, try_import - os_helper = try_import('os_helper') or from_test_support( 'FS_NONASCII', 'skip_unless_symlink', 'temp_dir' ) diff --git a/tests/compat/test_py39_compat.py b/tests/compat/test_py39_compat.py index 549e518a..db9fb1b7 100644 --- a/tests/compat/test_py39_compat.py +++ b/tests/compat/test_py39_compat.py @@ -1,8 +1,7 @@ -import sys import pathlib +import sys import unittest -from .. import fixtures from importlib_metadata import ( distribution, distributions, @@ -11,6 +10,8 @@ version, ) +from .. import fixtures + class OldStdlibFinderTests(fixtures.DistInfoPkgOffPath, unittest.TestCase): def setUp(self): diff --git a/tests/fixtures.py b/tests/fixtures.py index 187f1705..410e6f17 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -1,18 +1,16 @@ -import sys +import contextlib import copy +import functools import json -import shutil import pathlib +import shutil +import sys import textwrap -import functools -import contextlib - -from .compat.py312 import import_helper -from .compat.py39 import os_helper from . import _path from ._path import FilesSpec - +from .compat.py39 import os_helper +from .compat.py312 import import_helper try: from importlib import resources # type: ignore diff --git a/tests/test_api.py b/tests/test_api.py index 7ce0cd64..c36f93e0 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,9 +1,8 @@ +import importlib import re import textwrap import unittest -import importlib -from . import fixtures from importlib_metadata import ( Distribution, PackageNotFoundError, @@ -15,6 +14,8 @@ version, ) +from . import fixtures + class APITests( fixtures.EggInfoPkg, diff --git a/tests/test_integration.py b/tests/test_integration.py index f7af67f3..9bb3e793 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -8,15 +8,17 @@ """ import unittest + import packaging.requirements import packaging.version -from . import fixtures from importlib_metadata import ( _compat, version, ) +from . import fixtures + class IntegrationTests(fixtures.DistInfoPkg, unittest.TestCase): def test_package_spec_installed(self): diff --git a/tests/test_main.py b/tests/test_main.py index dc248492..7c9851fc 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,14 +1,11 @@ -import re +import importlib import pickle +import re import unittest -import importlib -import importlib_metadata -from .compat.py39 import os_helper import pyfakefs.fake_filesystem_unittest as ffs -from . import fixtures -from ._path import Symlink +import importlib_metadata from importlib_metadata import ( Distribution, EntryPoint, @@ -21,6 +18,10 @@ version, ) +from . import fixtures +from ._path import Symlink +from .compat.py39 import os_helper + class BasicTests(fixtures.DistInfoPkg, unittest.TestCase): version_pattern = r'\d+\.\d+(\.\d)?' diff --git a/tests/test_zip.py b/tests/test_zip.py index 01aba6df..d4f8e2f0 100644 --- a/tests/test_zip.py +++ b/tests/test_zip.py @@ -1,7 +1,6 @@ import sys import unittest -from . import fixtures from importlib_metadata import ( PackageNotFoundError, distribution, @@ -11,6 +10,8 @@ version, ) +from . import fixtures + class TestZip(fixtures.ZipFixtures, unittest.TestCase): def setUp(self): From d968f6270d55f27a10491344a22e9e0fd77b5583 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 20 Aug 2024 13:40:57 -0400 Subject: [PATCH 382/480] Remove superfluous parentheses. --- 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 84d16508..f6477999 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -1108,7 +1108,7 @@ def _get_toplevel_name(name: PackagePath) -> str: # Defer import of inspect for performance (python/cpython#118761) import inspect - return _topmost(name) or (inspect.getmodulename(name) or str(name)) + return _topmost(name) or inspect.getmodulename(name) or str(name) def _top_level_inferred(dist): From 56b61b3dd90df2dba2da445a8386029b54fdebf3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 20 Aug 2024 13:42:29 -0400 Subject: [PATCH 383/480] Rely on zipp overlay for zipfile.Path. --- importlib_metadata/__init__.py | 4 ++-- newsfragments/+d7832466.feature.rst | 1 + pyproject.toml | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 newsfragments/+d7832466.feature.rst diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index f6477999..b75befa3 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -20,7 +20,7 @@ from itertools import starmap from typing import Any, Iterable, List, Mapping, Match, Optional, Set, cast -import zipp +from zipp.compat.overlay import zipfile from . import _meta from ._collections import FreezableDefaultDict, Pair @@ -768,7 +768,7 @@ def children(self): return [] def zip_children(self): - zip_path = zipp.Path(self.root) + zip_path = zipfile.Path(self.root) names = zip_path.root.namelist() self.joinpath = zip_path.joinpath diff --git a/newsfragments/+d7832466.feature.rst b/newsfragments/+d7832466.feature.rst new file mode 100644 index 00000000..3f3f3162 --- /dev/null +++ b/newsfragments/+d7832466.feature.rst @@ -0,0 +1 @@ +Rely on zipp overlay for zipfile.Path. \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 24ce25e3..8c45cc9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,7 +18,7 @@ classifiers = [ ] requires-python = ">=3.8" dependencies = [ - "zipp>=0.5", + "zipp>=3.20", 'typing-extensions>=3.6.4; python_version < "3.8"', ] dynamic = ["version"] From f1350e413775a9e79e20779cc9705e28a1c55900 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 21 Aug 2024 07:05:32 -0400 Subject: [PATCH 384/480] Add upstream and local sections for 'type' extra, since many projects will have 'types-*' dependencies. --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 31057d85..3866a323 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,10 @@ enabler = [ ] type = [ + # upstream "pytest-mypy", + + # local ] From 6c0b7b37d94650ab5c59464615f56e3720cd3d56 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 21 Aug 2024 18:30:07 -0400 Subject: [PATCH 385/480] =?UTF-8?q?=F0=9F=A7=8E=E2=80=8D=E2=99=80=EF=B8=8F?= =?UTF-8?q?=20Genuflect=20to=20the=20types.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ref jaraco/pytest-perf#16 --- mypy.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mypy.ini b/mypy.ini index 83b0d15c..8e00827c 100644 --- a/mypy.ini +++ b/mypy.ini @@ -12,3 +12,7 @@ explicit_package_bases = True # Disable overload-overlap due to many false-positives disable_error_code = overload-overlap + +# jaraco/pytest-perf#16 +[mypy-pytest_perf.*] +ignore_missing_imports = True From 4551c19215511b192fb3e5253ed9b05d7aa6c821 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 21 Aug 2024 19:20:02 -0400 Subject: [PATCH 386/480] =?UTF-8?q?=F0=9F=A7=8E=E2=80=8D=E2=99=80=EF=B8=8F?= =?UTF-8?q?=20Genuflect=20to=20the=20types.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ref jaraco/zipp#123 --- mypy.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mypy.ini b/mypy.ini index 8e00827c..9f476547 100644 --- a/mypy.ini +++ b/mypy.ini @@ -16,3 +16,7 @@ disable_error_code = overload-overlap # jaraco/pytest-perf#16 [mypy-pytest_perf.*] ignore_missing_imports = True + +# jaraco/zipp#123 +[mypy-zipp.*] +ignore_missing_imports = True From d4aa5054a9818aeaa73174fd69ca21c0bb6a3fad Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 21 Aug 2024 19:26:42 -0400 Subject: [PATCH 387/480] =?UTF-8?q?=F0=9F=A7=8E=E2=80=8D=E2=99=80=EF=B8=8F?= =?UTF-8?q?=20Genuflect=20to=20the=20types.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- importlib_metadata/__init__.py | 4 ++-- tests/fixtures.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index b75befa3..3f1d942e 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -331,7 +331,7 @@ class PackagePath(pathlib.PurePosixPath): size: int dist: Distribution - def read_text(self, encoding: str = 'utf-8') -> str: # type: ignore[override] + def read_text(self, encoding: str = 'utf-8') -> str: return self.locate().read_text(encoding=encoding) def read_binary(self) -> bytes: @@ -750,7 +750,7 @@ class FastPath: True """ - @functools.lru_cache() # type: ignore + @functools.lru_cache() # type: ignore[misc] def __new__(cls, root): return super().__new__(cls) diff --git a/tests/fixtures.py b/tests/fixtures.py index 410e6f17..6708a063 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -13,12 +13,12 @@ from .compat.py312 import import_helper try: - from importlib import resources # type: ignore + from importlib import resources getattr(resources, 'files') getattr(resources, 'as_file') except (ImportError, AttributeError): - import importlib_resources as resources # type: ignore + import importlib_resources as resources # type: ignore[import-not-found, no-redef] @contextlib.contextmanager From 8484eccd39bda1ead5d8fec8cd967004f640a81f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 21 Aug 2024 19:27:40 -0400 Subject: [PATCH 388/480] =?UTF-8?q?=F0=9F=A7=8E=E2=80=8D=E2=99=80=EF=B8=8F?= =?UTF-8?q?=20Genuflect=20to=20the=20types.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-sync jaraco.path 3.7.1. --- tests/_path.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/_path.py b/tests/_path.py index 7e4f7ee0..81ab76ac 100644 --- a/tests/_path.py +++ b/tests/_path.py @@ -1,4 +1,4 @@ -# from jaraco.path 3.7 +# from jaraco.path 3.7.1 import functools import pathlib @@ -11,7 +11,7 @@ class Symlink(str): """ -FilesSpec = Dict[str, Union[str, bytes, Symlink, 'FilesSpec']] # type: ignore +FilesSpec = Dict[str, Union[str, bytes, Symlink, 'FilesSpec']] @runtime_checkable @@ -28,12 +28,12 @@ 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 + return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore[return-value] def build( spec: FilesSpec, - prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore + prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore[assignment] ): """ Build a set of files/directories, as described by the spec. @@ -67,7 +67,7 @@ def build( @functools.singledispatch def create(content: Union[str, bytes, FilesSpec], path): path.mkdir(exist_ok=True) - build(content, prefix=path) # type: ignore + build(content, prefix=path) # type: ignore[arg-type] @create.register From 0666d9da54886a68488d93a9907943e95fa2d1d4 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 21 Aug 2024 19:30:53 -0400 Subject: [PATCH 389/480] =?UTF-8?q?=F0=9F=A7=8E=E2=80=8D=E2=99=80=EF=B8=8F?= =?UTF-8?q?=20Genuflect=20to=20the=20types.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ref jaraco/jaraco.test#7 --- mypy.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mypy.ini b/mypy.ini index 9f476547..786d03f4 100644 --- a/mypy.ini +++ b/mypy.ini @@ -20,3 +20,7 @@ ignore_missing_imports = True # jaraco/zipp#123 [mypy-zipp.*] ignore_missing_imports = True + +# jaraco/jaraco.test#7 +[mypy-jaraco.test.*] +ignore_missing_imports = True From 6f8cc1ef4bf403e8c01e90230b6b2c6fc532179c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 21 Aug 2024 19:37:14 -0400 Subject: [PATCH 390/480] =?UTF-8?q?=F0=9F=A7=8E=E2=80=8D=E2=99=80=EF=B8=8F?= =?UTF-8?q?=20Genuflect=20to=20the=20types.?= 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 6708a063..7c9740d9 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -18,7 +18,7 @@ getattr(resources, 'files') getattr(resources, 'as_file') except (ImportError, AttributeError): - import importlib_resources as resources # type: ignore[import-not-found, no-redef] + import importlib_resources as resources # type: ignore[import-not-found, no-redef, unused-ignore] @contextlib.contextmanager From a55f01c752984538435f4baea1efc3ac58b5f006 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 22 Aug 2024 09:42:05 -0400 Subject: [PATCH 391/480] Add reference to development methodology. Ref python/cpython#123144 --- importlib_metadata/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 3f1d942e..6dfaad0c 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -1,3 +1,12 @@ +""" +APIs exposing metadata from third-party Python packages. + +This codebase is shared between importlib.metadata in the stdlib +and importlib_metadata in PyPI. See +https://github.com/python/importlib_metadata/wiki/Development-Methodology +for more detail. +""" + from __future__ import annotations import abc From 57b8aa81d77416805dcaaa22d5d45fef3e8b331c Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sun, 25 Aug 2024 09:29:10 +0100 Subject: [PATCH 392/480] Add `--fix` flag to ruff pre-commit hook for automatic suggestion of fixes (jaraco/skeleton#140) * Add `--fix` flag to ruff pre-commit hook for automatic suggestion of fixes. This is documented in https://github.com/astral-sh/ruff-pre-commit?tab=readme-ov-file#using-ruff-with-pre-commit and should be safe to apply, because it requires the developer to "manually approve" the suggested changes via `git add`. * Add --unsafe-fixes to ruff pre-commit hoot --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ff54405e..8ec58e22 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,4 +3,5 @@ repos: rev: v0.5.6 hooks: - id: ruff + args: [--fix, --unsafe-fixes] - id: ruff-format From d3e83beaec3bdf4a628f2f0ae0a52d21c84e346f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Aug 2024 06:33:23 -0400 Subject: [PATCH 393/480] Disable mypy for now. Ref jaraco/skeleton#143 --- pyproject.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 3866a323..1d81b1cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,5 +64,8 @@ type = [ ] - [tool.setuptools_scm] + + +[tool.pytest-enabler.mypy] +# Disabled due to jaraco/skeleton#143 From 3fcabf10b810c8585b858fb81fc3cd8c5efe898d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 25 Aug 2024 13:26:38 -0400 Subject: [PATCH 394/480] Move overload-overlap disablement to its own line for easier diffs and simpler relevant comments. Ref #142 --- mypy.ini | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mypy.ini b/mypy.ini index 83b0d15c..2806c330 100644 --- a/mypy.ini +++ b/mypy.ini @@ -10,5 +10,6 @@ enable_error_code = ignore-without-code # Support namespace packages per https://github.com/python/mypy/issues/14057 explicit_package_bases = True -# Disable overload-overlap due to many false-positives -disable_error_code = overload-overlap +disable_error_code = + # Disable due to many false positives + overload-overlap From 04c69839d11588f9f6b1e7bfb868d53f48ee52bd Mon Sep 17 00:00:00 2001 From: Avasam Date: Mon, 26 Aug 2024 16:53:06 -0400 Subject: [PATCH 395/480] Pass mypy and link issues --- importlib_metadata/__init__.py | 4 ++-- pyproject.toml | 4 ---- tests/_path.py | 7 ++++--- tests/fixtures.py | 9 +++------ 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 6dfaad0c..9d8fb980 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -66,7 +66,7 @@ def __str__(self) -> str: return f"No package metadata was found for {self.name}" @property - def name(self) -> str: # type: ignore[override] + def name(self) -> str: # type: ignore[override] # make readonly (name,) = self.args return name @@ -284,7 +284,7 @@ class EntryPoints(tuple): __slots__ = () - def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override] + def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override] # Work with str instead of int """ Get the EntryPoint in self matching name. """ diff --git a/pyproject.toml b/pyproject.toml index c00be7ed..2e5e40c4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,7 +77,3 @@ type = [ [tool.setuptools_scm] - - -[tool.pytest-enabler.mypy] -# Disabled due to jaraco/skeleton#143 diff --git a/tests/_path.py b/tests/_path.py index 81ab76ac..30a6c15d 100644 --- a/tests/_path.py +++ b/tests/_path.py @@ -28,12 +28,12 @@ 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[return-value] + return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore[return-value] # jaraco/jaraco.path#4 def build( spec: FilesSpec, - prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore[assignment] + prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore[assignment] # jaraco/jaraco.path#4 ): """ Build a set of files/directories, as described by the spec. @@ -67,7 +67,8 @@ def build( @functools.singledispatch def create(content: Union[str, bytes, FilesSpec], path): path.mkdir(exist_ok=True) - build(content, prefix=path) # type: ignore[arg-type] + # Mypy only looks at the signature of the main singledispatch method. So it must contain the complete Union + build(content, prefix=path) # type: ignore[arg-type] # python/mypy#11727 @create.register diff --git a/tests/fixtures.py b/tests/fixtures.py index 7c9740d9..8e692f86 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -12,13 +12,10 @@ from .compat.py39 import os_helper from .compat.py312 import import_helper -try: +if sys.version_info >= (3, 9): from importlib import resources - - getattr(resources, 'files') - getattr(resources, 'as_file') -except (ImportError, AttributeError): - import importlib_resources as resources # type: ignore[import-not-found, no-redef, unused-ignore] +else: + import importlib_resources as resources @contextlib.contextmanager From 76eebe0eb4632d7658f4fdaac119211313684f8b Mon Sep 17 00:00:00 2001 From: Daniel Hollas Date: Tue, 27 Aug 2024 15:31:57 +0100 Subject: [PATCH 396/480] Defer import of zipp --- 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 6dfaad0c..e9af4972 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -29,8 +29,6 @@ from itertools import starmap from typing import Any, Iterable, List, Mapping, Match, Optional, Set, cast -from zipp.compat.overlay import zipfile - from . import _meta from ._collections import FreezableDefaultDict, Pair from ._compat import ( @@ -777,6 +775,8 @@ def children(self): return [] def zip_children(self): + from zipp.compat.overlay import zipfile + zip_path = zipfile.Path(self.root) names = zip_path.root.namelist() self.joinpath = zip_path.joinpath From e0b4f09ed38845178d1bc5a2d8cb3d13ba69740f Mon Sep 17 00:00:00 2001 From: Daniel Hollas Date: Tue, 27 Aug 2024 15:49:58 +0100 Subject: [PATCH 397/480] Defer json import --- importlib_metadata/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index e9af4972..d1b0d92d 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -14,7 +14,6 @@ import email import functools import itertools -import json import operator import os import pathlib @@ -673,6 +672,8 @@ def origin(self): return self._load_json('direct_url.json') def _load_json(self, filename): + import json + return pass_none(json.loads)( self.read_text(filename), object_hook=lambda data: types.SimpleNamespace(**data), From 8436e8142685cc5378ecaea3311a6172ec8e34c0 Mon Sep 17 00:00:00 2001 From: Daniel Hollas Date: Tue, 27 Aug 2024 16:04:41 +0100 Subject: [PATCH 398/480] Defer platform import --- importlib_metadata/_compat.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/importlib_metadata/_compat.py b/importlib_metadata/_compat.py index 01356d69..e78ff59d 100644 --- a/importlib_metadata/_compat.py +++ b/importlib_metadata/_compat.py @@ -1,4 +1,3 @@ -import platform import sys __all__ = ['install', 'NullFinder'] @@ -52,5 +51,7 @@ def pypy_partial(val): Workaround for #327. """ + import platform + is_pypy = platform.python_implementation() == 'PyPy' return val + is_pypy From 0c326f3f77b2420163f73d97f8fbd090fa49147d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 29 Aug 2024 13:13:06 -0400 Subject: [PATCH 399/480] Add a degenerate nitpick_ignore for downstream consumers. Add a 'local' comment to delineate where the skeleton ends and the downstream begins. --- docs/conf.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 32150488..3d956a8c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,3 +1,6 @@ +from __future__ import annotations + + extensions = [ 'sphinx.ext.autodoc', 'jaraco.packaging.sphinx', @@ -30,6 +33,7 @@ # Be strict about any broken references nitpicky = True +nitpick_ignore: list[tuple[str, str]] = [] # Include Python intersphinx mapping to prevent failures # jaraco/skeleton#51 @@ -40,3 +44,5 @@ # Preserve authored syntax for defaults autodoc_preserve_defaults = True + +# local From cae15af2d0c34fca473bf5425c23da378d1b0419 Mon Sep 17 00:00:00 2001 From: Avasam Date: Thu, 29 Aug 2024 15:25:16 -0400 Subject: [PATCH 400/480] Update tests/_path.py with jaraco.path 3.7.2 --- tests/_path.py | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/tests/_path.py b/tests/_path.py index 81ab76ac..c66cf5f8 100644 --- a/tests/_path.py +++ b/tests/_path.py @@ -1,8 +1,13 @@ -# from jaraco.path 3.7.1 +# from jaraco.path 3.7.2 + +from __future__ import annotations import functools import pathlib -from typing import Dict, Protocol, Union, runtime_checkable +from typing import TYPE_CHECKING, Mapping, Protocol, Union, runtime_checkable + +if TYPE_CHECKING: + from typing_extensions import Self class Symlink(str): @@ -11,29 +16,25 @@ class Symlink(str): """ -FilesSpec = Dict[str, Union[str, bytes, Symlink, 'FilesSpec']] +FilesSpec = Mapping[str, Union[str, bytes, Symlink, 'FilesSpec']] @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 symlink_to(self, target): ... # pragma: no cover + def __truediv__(self, other, /) -> Self: ... + def mkdir(self, *, exist_ok) -> object: ... + def write_text(self, content, /, *, encoding) -> object: ... + def write_bytes(self, content, /) -> object: ... + def symlink_to(self, target, /) -> object: ... -def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker: - return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore[return-value] +def _ensure_tree_maker(obj: str | TreeMaker) -> TreeMaker: + return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) def build( spec: FilesSpec, - prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore[assignment] + prefix: str | TreeMaker = pathlib.Path(), ): """ Build a set of files/directories, as described by the spec. @@ -65,23 +66,24 @@ def build( @functools.singledispatch -def create(content: Union[str, bytes, FilesSpec], path): +def create(content: str | bytes | FilesSpec, path: TreeMaker) -> None: path.mkdir(exist_ok=True) - build(content, prefix=path) # type: ignore[arg-type] + # Mypy only looks at the signature of the main singledispatch method. So it must contain the complete Union + build(content, prefix=path) # type: ignore[arg-type] # python/mypy#11727 @create.register -def _(content: bytes, path): +def _(content: bytes, path: TreeMaker) -> None: path.write_bytes(content) @create.register -def _(content: str, path): +def _(content: str, path: TreeMaker) -> None: path.write_text(content, encoding='utf-8') @create.register -def _(content: Symlink, path): +def _(content: Symlink, path: TreeMaker) -> None: path.symlink_to(content) From 2beb8b0c9d0f7046370e7c58c4e6baaf35154a16 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 29 Aug 2024 16:26:28 -0400 Subject: [PATCH 401/480] Add support for linking usernames. Closes jaraco/skeleton#144 --- docs/conf.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index 3d956a8c..d5745d62 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -45,4 +45,13 @@ # Preserve authored syntax for defaults autodoc_preserve_defaults = True +# Add support for linking usernames, PyPI projects, Wikipedia pages +github_url = 'https://github.com/' +extlinks = { + 'user': (f'{github_url}%s', '@%s'), + 'pypi': ('https://pypi.org/project/%s', '%s'), + 'wiki': ('https://wikipedia.org/wiki/%s', '%s'), +} +extensions += ['sphinx.ext.extlinks'] + # local From 790fa6e6feb9a93d39135494819b12e9df8a7bba Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 29 Aug 2024 16:53:52 -0400 Subject: [PATCH 402/480] Include the trailing slash in disable_error_code(overload-overlap), also required for clean diffs. Ref jaraco/skeleton#142 --- mypy.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy.ini b/mypy.ini index 2806c330..efcb8cbc 100644 --- a/mypy.ini +++ b/mypy.ini @@ -12,4 +12,4 @@ explicit_package_bases = True disable_error_code = # Disable due to many false positives - overload-overlap + overload-overlap, From 1a6e38c0bfccd18a01deaca1491bcde3e778404c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 31 Aug 2024 05:50:38 -0400 Subject: [PATCH 403/480] Remove workaround for sphinx-contrib/sphinx-lint#83 --- tox.ini | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 01f0975f..14243051 100644 --- a/tox.ini +++ b/tox.ini @@ -31,9 +31,7 @@ extras = changedir = docs commands = python -m sphinx -W --keep-going . {toxinidir}/build/html - python -m sphinxlint \ - # workaround for sphinx-contrib/sphinx-lint#83 - --jobs 1 + python -m sphinxlint [testenv:finalize] description = assemble changelog and tag a release From a675458e1a7d6ae81d0d441338a74dc98ffc5a61 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 7 Sep 2024 10:16:01 -0400 Subject: [PATCH 404/480] Allow the workflow to be triggered manually. --- .github/workflows/main.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ac0ff69e..441b93ef 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,6 +10,7 @@ on: # required if branches-ignore is supplied (jaraco/skeleton#103) - '**' pull_request: + workflow_dispatch: permissions: contents: read From d11b67fed9f21503ca369e33c917a8038994ce0b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 09:58:23 -0400 Subject: [PATCH 405/480] Add comment to protect the deferred import. --- importlib_metadata/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index e9af4972..9b912f8e 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -775,6 +775,7 @@ def children(self): return [] def zip_children(self): + # deferred for performance (python/importlib_metadata#502) from zipp.compat.overlay import zipfile zip_path = zipfile.Path(self.root) From e3ce33b45e572824b482049570cac13da543999b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 09:59:03 -0400 Subject: [PATCH 406/480] Add news fragment. --- newsfragments/502.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/502.feature.rst diff --git a/newsfragments/502.feature.rst b/newsfragments/502.feature.rst new file mode 100644 index 00000000..20659bb8 --- /dev/null +++ b/newsfragments/502.feature.rst @@ -0,0 +1 @@ +Deferred import of zipfile.Path \ No newline at end of file From 18eb2da0ee267394c1735bec5b1d9f2b0fa77dd9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 10:13:51 -0400 Subject: [PATCH 407/480] Revert "Defer platform import" This reverts commit 8436e8142685cc5378ecaea3311a6172ec8e34c0. --- importlib_metadata/_compat.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/importlib_metadata/_compat.py b/importlib_metadata/_compat.py index e78ff59d..01356d69 100644 --- a/importlib_metadata/_compat.py +++ b/importlib_metadata/_compat.py @@ -1,3 +1,4 @@ +import platform import sys __all__ = ['install', 'NullFinder'] @@ -51,7 +52,5 @@ def pypy_partial(val): Workaround for #327. """ - import platform - is_pypy = platform.python_implementation() == 'PyPy' return val + is_pypy From 3f78dc17786e0e0290db450e843ac494af0158e9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 10:15:30 -0400 Subject: [PATCH 408/480] Add comment to protect the deferred import. --- importlib_metadata/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index d1b0d92d..58bbd95e 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -672,6 +672,7 @@ def origin(self): return self._load_json('direct_url.json') def _load_json(self, filename): + # Deferred for performance (python/importlib_metadata#503) import json return pass_none(json.loads)( From 2a3f50d8bbd41fc831676e7dc89d84c605c85760 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 10:15:46 -0400 Subject: [PATCH 409/480] Add news fragment. --- newsfragments/503.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/503.feature.rst diff --git a/newsfragments/503.feature.rst b/newsfragments/503.feature.rst new file mode 100644 index 00000000..7e6bdc7a --- /dev/null +++ b/newsfragments/503.feature.rst @@ -0,0 +1 @@ +Deferred import of json \ No newline at end of file From afa39e8e08b48fbedd3b8ac94cf58de39ff09c35 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 10:30:18 -0400 Subject: [PATCH 410/480] Back out changes to tests._path --- tests/_path.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/_path.py b/tests/_path.py index 30a6c15d..81ab76ac 100644 --- a/tests/_path.py +++ b/tests/_path.py @@ -28,12 +28,12 @@ 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[return-value] # jaraco/jaraco.path#4 + return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore[return-value] def build( spec: FilesSpec, - prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore[assignment] # jaraco/jaraco.path#4 + prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore[assignment] ): """ Build a set of files/directories, as described by the spec. @@ -67,8 +67,7 @@ def build( @functools.singledispatch def create(content: Union[str, bytes, FilesSpec], path): path.mkdir(exist_ok=True) - # Mypy only looks at the signature of the main singledispatch method. So it must contain the complete Union - build(content, prefix=path) # type: ignore[arg-type] # python/mypy#11727 + build(content, prefix=path) # type: ignore[arg-type] @create.register From b34810b1e0665580a91ea19b6317a1890ecd42c1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 10:50:50 -0400 Subject: [PATCH 411/480] Finalize --- NEWS.rst | 11 +++++++++++ newsfragments/+d7832466.feature.rst | 1 - newsfragments/502.feature.rst | 1 - newsfragments/503.feature.rst | 1 - 4 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 newsfragments/+d7832466.feature.rst delete mode 100644 newsfragments/502.feature.rst delete mode 100644 newsfragments/503.feature.rst diff --git a/NEWS.rst b/NEWS.rst index f4a37fdf..4e75f3b0 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,14 @@ +v8.5.0 +====== + +Features +-------- + +- Deferred import of zipfile.Path (#502) +- Deferred import of json (#503) +- Rely on zipp overlay for zipfile.Path. + + v8.4.0 ====== diff --git a/newsfragments/+d7832466.feature.rst b/newsfragments/+d7832466.feature.rst deleted file mode 100644 index 3f3f3162..00000000 --- a/newsfragments/+d7832466.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Rely on zipp overlay for zipfile.Path. \ No newline at end of file diff --git a/newsfragments/502.feature.rst b/newsfragments/502.feature.rst deleted file mode 100644 index 20659bb8..00000000 --- a/newsfragments/502.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Deferred import of zipfile.Path \ No newline at end of file diff --git a/newsfragments/503.feature.rst b/newsfragments/503.feature.rst deleted file mode 100644 index 7e6bdc7a..00000000 --- a/newsfragments/503.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Deferred import of json \ No newline at end of file From 20295ece8189459ac6d69273026fa4bc3f9a996b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 14:26:07 -0400 Subject: [PATCH 412/480] =?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 --- docs/conf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index bb19889b..c04545cd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,5 @@ from __future__ import annotations - extensions = [ 'sphinx.ext.autodoc', 'jaraco.packaging.sphinx', From db14d96503b8d9c3af49fa14e2347838de85b1ef Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 13:59:16 -0400 Subject: [PATCH 413/480] Ensure redent is idempotent (doesn't add 8 spaces to already dedented values). --- importlib_metadata/_adapters.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/importlib_metadata/_adapters.py b/importlib_metadata/_adapters.py index 3b516a2d..4e660938 100644 --- a/importlib_metadata/_adapters.py +++ b/importlib_metadata/_adapters.py @@ -57,9 +57,10 @@ def __getitem__(self, item): def _repair_headers(self): def redent(value): "Correct for RFC822 indentation" - if not value or '\n' not in value: + indent = ' ' * 8 + if not value or '\n' + indent not in value: return value - return textwrap.dedent(' ' * 8 + value) + return textwrap.dedent(indent + value) headers = [(key, redent(value)) for key, value in vars(self)['_headers']] if self._payload: From 81b766c06cc83679c4a04c2bfa6d2c8cc559bf33 Mon Sep 17 00:00:00 2001 From: Avasam Date: Wed, 11 Sep 2024 18:14:38 -0400 Subject: [PATCH 414/480] Fix an incompatibility (and source of merge conflicts) with projects using Ruff/isort. Remove extra line after imports in conf.py (jaraco/skeleton#147) --- docs/conf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index d5745d62..240329c3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,5 @@ from __future__ import annotations - extensions = [ 'sphinx.ext.autodoc', 'jaraco.packaging.sphinx', From 3fe8c5ba792fd58a5a24eef4e8a845f3b5dd6c2c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2024 18:14:58 -0400 Subject: [PATCH 415/480] Add Python 3.13 and 3.14 into the matrix. (jaraco/skeleton#146) --- .github/workflows/main.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 441b93ef..251b9c1d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -36,7 +36,7 @@ jobs: matrix: python: - "3.8" - - "3.12" + - "3.13" platform: - ubuntu-latest - macos-latest @@ -48,10 +48,14 @@ jobs: platform: ubuntu-latest - python: "3.11" platform: ubuntu-latest + - python: "3.12" + platform: ubuntu-latest + - python: "3.14" + platform: ubuntu-latest - python: pypy3.10 platform: ubuntu-latest runs-on: ${{ matrix.platform }} - continue-on-error: ${{ matrix.python == '3.13' }} + continue-on-error: ${{ matrix.python == '3.14' }} steps: - uses: actions/checkout@v4 - name: Setup Python From 90073b1aa7a49cc5fdbdc0e6e871f39e461b9422 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 12 Sep 2024 05:01:16 -0400 Subject: [PATCH 416/480] Separate bpo from Python issue numbers. --- docs/conf.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index c04545cd..32528f86 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,9 +27,13 @@ url='https://peps.python.org/pep-{pep_number:0>4}/', ), dict( - pattern=r'(python/cpython#|Python #|py-)(?P\d+)', + pattern=r'(python/cpython#|Python #)(?P\d+)', url='https://github.com/python/cpython/issues/{python}', ), + dict( + pattern=r'bpo-(?P\d+)', + url='http://bugs.python.org/issue{bpo}', + ), ], ) } From 62b6678a32087ed3bfc8ff19761764340295834e Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sat, 26 Oct 2024 00:12:59 +0100 Subject: [PATCH 417/480] Bump pre-commit hook for ruff to avoid clashes with pytest-ruff (jaraco/skeleton#150) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8ec58e22..04870d16 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.6 + rev: v0.7.1 hooks: - id: ruff args: [--fix, --unsafe-fixes] From db4dfc495552aca8d6f05ed58441fa65fdc2ed9c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 28 Oct 2024 09:11:52 -0700 Subject: [PATCH 418/480] Add Python 3.13 and 3.14 into the matrix. (jaraco/skeleton#151) From e61a9df7cdc9c8d1b56c30b7b3f94a7cdac14414 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 28 Oct 2024 12:19:31 -0400 Subject: [PATCH 419/480] Include pyproject.toml in ruff.toml. Closes jaraco/skeleton#119. Workaround for astral-sh/ruff#10299. --- ruff.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ruff.toml b/ruff.toml index 922aa1f1..8b22940a 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,3 +1,6 @@ +# include pyproject.toml for requires-python (workaround astral-sh/ruff#10299) +include = "pyproject.toml" + [lint] extend-select = [ "C901", From 750a1891ec4a1c0602050e3463e9593a8c13aa14 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 28 Oct 2024 12:22:50 -0400 Subject: [PATCH 420/480] Require Python 3.9 or later now that Python 3.8 is EOL. --- .github/workflows/main.yml | 4 +--- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 251b9c1d..9c01fc4d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -35,15 +35,13 @@ jobs: # https://blog.jaraco.com/efficient-use-of-ci-resources/ matrix: python: - - "3.8" + - "3.9" - "3.13" platform: - ubuntu-latest - macos-latest - windows-latest include: - - python: "3.9" - platform: ubuntu-latest - python: "3.10" platform: ubuntu-latest - python: "3.11" diff --git a/pyproject.toml b/pyproject.toml index 1d81b1cc..328b98cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", ] -requires-python = ">=3.8" +requires-python = ">=3.9" dependencies = [ ] dynamic = ["version"] From 5c34e69568f23a524af4fa9dad3f5e80f22ec3e6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 1 Nov 2024 22:35:31 -0400 Subject: [PATCH 421/480] Use extend for proper workaround. Closes jaraco/skeleton#152 Workaround for astral-sh/ruff#10299 --- ruff.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ruff.toml b/ruff.toml index 8b22940a..9379d6e1 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,5 +1,5 @@ -# include pyproject.toml for requires-python (workaround astral-sh/ruff#10299) -include = "pyproject.toml" +# extend pyproject.toml for requires-python (workaround astral-sh/ruff#10299) +extend = "pyproject.toml" [lint] extend-select = [ From 39a607d25def76ef760334a494554847da8c8f0f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 3 Jan 2025 10:23:13 -0500 Subject: [PATCH 422/480] Bump badge for 2025. --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index efabeee4..4d3cabee 100644 --- a/README.rst +++ b/README.rst @@ -14,5 +14,5 @@ .. .. 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-2024-informational +.. image:: https://img.shields.io/badge/skeleton-2025-informational :target: https://blog.jaraco.com/skeleton From 07aa607e54672bc1077007771cbca0e16475aa24 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 20 Jan 2025 16:55:08 -0500 Subject: [PATCH 423/480] Add support for rendering metadata where some fields have newlines (python/cpython#119650). --- importlib_metadata/_adapters.py | 46 +++++++++++++++++++++++++++++ newsfragments/+f9f493e6.feature.rst | 1 + 2 files changed, 47 insertions(+) create mode 100644 newsfragments/+f9f493e6.feature.rst diff --git a/importlib_metadata/_adapters.py b/importlib_metadata/_adapters.py index 4e660938..f6763aa7 100644 --- a/importlib_metadata/_adapters.py +++ b/importlib_metadata/_adapters.py @@ -1,11 +1,54 @@ import email.message +import email.policy import re import textwrap from ._text import FoldedCase +class RawPolicy(email.policy.EmailPolicy): + def fold(self, name, value): + folded = self.linesep.join( + textwrap.indent(value, prefix=' ' * 8).lstrip().splitlines() + ) + return f'{name}: {folded}{self.linesep}' + + class Message(email.message.Message): + r""" + Specialized Message subclass to handle metadata naturally. + + Reads values that may have newlines in them and converts the + payload to the Description. + + >>> msg_text = textwrap.dedent(''' + ... Name: Foo + ... Version: 3.0 + ... License: blah + ... de-blah + ... + ... First line of description. + ... Second line of description. + ... ''').lstrip().replace('', '') + >>> msg = Message(email.message_from_string(msg_text)) + >>> msg['Description'] + 'First line of description.\nSecond line of description.\n' + + Message should render even if values contain newlines. + + >>> print(msg) + Name: Foo + Version: 3.0 + License: blah + de-blah + Description: First line of description. + Second line of description. + + First line of description. + Second line of description. + + """ + multiple_use_keys = set( map( FoldedCase, @@ -67,6 +110,9 @@ def redent(value): headers.append(('Description', self.get_payload())) return headers + def as_string(self): + return super().as_string(policy=RawPolicy()) + @property def json(self): """ diff --git a/newsfragments/+f9f493e6.feature.rst b/newsfragments/+f9f493e6.feature.rst new file mode 100644 index 00000000..14b812b9 --- /dev/null +++ b/newsfragments/+f9f493e6.feature.rst @@ -0,0 +1 @@ +Add support for rendering metadata where some fields have newlines (python/cpython#119650). From dab1dd81507fef5bff6a32a63ab8fc8633e2c24c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 20 Jan 2025 17:00:01 -0500 Subject: [PATCH 424/480] When transforming the payload to a Description key, clear the payload. --- importlib_metadata/_adapters.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/importlib_metadata/_adapters.py b/importlib_metadata/_adapters.py index f6763aa7..ce4a2fe8 100644 --- a/importlib_metadata/_adapters.py +++ b/importlib_metadata/_adapters.py @@ -44,8 +44,6 @@ class Message(email.message.Message): Description: First line of description. Second line of description. - First line of description. - Second line of description. """ @@ -108,6 +106,7 @@ def redent(value): headers = [(key, redent(value)) for key, value in vars(self)['_headers']] if self._payload: headers.append(('Description', self.get_payload())) + self.set_payload('') return headers def as_string(self): From dad738095d6d28f24d254f3cc9f82e2394fb2a09 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 20 Jan 2025 17:00:47 -0500 Subject: [PATCH 425/480] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/+f9f493e6.feature.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/+f9f493e6.feature.rst diff --git a/NEWS.rst b/NEWS.rst index 4e75f3b0..1998ebcb 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v8.6.0 +====== + +Features +-------- + +- Add support for rendering metadata where some fields have newlines (python/cpython#119650). + + v8.5.0 ====== diff --git a/newsfragments/+f9f493e6.feature.rst b/newsfragments/+f9f493e6.feature.rst deleted file mode 100644 index 14b812b9..00000000 --- a/newsfragments/+f9f493e6.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Add support for rendering metadata where some fields have newlines (python/cpython#119650). From 506beb7cbd23fef4f93a63a5c3931e302d828896 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 20 Jan 2025 17:15:33 -0500 Subject: [PATCH 426/480] Fixed indentation logic to also honor blank lines. --- importlib_metadata/_adapters.py | 10 ++++++++-- newsfragments/+bd6aec5a.bugfix.rst | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 newsfragments/+bd6aec5a.bugfix.rst diff --git a/importlib_metadata/_adapters.py b/importlib_metadata/_adapters.py index ce4a2fe8..f5b30dd9 100644 --- a/importlib_metadata/_adapters.py +++ b/importlib_metadata/_adapters.py @@ -9,7 +9,9 @@ class RawPolicy(email.policy.EmailPolicy): def fold(self, name, value): folded = self.linesep.join( - textwrap.indent(value, prefix=' ' * 8).lstrip().splitlines() + textwrap.indent(value, prefix=' ' * 8, predicate=lambda line: True) + .lstrip() + .splitlines() ) return f'{name}: {folded}{self.linesep}' @@ -29,10 +31,12 @@ class Message(email.message.Message): ... ... First line of description. ... Second line of description. + ... + ... Fourth line! ... ''').lstrip().replace('', '') >>> msg = Message(email.message_from_string(msg_text)) >>> msg['Description'] - 'First line of description.\nSecond line of description.\n' + 'First line of description.\nSecond line of description.\n\nFourth line!\n' Message should render even if values contain newlines. @@ -43,6 +47,8 @@ class Message(email.message.Message): de-blah Description: First line of description. Second line of description. + + Fourth line! """ diff --git a/newsfragments/+bd6aec5a.bugfix.rst b/newsfragments/+bd6aec5a.bugfix.rst new file mode 100644 index 00000000..ec1dd1e3 --- /dev/null +++ b/newsfragments/+bd6aec5a.bugfix.rst @@ -0,0 +1 @@ +Fixed indentation logic to also honor blank lines. From 45e8bde73f8ce816fdc47618a31ba3916a332485 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 20 Jan 2025 17:15:43 -0500 Subject: [PATCH 427/480] Finalize --- NEWS.rst | 9 +++++++++ newsfragments/+bd6aec5a.bugfix.rst | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) delete mode 100644 newsfragments/+bd6aec5a.bugfix.rst diff --git a/NEWS.rst b/NEWS.rst index 1998ebcb..e5a4b397 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,12 @@ +v8.6.1 +====== + +Bugfixes +-------- + +- Fixed indentation logic to also honor blank lines. + + v8.6.0 ====== diff --git a/newsfragments/+bd6aec5a.bugfix.rst b/newsfragments/+bd6aec5a.bugfix.rst deleted file mode 100644 index ec1dd1e3..00000000 --- a/newsfragments/+bd6aec5a.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed indentation logic to also honor blank lines. From aee344d781920bba42ddbee4b4b44af29d7bab6e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 12 Feb 2025 10:44:24 -0500 Subject: [PATCH 428/480] Removing dependabot config. Closes jaraco/skeleton#156 --- .github/dependabot.yml | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 89ff3396..00000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,8 +0,0 @@ -version: 2 -updates: - - package-ecosystem: "pip" - directory: "/" - schedule: - interval: "daily" - allow: - - dependency-type: "all" From 75ce9aba3ed9f4002fa01db0287dfdb1600fb635 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 23 Feb 2025 18:57:40 -0500 Subject: [PATCH 429/480] Add support for building lxml on pre-release Pythons. Closes jaraco/skeleton#161 --- .github/workflows/main.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9c01fc4d..5841cc37 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -56,6 +56,13 @@ jobs: continue-on-error: ${{ matrix.python == '3.14' }} steps: - uses: actions/checkout@v4 + - name: Install build dependencies + # Install dependencies for building packages on pre-release Pythons + # jaraco/skeleton#161 + if: matrix.python == '3.14' && matrix.platform == 'ubuntu-latest' + run: | + sudo apt update + sudo apt install -y libxml2-dev libxslt-dev - name: Setup Python uses: actions/setup-python@v4 with: From 1c9467fdec1cc1456772cd71c7e740f048ce86fc Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Mon, 24 Feb 2025 22:00:11 +0000 Subject: [PATCH 430/480] Fix new mandatory configuration field for RTD (jaraco/skeleton#159) This field is now required and prevents the build from running if absent. Details in https://about.readthedocs.com/blog/2024/12/deprecate-config-files-without-sphinx-or-mkdocs-config/ --- .readthedocs.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.readthedocs.yaml b/.readthedocs.yaml index dc8516ac..72437063 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -5,6 +5,9 @@ python: extra_requirements: - doc +sphinx: + configuration: docs/conf.py + # required boilerplate readthedocs/readthedocs.org#10401 build: os: ubuntu-lts-latest From 1a2f93053d789f041d88c97c5da4eea9e949bdfe Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 25 Feb 2025 13:21:13 -0500 Subject: [PATCH 431/480] Select Ruff rules for modern type annotations (jaraco/skeleton#160) * Select Ruff rules for modern type annotations Ensure modern type annotation syntax and best practices Not including those covered by type-checkers or exclusive to Python 3.11+ Not including rules currently in preview either. These are the same set of rules I have in pywin32 as of https://github.com/mhammond/pywin32/pull/2458 setuptools has all the same rules enabled (except it also includes the `UP` group directly) * Add PYI011 ignore and #local section * Update ruff.toml Co-authored-by: Jason R. Coombs * Add # upstream --------- Co-authored-by: Jason R. Coombs --- ruff.toml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/ruff.toml b/ruff.toml index 9379d6e1..1d65c7c2 100644 --- a/ruff.toml +++ b/ruff.toml @@ -3,11 +3,32 @@ extend = "pyproject.toml" [lint] extend-select = [ + # upstream + "C901", "PERF401", "W", + + # Ensure modern type annotation syntax and best practices + # Not including those covered by type-checkers or exclusive to Python 3.11+ + "FA", # flake8-future-annotations + "F404", # late-future-import + "PYI", # flake8-pyi + "UP006", # non-pep585-annotation + "UP007", # non-pep604-annotation + "UP010", # unnecessary-future-import + "UP035", # deprecated-import + "UP037", # quoted-annotation + "UP043", # unnecessary-default-type-args + + # local ] ignore = [ + # upstream + + # Typeshed rejects complex or non-literal defaults for maintenance and testing reasons, + # irrelevant to this project. + "PYI011", # typed-argument-default-in-stub # https://docs.astral.sh/ruff/formatter/#conflicting-lint-rules "W191", "E111", @@ -23,6 +44,8 @@ ignore = [ "COM819", "ISC001", "ISC002", + + # local ] [format] From aa891069099398fe2eb294ac4b781460d8c0a39b Mon Sep 17 00:00:00 2001 From: Avasam Date: Wed, 26 Feb 2025 17:56:42 -0500 Subject: [PATCH 432/480] Consistent import sorting (isort) (jaraco/skeleton#157) --- ruff.toml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/ruff.toml b/ruff.toml index 1d65c7c2..b52a6d7c 100644 --- a/ruff.toml +++ b/ruff.toml @@ -5,9 +5,10 @@ extend = "pyproject.toml" extend-select = [ # upstream - "C901", - "PERF401", - "W", + "C901", # complex-structure + "I", # isort + "PERF401", # manual-list-comprehension + "W", # pycodestyle Warning # Ensure modern type annotation syntax and best practices # Not including those covered by type-checkers or exclusive to Python 3.11+ From 8f42595ca65133aeb4b75f38183233c27b2e6247 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Fri, 28 Feb 2025 00:19:07 +0100 Subject: [PATCH 433/480] Enable ruff rules ISC001/ISC002 (jaraco/skeleton#158) Starting with ruff 0.9.1, they are compatible with the ruff formatter when used together. --- ruff.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/ruff.toml b/ruff.toml index b52a6d7c..2b679267 100644 --- a/ruff.toml +++ b/ruff.toml @@ -43,8 +43,6 @@ ignore = [ "Q003", "COM812", "COM819", - "ISC001", - "ISC002", # local ] From b7d4b6ee00804bef36a8c398676e207813540c3b Mon Sep 17 00:00:00 2001 From: Avasam Date: Tue, 4 Mar 2025 03:24:14 -0500 Subject: [PATCH 434/480] remove extra spaces in ruff.toml (jaraco/skeleton#164) --- ruff.toml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ruff.toml b/ruff.toml index 2b679267..1e952846 100644 --- a/ruff.toml +++ b/ruff.toml @@ -4,13 +4,13 @@ extend = "pyproject.toml" [lint] extend-select = [ # upstream - + "C901", # complex-structure "I", # isort "PERF401", # manual-list-comprehension "W", # pycodestyle Warning - - # Ensure modern type annotation syntax and best practices + + # Ensure modern type annotation syntax and best practices # Not including those covered by type-checkers or exclusive to Python 3.11+ "FA", # flake8-future-annotations "F404", # late-future-import @@ -26,7 +26,7 @@ extend-select = [ ] ignore = [ # upstream - + # Typeshed rejects complex or non-literal defaults for maintenance and testing reasons, # irrelevant to this project. "PYI011", # typed-argument-default-in-stub @@ -44,7 +44,7 @@ ignore = [ "COM812", "COM819", - # local + # local ] [format] From b00e9dd730423a399c1d3c3d5621687adff0c5a5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 7 Mar 2025 09:05:55 -0500 Subject: [PATCH 435/480] Remove pycodestyle warnings, no longer meaningful when using ruff formatter. Ref https://github.com/jaraco/skeleton/commit/d1c5444126aeacefee3949b30136446ab99979d8#commitcomment-153409678 --- ruff.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/ruff.toml b/ruff.toml index 1e952846..267a1ba1 100644 --- a/ruff.toml +++ b/ruff.toml @@ -8,7 +8,6 @@ extend-select = [ "C901", # complex-structure "I", # isort "PERF401", # manual-list-comprehension - "W", # pycodestyle Warning # Ensure modern type annotation syntax and best practices # Not including those covered by type-checkers or exclusive to Python 3.11+ From d587ff737ee89778cf6f4bbd249e770c965fee06 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Fri, 7 Mar 2025 15:08:11 +0100 Subject: [PATCH 436/480] Update to the latest ruff version (jaraco/skeleton#166) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 04870d16..633e3648 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.7.1 + rev: v0.9.9 hooks: - id: ruff args: [--fix, --unsafe-fixes] From ad84110008b826efd6e39bcc39b9998b4f1cc767 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Fri, 21 Mar 2025 00:14:38 +0000 Subject: [PATCH 437/480] Remove deprecated license classifier (PEP 639) (jaraco/skeleton#170) --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 328b98cb..71b1a7da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,6 @@ readme = "README.rst" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", ] From 1ebb559a507f97ece7342d7f1532a49188cade33 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 20 Mar 2025 20:56:31 -0400 Subject: [PATCH 438/480] Remove workaround and update badge. Closes jaraco/skeleton#155 --- README.rst | 2 +- ruff.toml | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 4d3cabee..3000f5ab 100644 --- a/README.rst +++ b/README.rst @@ -7,7 +7,7 @@ :target: https://github.com/PROJECT_PATH/actions?query=workflow%3A%22tests%22 :alt: tests -.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/charliermarsh/ruff/main/assets/badge/v2.json +.. image:: https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json :target: https://github.com/astral-sh/ruff :alt: Ruff diff --git a/ruff.toml b/ruff.toml index 267a1ba1..63c0825f 100644 --- a/ruff.toml +++ b/ruff.toml @@ -1,6 +1,3 @@ -# extend pyproject.toml for requires-python (workaround astral-sh/ruff#10299) -extend = "pyproject.toml" - [lint] extend-select = [ # upstream From 979e626055ab60095b37be04555a01a40f62e470 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 22 Mar 2025 05:33:58 -0400 Subject: [PATCH 439/480] Remove PIP_NO_PYTHON_VERSION_WARNING. Ref pypa/pip#13154 --- .github/workflows/main.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5841cc37..928acf2c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,7 +21,6 @@ env: # Suppress noisy pip warnings PIP_DISABLE_PIP_VERSION_CHECK: 'true' - PIP_NO_PYTHON_VERSION_WARNING: 'true' PIP_NO_WARN_SCRIPT_LOCATION: 'true' # Ensure tests can sense settings about the environment From d9fc620fd5d00b439397dc15f1acfdd6f583b770 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 09:56:23 -0400 Subject: [PATCH 440/480] =?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 | 36 ++++++++++++++++++---------------- importlib_metadata/_meta.py | 20 ++++++++----------- tests/_path.py | 3 ++- 3 files changed, 29 insertions(+), 30 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 46a14e64..87c9eb51 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -22,11 +22,13 @@ import sys import textwrap import types +from collections.abc import Iterable, Mapping from contextlib import suppress from importlib import import_module from importlib.abc import MetaPathFinder from itertools import starmap -from typing import Any, Iterable, List, Mapping, Match, Optional, Set, cast +from re import Match +from typing import Any, List, Optional, Set, cast from . import _meta from ._collections import FreezableDefaultDict, Pair @@ -175,7 +177,7 @@ class EntryPoint: value: str group: str - dist: Optional[Distribution] = None + dist: Distribution | None = None def __init__(self, name: str, value: str, group: str) -> None: vars(self).update(name=name, value=value, group=group) @@ -203,7 +205,7 @@ def attr(self) -> str: return match.group('attr') @property - def extras(self) -> List[str]: + 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 '') @@ -305,14 +307,14 @@ def select(self, **params) -> EntryPoints: return EntryPoints(ep for ep in self if py39.ep_matches(ep, **params)) @property - def names(self) -> Set[str]: + 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) -> Set[str]: + def groups(self) -> set[str]: """ Return the set of all groups of all entry points. """ @@ -333,7 +335,7 @@ def _from_text(text): class PackagePath(pathlib.PurePosixPath): """A reference to a path in a package""" - hash: Optional[FileHash] + hash: FileHash | None size: int dist: Distribution @@ -368,7 +370,7 @@ class Distribution(metaclass=abc.ABCMeta): """ @abc.abstractmethod - def read_text(self, filename) -> Optional[str]: + def read_text(self, filename) -> str | None: """Attempt to load metadata file given by the name. Python distribution metadata is organized by blobs of text @@ -428,7 +430,7 @@ def from_name(cls, name: str) -> Distribution: @classmethod def discover( - cls, *, context: Optional[DistributionFinder.Context] = None, **kwargs + cls, *, context: DistributionFinder.Context | None = None, **kwargs ) -> Iterable[Distribution]: """Return an iterable of Distribution objects for all packages. @@ -524,7 +526,7 @@ def entry_points(self) -> EntryPoints: return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self) @property - def files(self) -> Optional[List[PackagePath]]: + def files(self) -> list[PackagePath] | None: """Files in this distribution. :return: List of PackagePath for this distribution or None @@ -616,7 +618,7 @@ def _read_files_egginfo_sources(self): return text and map('"{}"'.format, text.splitlines()) @property - def requires(self) -> Optional[List[str]]: + def requires(self) -> list[str] | None: """Generated requirements specified for this Distribution""" reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs() return reqs and list(reqs) @@ -722,7 +724,7 @@ def __init__(self, **kwargs): vars(self).update(kwargs) @property - def path(self) -> List[str]: + def path(self) -> list[str]: """ The sequence of directory path that a distribution finder should search. @@ -874,7 +876,7 @@ class Prepared: normalized = None legacy_normalized = None - def __init__(self, name: Optional[str]): + def __init__(self, name: str | None): self.name = name if name is None: return @@ -944,7 +946,7 @@ def __init__(self, path: SimplePath) -> None: """ self._path = path - def read_text(self, filename: str | os.PathLike[str]) -> Optional[str]: + def read_text(self, filename: str | os.PathLike[str]) -> str | None: with suppress( FileNotFoundError, IsADirectoryError, @@ -1051,7 +1053,7 @@ def entry_points(**params) -> EntryPoints: return EntryPoints(eps).select(**params) -def files(distribution_name: str) -> Optional[List[PackagePath]]: +def files(distribution_name: str) -> list[PackagePath] | None: """Return a list of files for the named package. :param distribution_name: The name of the distribution package to query. @@ -1060,7 +1062,7 @@ def files(distribution_name: str) -> Optional[List[PackagePath]]: return distribution(distribution_name).files -def requires(distribution_name: str) -> Optional[List[str]]: +def requires(distribution_name: str) -> list[str] | None: """ Return a list of requirements for the named package. @@ -1070,7 +1072,7 @@ def requires(distribution_name: str) -> Optional[List[str]]: return distribution(distribution_name).requires -def packages_distributions() -> Mapping[str, List[str]]: +def packages_distributions() -> Mapping[str, list[str]]: """ Return a mapping of top-level packages to their distributions. @@ -1091,7 +1093,7 @@ def _top_level_declared(dist): return (dist.read_text('top_level.txt') or '').split() -def _topmost(name: PackagePath) -> Optional[str]: +def _topmost(name: PackagePath) -> str | None: """ Return the top-most parent as long as there is a parent. """ diff --git a/importlib_metadata/_meta.py b/importlib_metadata/_meta.py index 0942bbd9..0c20eff3 100644 --- a/importlib_metadata/_meta.py +++ b/importlib_metadata/_meta.py @@ -1,15 +1,11 @@ from __future__ import annotations import os +from collections.abc import Iterator from typing import ( Any, - Dict, - Iterator, - List, - Optional, Protocol, TypeVar, - Union, overload, ) @@ -28,25 +24,25 @@ def __iter__(self) -> Iterator[str]: ... # pragma: no cover @overload def get( self, name: str, failobj: None = None - ) -> Optional[str]: ... # pragma: no cover + ) -> str | None: ... # pragma: no cover @overload - def get(self, name: str, failobj: _T) -> Union[str, _T]: ... # pragma: no cover + def get(self, name: str, failobj: _T) -> str | _T: ... # pragma: no cover # overload per python/importlib_metadata#435 @overload def get_all( self, name: str, failobj: None = None - ) -> Optional[List[Any]]: ... # pragma: no cover + ) -> list[Any] | None: ... # pragma: no cover @overload - def get_all(self, name: str, failobj: _T) -> Union[List[Any], _T]: + def get_all(self, name: str, failobj: _T) -> list[Any] | _T: """ Return all values associated with a possibly multi-valued key. """ @property - def json(self) -> Dict[str, Union[str, List[str]]]: + def json(self) -> dict[str, str | list[str]]: """ A JSON-compatible form of the metadata. """ @@ -58,11 +54,11 @@ class SimplePath(Protocol): """ def joinpath( - self, other: Union[str, os.PathLike[str]] + self, other: str | os.PathLike[str] ) -> SimplePath: ... # pragma: no cover def __truediv__( - self, other: Union[str, os.PathLike[str]] + self, other: str | os.PathLike[str] ) -> SimplePath: ... # pragma: no cover @property diff --git a/tests/_path.py b/tests/_path.py index c66cf5f8..e63d889f 100644 --- a/tests/_path.py +++ b/tests/_path.py @@ -4,7 +4,8 @@ import functools import pathlib -from typing import TYPE_CHECKING, Mapping, Protocol, Union, runtime_checkable +from collections.abc import Mapping +from typing import TYPE_CHECKING, Protocol, Union, runtime_checkable if TYPE_CHECKING: from typing_extensions import Self From 75670d283f379bbe7072cf5ec8fe1f6c7703f9ea Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 09:58:01 -0400 Subject: [PATCH 441/480] Remove unused imports. --- 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 87c9eb51..275c7106 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -28,7 +28,7 @@ from importlib.abc import MetaPathFinder from itertools import starmap from re import Match -from typing import Any, List, Optional, Set, cast +from typing import Any, cast from . import _meta from ._collections import FreezableDefaultDict, Pair From 2bfbaf3bed463fc85646d5d57c04d257876844b5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 10:01:40 -0400 Subject: [PATCH 442/480] Prefer typing.NamedTuple --- importlib_metadata/_collections.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/importlib_metadata/_collections.py b/importlib_metadata/_collections.py index cf0954e1..fc5045d3 100644 --- a/importlib_metadata/_collections.py +++ b/importlib_metadata/_collections.py @@ -1,4 +1,5 @@ import collections +import typing # from jaraco.collections 3.3 @@ -24,7 +25,10 @@ def freeze(self): self._frozen = lambda key: self.default_factory() -class Pair(collections.namedtuple('Pair', 'name value')): +class Pair(typing.NamedTuple): + name: str + value: str + @classmethod def parse(cls, text): return cls(*map(str.strip, text.split("=", 1))) From c10bdf30dafb55ec471a289e751089255e7f281d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 10:02:50 -0400 Subject: [PATCH 443/480] =?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/compat/py39.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/importlib_metadata/compat/py39.py b/importlib_metadata/compat/py39.py index 1f15bd97..2592436d 100644 --- a/importlib_metadata/compat/py39.py +++ b/importlib_metadata/compat/py39.py @@ -2,7 +2,9 @@ Compatibility layer with Python 3.8/3.9 """ -from typing import TYPE_CHECKING, Any, Optional +from __future__ import annotations + +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: # pragma: no cover # Prevent circular imports on runtime. @@ -11,7 +13,7 @@ Distribution = EntryPoint = Any -def normalized_name(dist: Distribution) -> Optional[str]: +def normalized_name(dist: Distribution) -> str | None: """ Honor name normalization for distributions that don't provide ``_normalized_name``. """ From 55c6070ad7f337a423962698d3e02c62a8e1b10e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 09:38:56 -0400 Subject: [PATCH 444/480] Refactored parsing and handling of EntryPoint.value. --- importlib_metadata/__init__.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 275c7106..849ce068 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -27,7 +27,6 @@ from importlib import import_module from importlib.abc import MetaPathFinder from itertools import starmap -from re import Match from typing import Any, cast from . import _meta @@ -135,6 +134,12 @@ def valid(line: str): return line and not line.startswith('#') +class _EntryPointMatch(types.SimpleNamespace): + module: str + attr: str + extras: str + + class EntryPoint: """An entry point as defined by Python packaging conventions. @@ -187,28 +192,27 @@ def load(self) -> Any: is indicated by the value, return that module. Otherwise, return the named object. """ - match = cast(Match, self.pattern.match(self.value)) - module = import_module(match.group('module')) - attrs = filter(None, (match.group('attr') or '').split('.')) + module = import_module(self.module) + attrs = filter(None, (self.attr or '').split('.')) return functools.reduce(getattr, attrs, module) @property def module(self) -> str: - match = self.pattern.match(self.value) - assert match is not None - return match.group('module') + return self._match.module @property def attr(self) -> str: - match = self.pattern.match(self.value) - assert match is not None - return match.group('attr') + return self._match.attr @property def extras(self) -> list[str]: + return re.findall(r'\w+', self._match.extras or '') + + @property + def _match(self) -> _EntryPointMatch: match = self.pattern.match(self.value) assert match is not None - return re.findall(r'\w+', match.group('extras') or '') + return _EntryPointMatch(**match.groupdict()) def _for(self, dist): vars(self).update(dist=dist) From eae6a754d004e8ea72d5d07b7dc3733a6be71f1b Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 09:41:26 -0400 Subject: [PATCH 445/480] Raise a ValueError if no match. Closes #488 --- importlib_metadata/__init__.py | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 849ce068..d527e403 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -155,6 +155,22 @@ class EntryPoint: 'attr' >>> ep.extras ['extra1', 'extra2'] + + If the value package or module are not valid identifiers, a + ValueError is raised on access. + + >>> EntryPoint(name=None, group=None, value='invalid-name').module + Traceback (most recent call last): + ... + ValueError: ('Invalid object reference...invalid-name... + >>> EntryPoint(name=None, group=None, value='invalid-name').attr + Traceback (most recent call last): + ... + ValueError: ('Invalid object reference...invalid-name... + >>> EntryPoint(name=None, group=None, value='invalid-name').extras + Traceback (most recent call last): + ... + ValueError: ('Invalid object reference...invalid-name... """ pattern = re.compile( @@ -211,7 +227,13 @@ def extras(self) -> list[str]: @property def _match(self) -> _EntryPointMatch: match = self.pattern.match(self.value) - assert match is not None + if not match: + raise ValueError( + 'Invalid object reference. ' + 'See https://packaging.python.org' + '/en/latest/specifications/entry-points/#data-model', + self.value, + ) return _EntryPointMatch(**match.groupdict()) def _for(self, dist): From f179e28888b2c6caf12baaf5449ff1cd82513dfe Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 09:45:56 -0400 Subject: [PATCH 446/480] Also raise ValueError on construction if the value is invalid. --- importlib_metadata/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index d527e403..ff3c2a44 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -171,6 +171,14 @@ class EntryPoint: Traceback (most recent call last): ... ValueError: ('Invalid object reference...invalid-name... + + The same thing happens on construction. + + >>> EntryPoint(name=None, group=None, value='invalid-name') + Traceback (most recent call last): + ... + ValueError: ('Invalid object reference...invalid-name... + """ pattern = re.compile( @@ -202,6 +210,7 @@ class EntryPoint: def __init__(self, name: str, value: str, group: str) -> None: vars(self).update(name=name, value=value, group=group) + self.module def load(self) -> Any: """Load the entry point from its definition. If only a module From 9f8af013635833cf3ac348413c9ac63b37caa3dd Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 09:50:19 -0400 Subject: [PATCH 447/480] Prefer a cached property, as the property is likely to be retrieved at least 3 times (on construction and for module:attr access). --- 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 ff3c2a44..157b2c6f 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -233,7 +233,7 @@ def attr(self) -> str: def extras(self) -> list[str]: return re.findall(r'\w+', self._match.extras or '') - @property + @functools.cached_property def _match(self) -> _EntryPointMatch: match = self.pattern.match(self.value) if not match: From 57f31d77e18fef11dfadfd44775f253971c36920 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 27 Jun 2024 12:53:04 -0400 Subject: [PATCH 448/480] Allow metadata to return None when there is no metadata present. --- importlib_metadata/__init__.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 157b2c6f..4717f3d7 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -511,7 +511,7 @@ def _discover_resolvers(): return filter(None, declared) @property - def metadata(self) -> _meta.PackageMetadata: + def metadata(self) -> _meta.PackageMetadata | None: """Return the parsed metadata for this Distribution. The returned object will have keys that name the various bits of @@ -521,10 +521,8 @@ def metadata(self) -> _meta.PackageMetadata: Custom providers may provide the METADATA file or override this property. """ - # deferred for performance (python/cpython#109829) - from . import _adapters - opt_text = ( + text = ( self.read_text('METADATA') or self.read_text('PKG-INFO') # This last clause is here to support old egg-info files. Its @@ -532,7 +530,14 @@ def metadata(self) -> _meta.PackageMetadata: # (which points to the egg-info file) attribute unchanged. or self.read_text('') ) - text = cast(str, opt_text) + return self._assemble_message(text) + + @staticmethod + @pass_none + def _assemble_message(text: str) -> _meta.PackageMetadata: + # deferred for performance (python/cpython#109829) + from . import _adapters + return _adapters.Message(email.message_from_string(text)) @property From 22bb567692d8e7bd216f864a9d8dee1272ee8674 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 10:32:46 -0400 Subject: [PATCH 449/480] Fix type errors where metadata could be None. --- importlib_metadata/__init__.py | 8 ++++---- importlib_metadata/compat/py39.py | 8 ++++++-- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 4717f3d7..ded27e13 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -543,7 +543,7 @@ def _assemble_message(text: str) -> _meta.PackageMetadata: @property def name(self) -> str: """Return the 'Name' metadata for the distribution package.""" - return self.metadata['Name'] + return cast(PackageMetadata, self.metadata)['Name'] @property def _normalized_name(self): @@ -553,7 +553,7 @@ def _normalized_name(self): @property def version(self) -> str: """Return the 'Version' metadata for the distribution package.""" - return self.metadata['Version'] + return cast(PackageMetadata, self.metadata)['Version'] @property def entry_points(self) -> EntryPoints: @@ -1050,7 +1050,7 @@ def distributions(**kwargs) -> Iterable[Distribution]: return Distribution.discover(**kwargs) -def metadata(distribution_name: str) -> _meta.PackageMetadata: +def metadata(distribution_name: str) -> _meta.PackageMetadata | None: """Get the metadata for the named package. :param distribution_name: The name of the distribution package to query. @@ -1125,7 +1125,7 @@ def packages_distributions() -> Mapping[str, list[str]]: pkg_to_dist = collections.defaultdict(list) for dist in distributions(): for pkg in _top_level_declared(dist) or _top_level_inferred(dist): - pkg_to_dist[pkg].append(dist.metadata['Name']) + pkg_to_dist[pkg].append(cast(PackageMetadata, dist.metadata)['Name']) return dict(pkg_to_dist) diff --git a/importlib_metadata/compat/py39.py b/importlib_metadata/compat/py39.py index 2592436d..1fbcbf7b 100644 --- a/importlib_metadata/compat/py39.py +++ b/importlib_metadata/compat/py39.py @@ -4,7 +4,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast if TYPE_CHECKING: # pragma: no cover # Prevent circular imports on runtime. @@ -12,6 +12,8 @@ else: Distribution = EntryPoint = Any +from .._meta import PackageMetadata + def normalized_name(dist: Distribution) -> str | None: """ @@ -22,7 +24,9 @@ def normalized_name(dist: Distribution) -> str | None: except AttributeError: from .. import Prepared # -> delay to prevent circular imports. - return Prepared.normalize(getattr(dist, "name", None) or dist.metadata['Name']) + return Prepared.normalize( + getattr(dist, "name", None) or cast(PackageMetadata, dist.metadata)['Name'] + ) def ep_matches(ep: EntryPoint, **params) -> bool: From 0830c39b8a23e48024365120c0e97a6f7c36c5ec Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 10:34:40 -0400 Subject: [PATCH 450/480] Add news fragment. --- newsfragments/493.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/493.feature.rst diff --git a/newsfragments/493.feature.rst b/newsfragments/493.feature.rst new file mode 100644 index 00000000..e75e0e3e --- /dev/null +++ b/newsfragments/493.feature.rst @@ -0,0 +1 @@ +``.metadata()`` (and ``Distribution.metadata``) can now return ``None`` if the metadata directory exists but not metadata file is present. From 5a657051f7386de6f0560c200d78e941be2c8058 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 10:47:08 -0400 Subject: [PATCH 451/480] Refactor the casting into a wrapper for brevity and to document its purpose. --- importlib_metadata/__init__.py | 9 +++++---- importlib_metadata/_typing.py | 15 +++++++++++++++ importlib_metadata/compat/py39.py | 6 +++--- 3 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 importlib_metadata/_typing.py diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index ded27e13..cdfc1f62 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -27,7 +27,7 @@ from importlib import import_module from importlib.abc import MetaPathFinder from itertools import starmap -from typing import Any, cast +from typing import Any from . import _meta from ._collections import FreezableDefaultDict, Pair @@ -38,6 +38,7 @@ from ._functools import method_cache, pass_none from ._itertools import always_iterable, bucket, unique_everseen from ._meta import PackageMetadata, SimplePath +from ._typing import md_none from .compat import py39, py311 __all__ = [ @@ -543,7 +544,7 @@ def _assemble_message(text: str) -> _meta.PackageMetadata: @property def name(self) -> str: """Return the 'Name' metadata for the distribution package.""" - return cast(PackageMetadata, self.metadata)['Name'] + return md_none(self.metadata)['Name'] @property def _normalized_name(self): @@ -553,7 +554,7 @@ def _normalized_name(self): @property def version(self) -> str: """Return the 'Version' metadata for the distribution package.""" - return cast(PackageMetadata, self.metadata)['Version'] + return md_none(self.metadata)['Version'] @property def entry_points(self) -> EntryPoints: @@ -1125,7 +1126,7 @@ def packages_distributions() -> Mapping[str, list[str]]: pkg_to_dist = collections.defaultdict(list) for dist in distributions(): for pkg in _top_level_declared(dist) or _top_level_inferred(dist): - pkg_to_dist[pkg].append(cast(PackageMetadata, dist.metadata)['Name']) + pkg_to_dist[pkg].append(md_none(dist.metadata)['Name']) return dict(pkg_to_dist) diff --git a/importlib_metadata/_typing.py b/importlib_metadata/_typing.py new file mode 100644 index 00000000..32b1d2b9 --- /dev/null +++ b/importlib_metadata/_typing.py @@ -0,0 +1,15 @@ +import functools +import typing + +from ._meta import PackageMetadata + +md_none = functools.partial(typing.cast, PackageMetadata) +""" +Suppress type errors for optional metadata. + +Although Distribution.metadata can return None when metadata is corrupt +and thus None, allow callers to assume it's not None and crash if +that's the case. + +# python/importlib_metadata#493 +""" diff --git a/importlib_metadata/compat/py39.py b/importlib_metadata/compat/py39.py index 1fbcbf7b..3eb9c01e 100644 --- a/importlib_metadata/compat/py39.py +++ b/importlib_metadata/compat/py39.py @@ -4,7 +4,7 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any if TYPE_CHECKING: # pragma: no cover # Prevent circular imports on runtime. @@ -12,7 +12,7 @@ else: Distribution = EntryPoint = Any -from .._meta import PackageMetadata +from .._typing import md_none def normalized_name(dist: Distribution) -> str | None: @@ -25,7 +25,7 @@ def normalized_name(dist: Distribution) -> str | None: from .. import Prepared # -> delay to prevent circular imports. return Prepared.normalize( - getattr(dist, "name", None) or cast(PackageMetadata, dist.metadata)['Name'] + getattr(dist, "name", None) or md_none(dist.metadata)['Name'] ) From e4351c226765f53a40316fa6aab50488aee8a90f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 10:51:40 -0400 Subject: [PATCH 452/480] Add a new test capturing the new expectation. --- tests/test_main.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_main.py b/tests/test_main.py index 7c9851fc..5ed08c89 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -155,6 +155,16 @@ def test_valid_dists_preferred(self): dist = Distribution.from_name('foo') assert dist.version == "1.0" + def test_missing_metadata(self): + """ + Dists with a missing metadata file should return None. + + Ref python/importlib_metadata#493. + """ + fixtures.build_files(self.make_pkg('foo-4.3', files={}), self.site_dir) + assert Distribution.from_name('foo').metadata is None + assert metadata('foo') is None + class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase): @staticmethod From 708dff4f1ab89bdd126e3e8c56098d04282c5809 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 27 Apr 2025 11:22:50 -0400 Subject: [PATCH 453/480] Finalize --- NEWS.rst | 15 +++++++++++++++ newsfragments/493.feature.rst | 1 - newsfragments/518.bugfix.rst | 1 - 3 files changed, 15 insertions(+), 2 deletions(-) delete mode 100644 newsfragments/493.feature.rst delete mode 100644 newsfragments/518.bugfix.rst diff --git a/NEWS.rst b/NEWS.rst index e5a4b397..4d0c4bdc 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,18 @@ +v8.7.0 +====== + +Features +-------- + +- ``.metadata()`` (and ``Distribution.metadata``) can now return ``None`` if the metadata directory exists but not metadata file is present. (#493) + + +Bugfixes +-------- + +- Raise consistent ValueError for invalid EntryPoint.value (#518) + + v8.6.1 ====== diff --git a/newsfragments/493.feature.rst b/newsfragments/493.feature.rst deleted file mode 100644 index e75e0e3e..00000000 --- a/newsfragments/493.feature.rst +++ /dev/null @@ -1 +0,0 @@ -``.metadata()`` (and ``Distribution.metadata``) can now return ``None`` if the metadata directory exists but not metadata file is present. diff --git a/newsfragments/518.bugfix.rst b/newsfragments/518.bugfix.rst deleted file mode 100644 index 416071f7..00000000 --- a/newsfragments/518.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Raise consistent ValueError for invalid EntryPoint.value From 9a81db3c77bc106017dcd4b0853a5a94f43ae33c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 3 May 2025 03:57:47 -0400 Subject: [PATCH 454/480] Replace copy of license with an SPDX identifier. (jaraco/skeleton#171) --- LICENSE | 17 ----------------- pyproject.toml | 1 + 2 files changed, 1 insertion(+), 17 deletions(-) delete mode 100644 LICENSE diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 1bb5a443..00000000 --- a/LICENSE +++ /dev/null @@ -1,17 +0,0 @@ -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to -deal in the Software without restriction, including without limitation the -rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -sell copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -IN THE SOFTWARE. diff --git a/pyproject.toml b/pyproject.toml index 71b1a7da..fa0c801f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ classifiers = [ "Programming Language :: Python :: 3 :: Only", ] requires-python = ">=3.9" +license = "MIT" dependencies = [ ] dynamic = ["version"] From 867396152fcb99055795120750dfda53f85bb414 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Sun, 4 May 2025 22:06:52 +0200 Subject: [PATCH 455/480] Python 3 is the default nowadays (jaraco/skeleton#173) --- .github/workflows/main.yml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 928acf2c..80294970 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -63,7 +63,7 @@ jobs: sudo apt update sudo apt install -y libxml2-dev libxslt-dev - name: Setup Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} allow-prereleases: true @@ -85,9 +85,7 @@ jobs: with: fetch-depth: 0 - name: Setup Python - uses: actions/setup-python@v4 - with: - python-version: 3.x + uses: actions/setup-python@v5 - name: Install tox run: python -m pip install tox - name: Eval ${{ matrix.job }} @@ -119,9 +117,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Setup Python - uses: actions/setup-python@v4 - with: - python-version: 3.x + uses: actions/setup-python@v5 - name: Install tox run: python -m pip install tox - name: Run From d2b8d7750f78e870def98c4e04053af4acc86e29 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 10 May 2025 12:32:22 -0400 Subject: [PATCH 456/480] Add coherent.licensed plugin to inject license texts into the build. Closes jaraco/skeleton#174 --- pyproject.toml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index fa0c801f..bda001a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,10 @@ [build-system] -requires = ["setuptools>=61.2", "setuptools_scm[toml]>=3.4.1"] +requires = [ + "setuptools>=61.2", + "setuptools_scm[toml]>=3.4.1", + # jaraco/skeleton#174 + "coherent.licensed", +] build-backend = "setuptools.build_meta" [project] From b535e75e95389eb8a16e34b238e2483f498593c8 Mon Sep 17 00:00:00 2001 From: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com> Date: Sat, 10 May 2025 18:47:43 +0200 Subject: [PATCH 457/480] Revert "Python 3 is the default nowadays (jaraco/skeleton#173)" (jaraco/skeleton#175) This reverts commit 867396152fcb99055795120750dfda53f85bb414. Removing `python-version` falls back on the Python bundled with the runner, making actions/setup-python a no-op. Here, the maintainer prefers using the latest release of Python 3. This is what `3.x` means: use the latest release of Python 3. --- .github/workflows/main.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 80294970..53513eee 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -86,6 +86,8 @@ jobs: fetch-depth: 0 - name: Setup Python uses: actions/setup-python@v5 + with: + python-version: 3.x - name: Install tox run: python -m pip install tox - name: Eval ${{ matrix.job }} @@ -118,6 +120,8 @@ jobs: - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v5 + with: + python-version: 3.x - name: Install tox run: python -m pip install tox - name: Run From 5a6c1532c206871bc2913349d97dda06e01b9963 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 11 May 2025 23:20:37 -0400 Subject: [PATCH 458/480] Bump to setuptools 77 or later. Closes jaraco/skeleton#176 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index bda001a4..ce6c1709 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [build-system] requires = [ - "setuptools>=61.2", + "setuptools>=77", "setuptools_scm[toml]>=3.4.1", # jaraco/skeleton#174 "coherent.licensed", From 04ff5549ee93f907bcebb1db570ad291ae55fd29 Mon Sep 17 00:00:00 2001 From: Anderson Bravalheri Date: Sun, 22 Jun 2025 13:49:02 +0100 Subject: [PATCH 459/480] Update pre-commit ruff (jaraco/skeleton#181) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 633e3648..fa559241 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.9.9 + rev: v0.12.0 hooks: - id: ruff args: [--fix, --unsafe-fixes] From 8c5810ed39f431598f8498499e7e8fa38a8ed455 Mon Sep 17 00:00:00 2001 From: Avasam Date: Sun, 22 Jun 2025 08:50:30 -0400 Subject: [PATCH 460/480] Log filenames when running pytest-mypy (jaraco/skeleton#177) --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ce6c1709..e916f46b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,12 +58,12 @@ cover = [ ] enabler = [ - "pytest-enabler >= 2.2", + "pytest-enabler >= 3.4", ] type = [ # upstream - "pytest-mypy", + "pytest-mypy >= 1.0.1", # local ] From 07349287790543c73ba8c38a6eb427ca9554f336 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 8 Sep 2025 16:12:40 +0300 Subject: [PATCH 461/480] Remove redundant compatibility code --- pyproject.toml | 2 -- tests/fixtures.py | 6 +----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 530f173f..2daf7922 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,6 @@ requires-python = ">=3.9" license = "Apache-2.0" dependencies = [ "zipp>=3.20", - 'typing-extensions>=3.6.4; python_version < "3.8"', ] dynamic = ["version"] @@ -37,7 +36,6 @@ test = [ "pytest >= 6, != 8.1.*", # local - 'importlib_resources>=1.3; python_version < "3.9"', "packaging", "pyfakefs", "flufl.flake8", diff --git a/tests/fixtures.py b/tests/fixtures.py index 8e692f86..021eb811 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -6,17 +6,13 @@ import shutil import sys import textwrap +from importlib import resources from . import _path from ._path import FilesSpec from .compat.py39 import os_helper from .compat.py312 import import_helper -if sys.version_info >= (3, 9): - from importlib import resources -else: - import importlib_resources as resources - @contextlib.contextmanager def tmp_path(): From d47a969ed4567bbdee26034ccaaa8b8169f44fcf Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 19 Oct 2025 13:06:02 -0400 Subject: [PATCH 462/480] Specify the directory for news fragments. Uses the default as found on towncrier prior to 25 and sets to a predictable value. Fixes jaraco/skeleton#184 --- towncrier.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/towncrier.toml b/towncrier.toml index 6fa480e4..577e87a7 100644 --- a/towncrier.toml +++ b/towncrier.toml @@ -1,2 +1,3 @@ [tool.towncrier] title_format = "{version}" +directory = "newsfragments" # jaraco/skeleton#184 From fc3f315445454c82ff1412770243430ac72fd316 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> Date: Mon, 17 Nov 2025 11:33:30 +0200 Subject: [PATCH 463/480] Replace zipp dependency with stdlib --- importlib_metadata/__init__.py | 2 +- mypy.ini | 4 ---- pyproject.toml | 3 --- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index cdfc1f62..534330d4 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -821,7 +821,7 @@ def children(self): def zip_children(self): # deferred for performance (python/importlib_metadata#502) - from zipp.compat.overlay import zipfile + import zipfile zip_path = zipfile.Path(self.root) names = zip_path.root.namelist() diff --git a/mypy.ini b/mypy.ini index feac94cc..bfb6db30 100644 --- a/mypy.ini +++ b/mypy.ini @@ -18,10 +18,6 @@ disable_error_code = [mypy-pytest_perf.*] ignore_missing_imports = True -# jaraco/zipp#123 -[mypy-zipp.*] -ignore_missing_imports = True - # jaraco/jaraco.test#7 [mypy-jaraco.test.*] ignore_missing_imports = True diff --git a/pyproject.toml b/pyproject.toml index 2daf7922..9c949e83 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,9 +22,6 @@ classifiers = [ ] requires-python = ">=3.9" license = "Apache-2.0" -dependencies = [ - "zipp>=3.20", -] dynamic = ["version"] [project.urls] From 372be3842f8e2d22ebd5968a115ac5cc0eeee604 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 20 Dec 2025 12:06:11 -0500 Subject: [PATCH 464/480] Revert "Replace zipp dependency with stdlib" This reverts commit fc3f315445454c82ff1412770243430ac72fd316. --- importlib_metadata/__init__.py | 2 +- mypy.ini | 4 ++++ pyproject.toml | 3 +++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 534330d4..cdfc1f62 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -821,7 +821,7 @@ def children(self): def zip_children(self): # deferred for performance (python/importlib_metadata#502) - import zipfile + from zipp.compat.overlay import zipfile zip_path = zipfile.Path(self.root) names = zip_path.root.namelist() diff --git a/mypy.ini b/mypy.ini index bfb6db30..feac94cc 100644 --- a/mypy.ini +++ b/mypy.ini @@ -18,6 +18,10 @@ disable_error_code = [mypy-pytest_perf.*] ignore_missing_imports = True +# jaraco/zipp#123 +[mypy-zipp.*] +ignore_missing_imports = True + # jaraco/jaraco.test#7 [mypy-jaraco.test.*] ignore_missing_imports = True diff --git a/pyproject.toml b/pyproject.toml index 9c949e83..2daf7922 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,9 @@ classifiers = [ ] requires-python = ">=3.9" license = "Apache-2.0" +dependencies = [ + "zipp>=3.20", +] dynamic = ["version"] [project.urls] From 49427ed6129e350d9b5eff6dac94486c38c2b04a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 20 Dec 2025 12:07:36 -0500 Subject: [PATCH 465/480] Add news fragment. --- newsfragments/524.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/524.bugfix.rst diff --git a/newsfragments/524.bugfix.rst b/newsfragments/524.bugfix.rst new file mode 100644 index 00000000..80527a0c --- /dev/null +++ b/newsfragments/524.bugfix.rst @@ -0,0 +1 @@ +Removed cruft from Python 3.8. From 40bb485b7fda162c503e2d70eb00a89321bd5fa3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 21 Dec 2025 03:35:30 -0500 Subject: [PATCH 466/480] =?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 | 3 ++- importlib_metadata/_adapters.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index cdfc1f62..03031190 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -636,7 +636,8 @@ def _read_files_egginfo_installed(self): return paths = ( - py311.relative_fix((subdir / name).resolve()) + py311 + .relative_fix((subdir / name).resolve()) .relative_to(self.locate_file('').resolve(), walk_up=True) .as_posix() for name in text.splitlines() diff --git a/importlib_metadata/_adapters.py b/importlib_metadata/_adapters.py index f5b30dd9..dede395d 100644 --- a/importlib_metadata/_adapters.py +++ b/importlib_metadata/_adapters.py @@ -9,7 +9,8 @@ class RawPolicy(email.policy.EmailPolicy): def fold(self, name, value): folded = self.linesep.join( - textwrap.indent(value, prefix=' ' * 8, predicate=lambda line: True) + textwrap + .indent(value, prefix=' ' * 8, predicate=lambda line: True) .lstrip() .splitlines() ) From 8f3d95e7db0114e26e57dd95932b141ead74f7c5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 21 Dec 2025 04:04:12 -0500 Subject: [PATCH 467/480] Pin mypy on PyPy. Closes jaraco/skeleton#188. Ref python/mypy#20454. --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index e916f46b..987b802c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,6 +65,9 @@ type = [ # upstream "pytest-mypy >= 1.0.1", + ## workaround for python/mypy#20454 + "mypy < 1.19; python_implementation == 'PyPy'", + # local ] From cbc721bfacd0ce396dba55235703525a8feaf0ac Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sun, 8 Jun 2025 14:34:45 +0200 Subject: [PATCH 468/480] Fix errors with multiprocessing Before, one could get OSError 22 and BadZipFile errors due to re-used file pointers in forked subprocesses. Fixes #520 --- importlib_metadata/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 03031190..79f356a8 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -803,6 +803,7 @@ class FastPath: True """ + # The following cache is cleared at fork, see os.register_at_fork below @functools.lru_cache() # type: ignore[misc] def __new__(cls, root): return super().__new__(cls) @@ -843,6 +844,10 @@ def mtime(self): def lookup(self, mtime): return Lookup(self) +# Clear FastPath.__new__ cache when forked, avoids trying to re-useing open +# file pointers from zipp.Path/zipfile.Path objects in forked process +os.register_at_fork(after_in_child=FastPath.__new__.cache_clear) + class Lookup: """ From 339d7a57c7190d462c81ee12e60875c69d60f925 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 20 Dec 2025 12:47:03 -0500 Subject: [PATCH 469/480] Added decorator to encapsulate the fork multiprocessing workaround. --- importlib_metadata/__init__.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 79f356a8..a8bf1c93 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -787,6 +787,24 @@ def find_distributions(self, context=Context()) -> Iterable[Distribution]: """ +def _clear_lru_cache_after_fork(func): + """Wrap ``func`` with ``functools.lru_cache`` and clear it after ``fork``. + + ``FastPath`` caches zip-backed ``pathlib.Path`` objects that keep a + reference to the parent's open ``ZipFile`` handle. Re-using a cached + instance in a forked child can therefore resurrect invalid file pointers + and trigger ``BadZipFile``/``OSError`` failures (python/importlib_metadata#520). + Registering ``cache_clear`` with ``os.register_at_fork`` ensures every + process gets a pristine cache and opens its own archive handles. + """ + + cached = functools.lru_cache()(func) + register = getattr(os, 'register_at_fork', None) + if register is not None: + register(after_in_child=cached.cache_clear) + return cached + + class FastPath: """ Micro-optimized class for searching a root for children. @@ -803,8 +821,7 @@ class FastPath: True """ - # The following cache is cleared at fork, see os.register_at_fork below - @functools.lru_cache() # type: ignore[misc] + @_clear_lru_cache_after_fork # type: ignore[misc] def __new__(cls, root): return super().__new__(cls) @@ -844,11 +861,6 @@ def mtime(self): def lookup(self, mtime): return Lookup(self) -# Clear FastPath.__new__ cache when forked, avoids trying to re-useing open -# file pointers from zipp.Path/zipfile.Path objects in forked process -os.register_at_fork(after_in_child=FastPath.__new__.cache_clear) - - class Lookup: """ A micro-optimized class for searching a (fast) path for metadata. From 104265b037f8994588992ebfbdd316cc78e3d457 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 20 Dec 2025 13:19:44 -0500 Subject: [PATCH 470/480] Add test capturing missed expectation. --- tests/test_zip.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/test_zip.py b/tests/test_zip.py index d4f8e2f0..aeb91e79 100644 --- a/tests/test_zip.py +++ b/tests/test_zip.py @@ -1,7 +1,10 @@ +import multiprocessing +import os import sys import unittest from importlib_metadata import ( + FastPath, PackageNotFoundError, distribution, distributions, @@ -47,6 +50,37 @@ def test_one_distribution(self): dists = list(distributions(path=sys.path[:1])) assert len(dists) == 1 + @unittest.skipUnless( + hasattr(os, 'register_at_fork') + and 'fork' in multiprocessing.get_all_start_methods(), + 'requires fork-based multiprocessing support', + ) + def test_fastpath_cache_cleared_in_forked_child(self): + zip_path = sys.path[0] + + FastPath(zip_path) + self.assertEqual(FastPath.__new__.cache_info().currsize, 1) + + ctx = multiprocessing.get_context('fork') + parent_conn, child_conn = ctx.Pipe() + + def child(conn, root): + try: + before = FastPath.__new__.cache_info().currsize + FastPath(root) + after = FastPath.__new__.cache_info().currsize + conn.send((before, after)) + finally: + conn.close() + + proc = ctx.Process(target=child, args=(child_conn, zip_path)) + proc.start() + child_conn.close() + cache_sizes = parent_conn.recv() + proc.join() + + self.assertEqual(cache_sizes, (0, 1)) + class TestEgg(TestZip): def setUp(self): From 6a30ab96290b18c0b9805268a201ca5011c1feae Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 21 Dec 2025 03:27:23 -0500 Subject: [PATCH 471/480] Allow initial currsize to be greater than one (as happens when running the test suite). --- tests/test_zip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_zip.py b/tests/test_zip.py index aeb91e79..165aa6dd 100644 --- a/tests/test_zip.py +++ b/tests/test_zip.py @@ -59,7 +59,7 @@ def test_fastpath_cache_cleared_in_forked_child(self): zip_path = sys.path[0] FastPath(zip_path) - self.assertEqual(FastPath.__new__.cache_info().currsize, 1) + assert FastPath.__new__.cache_info().currsize >= 1 ctx = multiprocessing.get_context('fork') parent_conn, child_conn = ctx.Pipe() From 4e962a8498990ba82120e7a58ce71abedefa0003 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 21 Dec 2025 03:27:37 -0500 Subject: [PATCH 472/480] =?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 + 1 file changed, 1 insertion(+) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index a8bf1c93..3e436d24 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -861,6 +861,7 @@ def mtime(self): def lookup(self, mtime): return Lookup(self) + class Lookup: """ A micro-optimized class for searching a (fast) path for metadata. From a1c25d8f2dc50abec65e4cf6d733b15d73c2f3b1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 21 Dec 2025 03:29:36 -0500 Subject: [PATCH 473/480] =?UTF-8?q?=F0=9F=A7=8E=E2=80=8D=E2=99=80=EF=B8=8F?= =?UTF-8?q?=20Genuflect=20to=20the=20types.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 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 3e436d24..68f9b5f9 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -821,7 +821,7 @@ class FastPath: True """ - @_clear_lru_cache_after_fork # type: ignore[misc] + @_clear_lru_cache_after_fork def __new__(cls, root): return super().__new__(cls) From 1da3f456ab53832fd6e1236f2338388d9ea0b0c6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 21 Dec 2025 04:09:52 -0500 Subject: [PATCH 474/480] Add news fragment. --- newsfragments/520.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 newsfragments/520.bugfix.rst diff --git a/newsfragments/520.bugfix.rst b/newsfragments/520.bugfix.rst new file mode 100644 index 00000000..1fbe7cec --- /dev/null +++ b/newsfragments/520.bugfix.rst @@ -0,0 +1 @@ +Fixed errors in FastPath under fork-multiprocessing. From 8dd2937cf852eb0d9ad96d4e45ed3470e80c1463 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 21 Dec 2025 04:17:14 -0500 Subject: [PATCH 475/480] Decouple clear_after_fork from lru_cache and then compose. --- importlib_metadata/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 68f9b5f9..15ecb0b2 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -787,18 +787,17 @@ def find_distributions(self, context=Context()) -> Iterable[Distribution]: """ -def _clear_lru_cache_after_fork(func): - """Wrap ``func`` with ``functools.lru_cache`` and clear it after ``fork``. +def _clear_after_fork(cached): + """Ensure ``func`` clears cached state after ``fork`` when supported. - ``FastPath`` caches zip-backed ``pathlib.Path`` objects that keep a + ``FastPath`` caches zip-backed ``pathlib.Path`` objects that retain a reference to the parent's open ``ZipFile`` handle. Re-using a cached instance in a forked child can therefore resurrect invalid file pointers and trigger ``BadZipFile``/``OSError`` failures (python/importlib_metadata#520). - Registering ``cache_clear`` with ``os.register_at_fork`` ensures every - process gets a pristine cache and opens its own archive handles. + Registering ``cache_clear`` with ``os.register_at_fork`` keeps each process + on its own cache. """ - cached = functools.lru_cache()(func) register = getattr(os, 'register_at_fork', None) if register is not None: register(after_in_child=cached.cache_clear) @@ -821,7 +820,8 @@ class FastPath: True """ - @_clear_lru_cache_after_fork + @_clear_after_fork + @functools.lru_cache() def __new__(cls, root): return super().__new__(cls) From a36bab926643dcd67513851d5bebc285ef9ac681 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 21 Dec 2025 04:22:17 -0500 Subject: [PATCH 476/480] Avoid if block. --- importlib_metadata/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 15ecb0b2..22824be8 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -797,10 +797,9 @@ def _clear_after_fork(cached): Registering ``cache_clear`` with ``os.register_at_fork`` keeps each process on its own cache. """ - - register = getattr(os, 'register_at_fork', None) - if register is not None: - register(after_in_child=cached.cache_clear) + getattr(os, 'register_at_fork', lambda **kw: None)( + after_in_child=cached.cache_clear, + ) return cached From 3c9510bf848fd4031e76028da0c9f60129047546 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 21 Dec 2025 04:28:24 -0500 Subject: [PATCH 477/480] Prefer noop for degenerate behavior. --- importlib_metadata/__init__.py | 6 ++---- importlib_metadata/_functools.py | 9 +++++++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 22824be8..df9ff61a 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -35,7 +35,7 @@ NullFinder, install, ) -from ._functools import method_cache, pass_none +from ._functools import method_cache, noop, pass_none from ._itertools import always_iterable, bucket, unique_everseen from ._meta import PackageMetadata, SimplePath from ._typing import md_none @@ -797,9 +797,7 @@ def _clear_after_fork(cached): Registering ``cache_clear`` with ``os.register_at_fork`` keeps each process on its own cache. """ - getattr(os, 'register_at_fork', lambda **kw: None)( - after_in_child=cached.cache_clear, - ) + getattr(os, 'register_at_fork', noop)(after_in_child=cached.cache_clear) return cached diff --git a/importlib_metadata/_functools.py b/importlib_metadata/_functools.py index 5dda6a21..8dcec720 100644 --- a/importlib_metadata/_functools.py +++ b/importlib_metadata/_functools.py @@ -102,3 +102,12 @@ def wrapper(param, *args, **kwargs): return func(param, *args, **kwargs) return wrapper + + +# From jaraco.functools 4.4 +def noop(*args, **kwargs): + """ + A no-operation function that does nothing. + + >>> noop(1, 2, three=3) + """ From f6eee5671a3e9e1cb56a6d3a6219145c19518713 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 21 Dec 2025 04:48:18 -0500 Subject: [PATCH 478/480] Rely on passthrough to designate a wrapper for its side effect. --- importlib_metadata/__init__.py | 6 +++--- importlib_metadata/_functools.py | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index df9ff61a..508b02e4 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -35,7 +35,7 @@ NullFinder, install, ) -from ._functools import method_cache, noop, pass_none +from ._functools import method_cache, noop, pass_none, passthrough from ._itertools import always_iterable, bucket, unique_everseen from ._meta import PackageMetadata, SimplePath from ._typing import md_none @@ -787,6 +787,7 @@ def find_distributions(self, context=Context()) -> Iterable[Distribution]: """ +@passthrough def _clear_after_fork(cached): """Ensure ``func`` clears cached state after ``fork`` when supported. @@ -798,7 +799,6 @@ def _clear_after_fork(cached): on its own cache. """ getattr(os, 'register_at_fork', noop)(after_in_child=cached.cache_clear) - return cached class FastPath: @@ -817,7 +817,7 @@ class FastPath: True """ - @_clear_after_fork + @_clear_after_fork # type: ignore[misc] @functools.lru_cache() def __new__(cls, root): return super().__new__(cls) diff --git a/importlib_metadata/_functools.py b/importlib_metadata/_functools.py index 8dcec720..b1fd04a8 100644 --- a/importlib_metadata/_functools.py +++ b/importlib_metadata/_functools.py @@ -1,5 +1,6 @@ import functools import types +from typing import Callable, TypeVar # from jaraco.functools 3.3 @@ -111,3 +112,24 @@ def noop(*args, **kwargs): >>> noop(1, 2, three=3) """ + + +_T = TypeVar('_T') + + +# From jaraco.functools 4.4 +def passthrough(func: Callable[..., object]) -> Callable[[_T], _T]: + """ + Wrap the function to always return the first parameter. + + >>> passthrough(print)('3') + 3 + '3' + """ + + @functools.wraps(func) + def wrapper(first: _T, *args, **kwargs) -> _T: + func(first, *args, **kwargs) + return first + + return wrapper # type: ignore[return-value] From 84e9028d39062af975d0659c0e987c28bcc808a5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 21 Dec 2025 04:54:12 -0500 Subject: [PATCH 479/480] Finalize --- NEWS.rst | 10 ++++++++++ newsfragments/520.bugfix.rst | 1 - newsfragments/524.bugfix.rst | 1 - 3 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 newsfragments/520.bugfix.rst delete mode 100644 newsfragments/524.bugfix.rst diff --git a/NEWS.rst b/NEWS.rst index 4d0c4bdc..1a92cd19 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,13 @@ +v8.7.1 +====== + +Bugfixes +-------- + +- Fixed errors in FastPath under fork-multiprocessing. (#520) +- Removed cruft from Python 3.8. (#524) + + v8.7.0 ====== diff --git a/newsfragments/520.bugfix.rst b/newsfragments/520.bugfix.rst deleted file mode 100644 index 1fbe7cec..00000000 --- a/newsfragments/520.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed errors in FastPath under fork-multiprocessing. diff --git a/newsfragments/524.bugfix.rst b/newsfragments/524.bugfix.rst deleted file mode 100644 index 80527a0c..00000000 --- a/newsfragments/524.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Removed cruft from Python 3.8. From d8a7576dedb16de480e1d8798d2a02771f8eb844 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 29 Dec 2025 11:40:24 -0500 Subject: [PATCH 480/480] Remove dependency on flufl.flake8 Closes #527 --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a367f162..b71b9a9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,7 +38,6 @@ test = [ # local "packaging", "pyfakefs", - "flufl.flake8", "pytest-perf >= 0.9.2", "jaraco.test >= 5.4", ]