New Upstream Release - python-importlib-metadata
Ready changes
Summary
Merged new upstream version: 6.6.0 (was: 5.2.0).
Resulting package
Built on 2023-05-27T01:57 (took 5m31s)
The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:
apt install -t fresh-releases python3-importlib-metadata
Lintian Result
Diff
diff --git a/.coveragerc b/.coveragerc
index 1b8c50e..1763e9e 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -8,6 +8,8 @@ omit =
*/_itertools.py
exercises.py
*/pip-run-*
+disable_warnings =
+ couldnt-parse
[report]
show_missing = True
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..5cbfe04
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1 @@
+tidelift: pypi/importlib-metadata
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 19b0342..1269e2e 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -2,21 +2,71 @@ 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
+ # <blank>, 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:
- # 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.11"
+ - "3.12"
+ # Workaround for actions/setup-python#508
+ dev:
+ - -dev
platform:
- ubuntu-latest
- 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
+ - platform: ubuntu-latest
+ python: "3.8"
+ - platform: ubuntu-latest
+ python: "3.9"
runs-on: ${{ matrix.platform }}
+ continue-on-error: ${{ matrix.python == '3.12' }}
steps:
- uses: actions/checkout@v3
with:
@@ -24,9 +74,25 @@ jobs:
# ref actions/checkout#448
fetch-depth: 0
- name: Setup Python
- uses: actions/setup-python@v3
+ 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
+
+ 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 }}
+ python-version: ${{ matrix.python }}${{ matrix.dev }}
- name: Install tox
run: |
python -m pip install tox
@@ -38,6 +104,7 @@ jobs:
needs:
- test
+ - docs
runs-on: ubuntu-latest
@@ -66,6 +133,8 @@ jobs:
TOXENV: diffcov
release:
+ permissions:
+ contents: write
needs:
- check
if: github.event_name == 'push' && contains(github.ref, 'refs/tags/')
@@ -74,9 +143,9 @@ jobs:
steps:
- uses: actions/checkout@v3
- name: Setup Python
- uses: actions/setup-python@v3
+ uses: actions/setup-python@v4
with:
- python-version: "3.10"
+ python-version: 3.11-dev
- name: Install tox
run: |
python -m pip install tox
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index edf6f55..af50201 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
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 0000000..6bef349
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,13 @@
+version: 2
+python:
+ install:
+ - path: .
+ extra_requirements:
+ - docs
+
+# workaround for readthedocs/readthedocs.org#9623
+build:
+ # workaround for readthedocs/readthedocs.org#9635
+ os: ubuntu-22.04
+ tools:
+ python: "3"
diff --git a/.readthedocs.yml b/.readthedocs.yml
deleted file mode 100644
index cc69854..0000000
--- a/.readthedocs.yml
+++ /dev/null
@@ -1,6 +0,0 @@
-version: 2
-python:
- install:
- - path: .
- extra_requirements:
- - docs
diff --git a/CHANGES.rst b/CHANGES.rst
index 84684ee..c7e5889 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,3 +1,103 @@
+v6.6.0
+======
+
+* #449: Expanded type annotations.
+
+v6.5.1
+======
+
+* python/cpython#103661: Removed excess error suppression in
+ ``_read_files_egginfo_installed`` and fixed path handling
+ on Windows.
+
+v6.5.0
+======
+
+* #422: Removed ABC metaclass from ``Distribution`` and instead
+ deprecated construction of ``Distribution`` objects without
+ concrete methods.
+
+v6.4.1
+======
+
+* Updated docs with tweaks from upstream CPython.
+
+v6.4.0
+======
+
+* Consolidated some behaviors in tests around ``_path``.
+* Added type annotation for ``Distribution.read_text``.
+
+v6.3.0
+======
+
+* #115: Support ``installed-files.txt`` for ``Distribution.files``
+ when present.
+
+v6.2.1
+======
+
+* #442: Fixed issue introduced in v6.1.0 where non-importable
+ names (metadata dirs) began appearing in
+ ``packages_distributions``.
+
+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
+======
+
+* #428: ``packages_distributions`` now honors packages and modules
+ with Python modules that not ``.py`` sources (e.g. ``.pyc``,
+ ``.so``).
+
+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
+======
+
+* #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
+======
+
+* #371: Deprecated expectation that ``PackageMetadata.__getitem__``
+ will return ``None`` for missing keys. In the future, it will raise a
+ ``KeyError``.
+
+v5.1.0
+======
+
+* #415: Instrument ``SimplePath`` with generic support.
+
+v5.0.0
+======
+
+* #97, #284, #300: Removed compatibility shims for deprecated entry
+ point interfaces.
+
+v4.13.0
+=======
+
+* #396: Added compatibility for ``PathDistributions`` originating
+ from Python 3.8 and 3.9.
+
v4.12.0
=======
diff --git a/PKG-INFO b/PKG-INFO
index 221ab88..fe2d9af 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: importlib_metadata
-Version: 4.12.0
+Version: 6.6.0
Summary: Read metadata from Python packages
Home-page: https://github.com/python/importlib_metadata
Author: Jason R. Coombs
@@ -17,12 +17,9 @@ Provides-Extra: perf
License-File: LICENSE
.. image:: https://img.shields.io/pypi/v/importlib_metadata.svg
- :target: `PyPI link`_
+ :target: https://pypi.org/project/importlib_metadata
.. image:: https://img.shields.io/pypi/pyversions/importlib_metadata.svg
- :target: `PyPI link`_
-
-.. _PyPI link: https://pypi.org/project/importlib_metadata
.. image:: https://github.com/python/importlib_metadata/workflows/tests/badge.svg
:target: https://github.com/python/importlib_metadata/actions?query=workflow%3A%22tests%22
@@ -35,9 +32,11 @@ License-File: LICENSE
.. image:: https://readthedocs.org/projects/importlib-metadata/badge/?version=latest
:target: https://importlib-metadata.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
+.. image:: https://tidelift.com/badges/package/pypi/importlib-metadata
+ :target: https://tidelift.com/subscription/pkg/pypi-importlib-metadata?utm_source=pypi-importlib-metadata&utm_medium=readme
Library to access the metadata for a Python package.
@@ -58,9 +57,11 @@ were contributed to different versions in the standard library:
* - importlib_metadata
- stdlib
- * - 4.8
+ * - 6.5
+ - 3.12
+ * - 4.13
- 3.11
- * - 4.4
+ * - 4.6
- 3.10
* - 1.4
- 3.8
@@ -69,7 +70,7 @@ were contributed to different versions in the standard library:
Usage
=====
-See the `online documentation <https://importlib_metadata.readthedocs.io/>`_
+See the `online documentation <https://importlib-metadata.readthedocs.io/>`_
for usage details.
`Finder authors
@@ -93,4 +94,20 @@ 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
+==============
+
+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 <https://tidelift.com/subscription/pkg/pypi-importlib-metadata?utm_source=pypi-importlib-metadata&utm_medium=referral&utm_campaign=github>`_.
+
+Security Contact
+================
+
+To report a security vulnerability, please use the
+`Tidelift security contact <https://tidelift.com/security>`_.
+Tidelift will coordinate the fix and disclosure.
diff --git a/README.rst b/README.rst
index 78a6af7..a315c0f 100644
--- a/README.rst
+++ b/README.rst
@@ -1,10 +1,7 @@
.. image:: https://img.shields.io/pypi/v/importlib_metadata.svg
- :target: `PyPI link`_
+ :target: https://pypi.org/project/importlib_metadata
.. image:: https://img.shields.io/pypi/pyversions/importlib_metadata.svg
- :target: `PyPI link`_
-
-.. _PyPI link: https://pypi.org/project/importlib_metadata
.. image:: https://github.com/python/importlib_metadata/workflows/tests/badge.svg
:target: https://github.com/python/importlib_metadata/actions?query=workflow%3A%22tests%22
@@ -17,9 +14,11 @@
.. image:: https://readthedocs.org/projects/importlib-metadata/badge/?version=latest
:target: https://importlib-metadata.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
+.. image:: https://tidelift.com/badges/package/pypi/importlib-metadata
+ :target: https://tidelift.com/subscription/pkg/pypi-importlib-metadata?utm_source=pypi-importlib-metadata&utm_medium=readme
Library to access the metadata for a Python package.
@@ -40,9 +39,11 @@ were contributed to different versions in the standard library:
* - importlib_metadata
- stdlib
- * - 4.8
+ * - 6.5
+ - 3.12
+ * - 4.13
- 3.11
- * - 4.4
+ * - 4.6
- 3.10
* - 1.4
- 3.8
@@ -51,7 +52,7 @@ were contributed to different versions in the standard library:
Usage
=====
-See the `online documentation <https://importlib_metadata.readthedocs.io/>`_
+See the `online documentation <https://importlib-metadata.readthedocs.io/>`_
for usage details.
`Finder authors
@@ -75,4 +76,20 @@ 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
+==============
+
+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 <https://tidelift.com/subscription/pkg/pypi-importlib-metadata?utm_source=pypi-importlib-metadata&utm_medium=referral&utm_campaign=github>`_.
+
+Security Contact
+================
+
+To report a security vulnerability, please use the
+`Tidelift security contact <https://tidelift.com/security>`_.
+Tidelift will coordinate the fix and disclosure.
diff --git a/debian/changelog b/debian/changelog
index 10bb0fe..350f234 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,10 @@
+python-importlib-metadata (6.6.0-1) UNRELEASED; urgency=low
+
+ * New upstream release.
+ * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk> Sat, 27 May 2023 01:52:41 -0000
+
python-importlib-metadata (4.12.0-1) unstable; urgency=medium
* Team upload.
diff --git a/docs/conf.py b/docs/conf.py
index ec2bfe5..4c707b2 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -1,9 +1,13 @@
-#!/usr/bin/env python3
-
-extensions = ['sphinx.ext.autodoc', 'jaraco.packaging.sphinx', 'rst.linker']
+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 = {
'../CHANGES.rst': dict(
using=dict(GH='https://github.com'),
@@ -28,7 +32,7 @@ link_files = {
)
}
-# Be strict about any broken references:
+# Be strict about any broken references
nitpicky = True
# Include Python intersphinx mapping to prevent failures
@@ -38,6 +42,11 @@ intersphinx_mapping = {
'python': ('https://docs.python.org/3', None),
}
+# Preserve authored syntax for defaults
+autodoc_preserve_defaults = True
+
+extensions += ['jaraco.tidelift']
+
intersphinx_mapping.update(
importlib_resources=(
'https://importlib-resources.readthedocs.io/en/latest/',
@@ -45,9 +54,21 @@ intersphinx_mapping.update(
),
)
-# Workaround for #316
+intersphinx_mapping.update(
+ packaging=(
+ 'https://packaging.python.org/en/latest/',
+ None,
+ ),
+)
+
nitpick_ignore = [
+ # Workaround for #316
('py:class', 'importlib_metadata.EntryPoints'),
+ ('py:class', 'importlib_metadata.PackagePath'),
('py:class', 'importlib_metadata.SelectableGroups'),
('py:class', 'importlib_metadata._meta._T'),
+ # Workaround for #435
+ ('py:class', '_T'),
+ # Other workarounds
+ ('py:class', 'importlib_metadata.DeprecatedNonAbstract'),
]
diff --git a/docs/index.rst b/docs/index.rst
index 1ebbf34..a5bacd4 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 <importlib_resources:index>`),
-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
@@ -27,6 +19,8 @@ The documentation here includes a general :ref:`usage <using>` guide.
migration
history
+.. tidelift-referral-banner::
+
Project details
===============
@@ -34,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
@@ -43,7 +37,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 8bd92f6..17af34d 100644
--- a/docs/using.rst
+++ b/docs/using.rst
@@ -4,22 +4,39 @@
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
+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` (with new features backported to
-:doc:`importlib_resources <importlib_resources:index>`),
-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
-Python's ``site-packages`` directory via tools such as `pip
-<https://pypi.org/project/pip/>`_. Specifically,
-it means a package 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
+``importlib_metadata`` operates on third-party *distribution packages*
+installed into Python's ``site-packages`` directory via tools such as
+`pip <https://pypi.org/project/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() <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.
@@ -29,19 +46,26 @@ anywhere.
https://importlib-metadata.readthedocs.io/
The documentation for ``importlib_metadata``, which supplies a
backport of ``importlib.metadata``.
+ This includes an `API reference
+ <https://importlib-metadata.readthedocs.io/en/latest/api.html>`__
+ for this module's classes and functions,
+ as well as a `migration guide
+ <https://importlib-metadata.readthedocs.io/en/latest/migration.html>`__
+ for existing users of ``pkg_resources``.
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
+:term:`packaging:Distribution Package` you've installed
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) $ 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::
@@ -132,7 +156,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
-<https://setuptools.readthedocs.io/en/latest/setuptools.html#dynamic-discovery-of-services-and-plugins>`_
+<https://setuptools.pypa.io/en/latest/userguide/entry_point.html>`_
for more information on entry points, their definition, and usage.
*Compatibility Note*
@@ -140,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 <https://pypi.org/project/backports.entry_points_selectable>`_
+for compatibility options.
.. _metadata:
@@ -152,7 +175,8 @@ interface to retrieve entry points by group.
Distribution metadata
---------------------
-Every distribution includes some metadata, which you can extract using the
+Every :term:`packaging:Distribution Package` includes some metadata,
+which you can extract using the
``metadata()`` function::
>>> wheel_metadata = metadata('wheel')
@@ -175,7 +199,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 <https://importlib-metadata.readthedocs.io/en/latest/api.html#importlib_metadata.PackageMetadata>`.
+ `PackageMetadata protocol <https://importlib-metadata.readthedocs.io/en/latest/api.html#importlib_metadata.PackageMetadata>`_.
.. _version:
@@ -183,7 +207,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
+:term:`packaging:Distribution Package`'s version
number, as a string::
>>> version('wheel')
@@ -196,7 +221,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 :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::
@@ -241,23 +267,32 @@ 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 :term:`packaging: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 :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
+<https://github.com/pypa/packaging-problems/issues/609>`_, and thus this
+function is not reliable with such installs.
+
.. _distributions:
Distributions
@@ -265,7 +300,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 :term:`packaging:Distribution Package`. You can
get the ``Distribution`` instance::
>>> from importlib_metadata import distribution
@@ -285,14 +321,16 @@ 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
======================
-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 :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.
@@ -301,15 +339,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
-system `finders`_. To find a distribution package's 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
+system :ref:`finders <finders-and-loaders>`. To find a distribution package's metadata,
``importlib.metadata`` queries the list of :term:`meta path finders <meta path finder>` 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.
@@ -338,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
diff --git a/importlib_metadata.egg-info/PKG-INFO b/importlib_metadata.egg-info/PKG-INFO
index fc74cb7..5f9d7a5 100644
--- a/importlib_metadata.egg-info/PKG-INFO
+++ b/importlib_metadata.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: importlib-metadata
-Version: 4.12.0
+Version: 6.6.0
Summary: Read metadata from Python packages
Home-page: https://github.com/python/importlib_metadata
Author: Jason R. Coombs
@@ -17,12 +17,9 @@ Provides-Extra: perf
License-File: LICENSE
.. image:: https://img.shields.io/pypi/v/importlib_metadata.svg
- :target: `PyPI link`_
+ :target: https://pypi.org/project/importlib_metadata
.. image:: https://img.shields.io/pypi/pyversions/importlib_metadata.svg
- :target: `PyPI link`_
-
-.. _PyPI link: https://pypi.org/project/importlib_metadata
.. image:: https://github.com/python/importlib_metadata/workflows/tests/badge.svg
:target: https://github.com/python/importlib_metadata/actions?query=workflow%3A%22tests%22
@@ -35,9 +32,11 @@ License-File: LICENSE
.. image:: https://readthedocs.org/projects/importlib-metadata/badge/?version=latest
:target: https://importlib-metadata.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
+.. image:: https://tidelift.com/badges/package/pypi/importlib-metadata
+ :target: https://tidelift.com/subscription/pkg/pypi-importlib-metadata?utm_source=pypi-importlib-metadata&utm_medium=readme
Library to access the metadata for a Python package.
@@ -58,9 +57,11 @@ were contributed to different versions in the standard library:
* - importlib_metadata
- stdlib
- * - 4.8
+ * - 6.5
+ - 3.12
+ * - 4.13
- 3.11
- * - 4.4
+ * - 4.6
- 3.10
* - 1.4
- 3.8
@@ -69,7 +70,7 @@ were contributed to different versions in the standard library:
Usage
=====
-See the `online documentation <https://importlib_metadata.readthedocs.io/>`_
+See the `online documentation <https://importlib-metadata.readthedocs.io/>`_
for usage details.
`Finder authors
@@ -93,4 +94,20 @@ 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
+==============
+
+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 <https://tidelift.com/subscription/pkg/pypi-importlib-metadata?utm_source=pypi-importlib-metadata&utm_medium=referral&utm_campaign=github>`_.
+
+Security Contact
+================
+
+To report a security vulnerability, please use the
+`Tidelift security contact <https://tidelift.com/security>`_.
+Tidelift will coordinate the fix and disclosure.
diff --git a/importlib_metadata.egg-info/SOURCES.txt b/importlib_metadata.egg-info/SOURCES.txt
index fc179ef..a001623 100644
--- a/importlib_metadata.egg-info/SOURCES.txt
+++ b/importlib_metadata.egg-info/SOURCES.txt
@@ -3,7 +3,7 @@
.flake8
.gitignore
.pre-commit-config.yaml
-.readthedocs.yml
+.readthedocs.yaml
CHANGES.rst
LICENSE
README.rst
@@ -14,6 +14,7 @@ pyproject.toml
pytest.ini
setup.cfg
tox.ini
+.github/FUNDING.yml
.github/dependabot.yml
.github/workflows/main.yml
docs/__init__.py
@@ -30,6 +31,7 @@ importlib_metadata/_compat.py
importlib_metadata/_functools.py
importlib_metadata/_itertools.py
importlib_metadata/_meta.py
+importlib_metadata/_py39compat.py
importlib_metadata/_text.py
importlib_metadata/py.typed
importlib_metadata.egg-info/PKG-INFO
@@ -42,11 +44,14 @@ prepare/example/example/__init__.py
prepare/example2/pyproject.toml
prepare/example2/example2/__init__.py
tests/__init__.py
+tests/_context.py
+tests/_path.py
tests/fixtures.py
tests/py39compat.py
tests/test_api.py
tests/test_integration.py
tests/test_main.py
+tests/test_py39compat.py
tests/test_zip.py
tests/data/__init__.py
tests/data/example-21.12-py3-none-any.whl
diff --git a/importlib_metadata.egg-info/requires.txt b/importlib_metadata.egg-info/requires.txt
index 3704643..6a508bb 100644
--- a/importlib_metadata.egg-info/requires.txt
+++ b/importlib_metadata.egg-info/requires.txt
@@ -4,9 +4,12 @@ zipp>=0.5
typing-extensions>=3.6.4
[docs]
-sphinx
+sphinx>=3.5
jaraco.packaging>=9
rst.linker>=1.9
+furo
+sphinx-lint
+jaraco.tidelift>=1.4
[perf]
ipython
@@ -14,7 +17,7 @@ ipython
[testing]
pytest>=6
pytest-checkdocs>=2.4
-pytest-flake8
+flake8<5
pytest-cov
pytest-enabler>=1.3
packaging
@@ -26,5 +29,8 @@ pytest-perf>=0.9.2
pytest-black>=0.3.7
pytest-mypy>=0.9.1
+[testing:python_version < "3.12"]
+pytest-flake8
+
[testing:python_version < "3.9"]
importlib_resources>=1.3
diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py
index 8761307..281cfb0 100644
--- a/importlib_metadata/__init__.py
+++ b/importlib_metadata/__init__.py
@@ -5,6 +5,7 @@ import csv
import sys
import zipp
import email
+import inspect
import pathlib
import operator
import textwrap
@@ -12,12 +13,14 @@ import warnings
import functools
import itertools
import posixpath
+import contextlib
import collections
-from . import _adapters, _meta
+from . import _adapters, _meta, _py39compat
from ._collections import FreezableDefaultDict, Pair
from ._compat import (
NullFinder,
+ StrPath,
install,
pypy_partial,
)
@@ -29,8 +32,7 @@ from contextlib import suppress
from importlib import import_module
from importlib.abc import MetaPathFinder
from itertools import starmap
-from typing import List, Mapping, Optional, Union
-
+from typing import Iterable, List, Mapping, Optional, Set, cast
__all__ = [
'Distribution',
@@ -51,11 +53,11 @@ __all__ = [
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
@@ -121,7 +123,7 @@ class Sectioned:
yield Pair(name, value)
@staticmethod
- def valid(line):
+ def valid(line: str):
return line and not line.startswith('#')
@@ -139,6 +141,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.",
@@ -189,9 +192,13 @@ 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):
+ def __init__(self, name: str, value: str, group: str) -> None:
vars(self).update(name=name, value=value, group=group)
def load(self):
@@ -205,35 +212,27 @@ class EntryPoint(DeprecatedTuple):
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):
vars(self).update(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 matches(self, **params):
"""
EntryPoint matches the given parameters.
@@ -275,99 +274,21 @@ class EntryPoint(DeprecatedTuple):
f'group={self.group!r})'
)
- def __hash__(self):
+ def __hash__(self) -> int:
return hash(self._key())
-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
- """
-
- __slots__ = ()
-
- _warn = functools.partial(
- warnings.warn,
- "EntryPoints list interface is deprecated. Cast to list if needed.",
- DeprecationWarning,
- stacklevel=pypy_partial(2),
- )
-
- def _wrap_deprecated_method(method_name: str): # type: ignore
- def wrapped(self, *args, **kwargs):
- self._warn()
- return getattr(super(), method_name)(*args, **kwargs)
-
- return method_name, wrapped
-
- locals().update(
- map(
- _wrap_deprecated_method,
- '__setitem__ __delitem__ append reverse extend pop remove '
- '__iadd__ insert sort'.split(),
- )
- )
-
- def __add__(self, other):
- if not isinstance(other, tuple):
- self._warn()
- other = tuple(other)
- return self.__class__(tuple(self) + other)
-
- 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.
"""
__slots__ = ()
- def __getitem__(self, name): # -> EntryPoint:
+ def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override]
"""
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:
@@ -378,23 +299,19 @@ class EntryPoints(DeprecatedList):
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))
+ 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.
-
- For coverage while SelectableGroups is present.
- >>> EntryPoints().groups
- set()
"""
return {ep.group for ep in self}
@@ -410,130 +327,58 @@ class EntryPoints(DeprecatedList):
)
-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=pypy_partial(2),
- )
-
- def __getitem__(self, name):
- self._warn()
- return super().__getitem__(name)
-
- def get(self, name, default=None):
- 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"""
- 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'<FileHash mode: {self.mode} value: {self.value}>'
-class Distribution:
+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
- 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.
@@ -541,14 +386,14 @@ class Distribution:
"""
@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.
@@ -561,12 +406,12 @@ class 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):
+ def discover(cls, **kwargs) -> Iterable["Distribution"]:
"""Return an iterable of Distribution objects for all packages.
Pass a ``context`` or pass keyword arguments for constructing
@@ -584,7 +429,7 @@ class Distribution:
)
@staticmethod
- def at(path):
+ def at(path: StrPath) -> "Distribution":
"""Return a Distribution for the indicated metadata path
:param path: a string or path-like object
@@ -607,7 +452,7 @@ class Distribution:
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
@@ -615,10 +460,11 @@ class Distribution:
# (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
- def name(self):
+ def name(self) -> str:
"""Return the 'Name' metadata for the distribution package."""
return self.metadata['Name']
@@ -628,23 +474,23 @@ class Distribution:
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
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.
"""
@@ -657,9 +503,19 @@ class Distribution:
@pass_none
def make_files(lines):
- return list(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())
+ @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):
"""
@@ -668,16 +524,51 @@ class Distribution:
text = self.read_text('RECORD')
return text and text.splitlines()
- def _read_files_egginfo(self):
+ def _read_files_egginfo_installed(self):
"""
- SOURCES.txt might contain literal commas, so wrap each line
- in quotes.
+ 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.
+ Assume the file is accurate if it exists.
+ """
+ text = self.read_text('installed-files.txt')
+ # 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
+
+ paths = (
+ (subdir / name)
+ .resolve()
+ .relative_to(self.locate_file('').resolve())
+ .as_posix()
+ for name in text.splitlines()
+ )
+ return map('"{}"'.format, paths)
+
+ def _read_files_egginfo_sources(self):
+ """
+ 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
+ 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())
@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)
@@ -756,7 +647,7 @@ class DistributionFinder(MetaPathFinder):
vars(self).update(kwargs)
@property
- def path(self):
+ def path(self) -> List[str]:
"""
The sequence of directory path that a distribution finder
should search.
@@ -767,7 +658,7 @@ class DistributionFinder(MetaPathFinder):
return vars(self).get('path', sys.path)
@abc.abstractmethod
- def find_distributions(self, context=Context()):
+ def find_distributions(self, context=Context()) -> Iterable[Distribution]:
"""
Find distributions.
@@ -902,7 +793,9 @@ 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()
+ ) -> Iterable["PathDistribution"]:
"""
Find distributions.
@@ -922,19 +815,19 @@ class MetadataPathFinder(NullFinder, DistributionFinder):
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,
@@ -944,9 +837,11 @@ class PathDistribution(Distribution):
):
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
@@ -979,7 +874,7 @@ class PathDistribution(Distribution):
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.
@@ -988,7 +883,7 @@ def distribution(distribution_name):
return Distribution.from_name(distribution_name)
-def distributions(**kwargs):
+def distributions(**kwargs) -> Iterable[Distribution]:
"""Get all ``Distribution`` instances in the current environment.
:return: An iterable of ``Distribution`` instances.
@@ -1005,7 +900,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.
@@ -1017,37 +912,29 @@ def version(distribution_name):
_unique = functools.partial(
unique_everseen,
- key=operator.attrgetter('_normalized_name'),
+ key=_py39compat.normalized_name,
)
"""
Wrapper for ``distributions`` to return unique distributions by name.
"""
-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.
"""
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):
+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.
@@ -1056,11 +943,11 @@ 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.
- :return: An iterator of requirements, suitable for
+ :return: An iterable of requirements, suitable for
packaging.requirement.Requirement.
"""
return distribution(distribution_name).requires
@@ -1088,8 +975,13 @@ def _top_level_declared(dist):
def _top_level_inferred(dist):
- return {
- f.parts[0] if len(f.parts) > 1 else f.with_suffix('').name
+ opt_names = {
+ f.parts[0] if len(f.parts) > 1 else inspect.getmodulename(f)
for f in always_iterable(dist.files)
- if f.suffix == ".py"
}
+
+ @pass_none
+ def importable_name(name):
+ return '.' not in name
+
+ return filter(importable_name, opt_names)
diff --git a/importlib_metadata/_adapters.py b/importlib_metadata/_adapters.py
index aa460d3..e33cba5 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 @@ class Message(email.message.Message):
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/importlib_metadata/_compat.py b/importlib_metadata/_compat.py
index 3d78566..638e779 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
diff --git a/importlib_metadata/_meta.py b/importlib_metadata/_meta.py
index 37ee43e..0c7e879 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,21 @@ class PackageMetadata(Protocol):
def __iter__(self) -> Iterator[str]:
... # pragma: no cover
- def get_all(self, name: str, failobj: _T = ...) -> Union[List[Any], _T]:
+ @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]]:
+ ... # pragma: no cover
+
+ @overload
+ def get_all(self, name: str, failobj: _T) -> Union[List[Any], _T]:
"""
Return all values associated with a possibly multi-valued key.
"""
@@ -30,18 +44,19 @@ class PackageMetadata(Protocol):
"""
-class SimplePath(Protocol):
+class SimplePath(Protocol[_T]):
"""
A minimal subset of pathlib.Path required by PathDistribution.
"""
- def joinpath(self) -> 'SimplePath':
+ def joinpath(self, other: Union[str, _T]) -> _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:
diff --git a/importlib_metadata/_py39compat.py b/importlib_metadata/_py39compat.py
new file mode 100644
index 0000000..cde4558
--- /dev/null
+++ b/importlib_metadata/_py39compat.py
@@ -0,0 +1,35 @@
+"""
+Compatibility layer with Python 3.8/3.9
+"""
+from typing import TYPE_CHECKING, Any, Optional
+
+if TYPE_CHECKING: # pragma: no cover
+ # Prevent circular imports on runtime.
+ 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 # -> delay to prevent circular imports.
+
+ return Prepared.normalize(getattr(dist, "name", None) or dist.metadata['Name'])
+
+
+def ep_matches(ep: EntryPoint, **params) -> bool:
+ """
+ Workaround for ``EntryPoint`` objects without the ``matches`` method.
+ """
+ try:
+ return ep.matches(**params)
+ except AttributeError:
+ 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)
diff --git a/mypy.ini b/mypy.ini
index 976ba02..b6f9727 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/pyproject.toml b/pyproject.toml
index 60de242..27b0f18 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"
diff --git a/pytest.ini b/pytest.ini
index 80e98cc..99a2519 100644
--- a/pytest.ini
+++ b/pytest.ini
@@ -1,8 +1,12 @@
[pytest]
norecursedirs=dist build .tox .eggs
addopts=--doctest-modules
-doctest_optionflags=ALLOW_UNICODE ELLIPSIS
filterwarnings=
+ ## upstream
+
+ # Ensure ResourceWarnings are emitted
+ default::ResourceWarning
+
# Suppress deprecation warning in flake8
ignore:SelectableGroups dict interface is deprecated::flake8
@@ -15,3 +19,14 @@ filterwarnings=
ignore:<class 'pytest_flake8.Flake8Item'> 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
+
+ # realpython/pytest-mypy#152
+ ignore:'encoding' argument not specified::pytest_mypy
+
+ # python/cpython#100750
+ ignore:'encoding' argument not specified::platform
+
+ ## end upstream
diff --git a/setup.cfg b/setup.cfg
index daf1b07..ad6a7f3 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -32,7 +32,9 @@ exclude =
testing =
pytest >= 6
pytest-checkdocs >= 2.4
- pytest-flake8
+ pytest-flake8; \
+ python_version < "3.12"
+ flake8 < 5
pytest-black >= 0.3.7; \
python_implementation != "PyPy"
pytest-cov
@@ -46,9 +48,13 @@ testing =
flufl.flake8
pytest-perf >= 0.9.2
docs =
- sphinx
+ sphinx >= 3.5
jaraco.packaging >= 9
rst.linker >= 1.9
+ furo
+ sphinx-lint
+
+ jaraco.tidelift >= 1.4
perf =
ipython
diff --git a/tests/_context.py b/tests/_context.py
new file mode 100644
index 0000000..8a53eb5
--- /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/_path.py b/tests/_path.py
new file mode 100644
index 0000000..71a7043
--- /dev/null
+++ b/tests/_path.py
@@ -0,0 +1,109 @@
+# 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')
+
+
+@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
diff --git a/tests/fixtures.py b/tests/fixtures.py
index 6d9a9d2..6d26bb9 100644
--- a/tests/fixtures.py
+++ b/tests/fixtures.py
@@ -9,7 +9,10 @@ import functools
import contextlib
from .py39compat import FS_NONASCII
-from typing import Dict, Union
+
+from . import _path
+from ._path import FilesSpec
+
try:
from importlib import resources # type: ignore
@@ -82,13 +85,8 @@ class OnSysPath(Fixtures):
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]]
-FilesDef = Dict[str, Union[Dict[str, Union[Dict[str, str], str]], str]]
-
-
class DistInfoPkg(OnSysPath, SiteDir):
- files: FilesDef = {
+ files: FilesSpec = {
"distinfo_pkg-1.0.0.dist-info": {
"METADATA": """
Name: distinfo-pkg
@@ -130,7 +128,7 @@ class DistInfoPkg(OnSysPath, SiteDir):
class DistInfoPkgWithDot(OnSysPath, SiteDir):
- files: FilesDef = {
+ files: FilesSpec = {
"pkg_dot-1.0.0.dist-info": {
"METADATA": """
Name: pkg.dot
@@ -145,7 +143,7 @@ class DistInfoPkgWithDot(OnSysPath, SiteDir):
class DistInfoPkgWithDotLegacy(OnSysPath, SiteDir):
- files: FilesDef = {
+ files: FilesSpec = {
"pkg.dot-1.0.0.dist-info": {
"METADATA": """
Name: pkg.dot
@@ -172,7 +170,7 @@ class DistInfoPkgOffPath(SiteDir):
class EggInfoPkg(OnSysPath, SiteDir):
- files: FilesDef = {
+ files: FilesSpec = {
"egginfo_pkg.egg-info": {
"PKG-INFO": """
Name: egginfo-pkg
@@ -211,8 +209,99 @@ class EggInfoPkg(OnSysPath, SiteDir):
build_files(EggInfoPkg.files, prefix=self.site_dir)
+class EggInfoPkgPipInstalledNoToplevel(OnSysPath, SiteDir):
+ 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_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: 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
+ # (setup.py) that are not present after installation.
+ "SOURCES.txt": """
+ setup.py
+ 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
+ # 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 EggInfoPkgSourcesFallback(OnSysPath, SiteDir):
+ files: FilesSpec = {
+ "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": """
+ sources_fallback.py
+ setup.py
+ sources_fallback_pkg.egg-info/PKG-INFO
+ sources_fallback_pkg.egg-info/SOURCES.txt
+ """,
+ # missing installed-files.txt (i.e. not installed by pip) and
+ # missing top_level.txt (to trigger fallback to SOURCES.txt)
+ },
+ "sources_fallback.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 = {
+ files: FilesSpec = {
"egginfo_file.egg-info": """
Metadata-Version: 1.0
Name: egginfo_file
@@ -232,38 +321,22 @@ class EggInfoFile(OnSysPath, SiteDir):
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:
+build_files = _path.build
- {"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))
+
+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:
diff --git a/tests/test_api.py b/tests/test_api.py
index 3f75ceb..a85c62a 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -27,12 +27,14 @@ def suppress_known_deprecation():
class APITests(
fixtures.EggInfoPkg,
+ fixtures.EggInfoPkgPipInstalledNoToplevel,
+ fixtures.EggInfoPkgPipInstalledNoModules,
+ fixtures.EggInfoPkgSourcesFallback,
fixtures.DistInfoPkg,
fixtures.DistInfoPkgWithDot,
fixtures.EggInfoFile,
unittest.TestCase,
):
-
version_pattern = r'\d+\.\d+(\.\d)?'
def test_retrieves_version_of_self(self):
@@ -63,15 +65,28 @@ class APITests(
distribution(prefix)
def test_for_top_level(self):
- self.assertEqual(
- distribution('egginfo-pkg').read_text('top_level.txt').strip(), 'mod'
- )
+ tests = [
+ ('egginfo-pkg', 'mod'),
+ ('egg_with_no_modules-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'),
+ ('egg_with_no_modules-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()
@@ -124,62 +139,6 @@ class APITests(
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 suppress_known_deprecation() 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_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 suppress_known_deprecation() 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 suppress_known_deprecation():
- 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 suppress_known_deprecation():
- entry_points().get('missing', 'default') == 'default'
- entry_points().get('entries', 'default') == entry_points()['entries']
- entry_points().get('missing', ()) == ()
-
def test_entry_points_allows_no_attributes(self):
ep = entry_points().select(group='entries', name='main')
with self.assertRaises(AttributeError):
@@ -197,6 +156,28 @@ class APITests(
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
+
+ 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
@@ -219,6 +200,9 @@ class APITests(
def test_files_egg_info(self):
self._test_files(files('egginfo-pkg'))
+ self._test_files(files('egg_with_module-pkg'))
+ self._test_files(files('egg_with_no_modules-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 921f5d9..a765017 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -1,17 +1,18 @@
import re
-import json
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,
- MetadataPathFinder,
PackageNotFoundError,
_unique,
distributions,
@@ -22,6 +23,13 @@ from importlib_metadata import (
)
+@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)?'
@@ -37,7 +45,7 @@ class BasicTests(fixtures.DistInfoPkg, unittest.TestCase):
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.
"""
@@ -46,9 +54,12 @@ class BasicTests(fixtures.DistInfoPkg, unittest.TestCase):
assert "metadata" in str(ctx.exception)
- def test_new_style_classes(self):
- self.assertIsInstance(Distribution, type)
- self.assertIsInstance(MetadataPathFinder, type)
+ # expected to fail until ABC is enforced
+ @suppress(AssertionError)
+ @suppress_known_deprecation()
+ def test_abc_enforced(self):
+ with self.assertRaises(TypeError):
+ type('DistributionSubclass', (Distribution,), {})()
@fixtures.parameterize(
dict(name=None),
@@ -173,11 +184,21 @@ class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
assert meta['Description'] == 'pôrˈtend'
-class DiscoveryTests(fixtures.EggInfoPkg, fixtures.DistInfoPkg, unittest.TestCase):
+class DiscoveryTests(
+ fixtures.EggInfoPkg,
+ fixtures.EggInfoPkgPipInstalledNoToplevel,
+ fixtures.EggInfoPkgPipInstalledNoModules,
+ fixtures.EggInfoPkgSourcesFallback,
+ 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'] == 'egg_with_module-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)
def test_invalid_usage(self):
@@ -259,14 +280,6 @@ class TestEntryPoints(unittest.TestCase):
"""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'
@@ -333,3 +346,79 @@ class PackagesDistributionsTest(
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()
+ metadata = dict(
+ METADATA="""
+ Name: all_distributions
+ Version: 1.0.0
+ """,
+ )
+ files = {
+ '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}': '',
+ },
+ }
+ )
+ metadata.update(RECORD=fixtures.build_record(files))
+ fixtures.build_files(files, prefix=self.site_dir)
+
+ distributions = packages_distributions()
+
+ for i in range(len(suffixes)):
+ assert distributions[f'importable-name {i}'] == ['all_distributions']
+ assert distributions[f'in_namespace_{i}'] == ['all_distributions']
+ assert distributions[f'in_package_{i}'] == ['all_distributions']
+
+ assert not any(name.endswith('.dist-info') for name in distributions)
+
+
+class PackagesDistributionsEggTest(
+ fixtures.EggInfoPkg,
+ fixtures.EggInfoPkgPipInstalledNoToplevel,
+ 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',
+ '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'}
+
+ # 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'}
+
+ # 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('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)
+ assert import_names_from_package('sources_fallback-pkg') == {'sources_fallback'}
diff --git a/tests/test_py39compat.py b/tests/test_py39compat.py
new file mode 100644
index 0000000..7e6235e
--- /dev/null
+++ b/tests/test_py39compat.py
@@ -0,0 +1,74 @@
+import sys
+import pathlib
+import unittest
+
+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"))
diff --git a/tox.ini b/tox.ini
index a0ce7c6..4d7d90d 100644
--- a/tox.ini
+++ b/tox.ini
@@ -8,12 +8,15 @@ toxworkdir={env:TOX_WORK_DIR:.tox}
[testenv]
deps =
+setenv =
+ PYTHONWARNDEFAULTENCODING = 1
commands =
pytest {posargs}
passenv =
HOME
usedevelop = True
-extras = testing
+extras =
+ testing
[testenv:docs]
@@ -23,6 +26,7 @@ extras =
changedir = docs
commands =
python -m sphinx -W --keep-going . {toxinidir}/build/html
+ python -m sphinxlint
[testenv:diffcov]
deps =
Debdiff
[The following lists of changes regard files as different if they have different names, permissions or owners.]
Files in second set of .debs but not in first
-rw-r--r-- root/root /usr/lib/python3/dist-packages/importlib_metadata-6.6.0.dist-info/METADATA -rw-r--r-- root/root /usr/lib/python3/dist-packages/importlib_metadata-6.6.0.dist-info/RECORD -rw-r--r-- root/root /usr/lib/python3/dist-packages/importlib_metadata-6.6.0.dist-info/WHEEL -rw-r--r-- root/root /usr/lib/python3/dist-packages/importlib_metadata-6.6.0.dist-info/top_level.txt -rw-r--r-- root/root /usr/lib/python3/dist-packages/importlib_metadata/_py39compat.py
Files in first set of .debs but not in second
-rw-r--r-- root/root /usr/lib/python3/dist-packages/importlib_metadata-4.12.0.dist-info/METADATA -rw-r--r-- root/root /usr/lib/python3/dist-packages/importlib_metadata-4.12.0.dist-info/RECORD -rw-r--r-- root/root /usr/lib/python3/dist-packages/importlib_metadata-4.12.0.dist-info/WHEEL -rw-r--r-- root/root /usr/lib/python3/dist-packages/importlib_metadata-4.12.0.dist-info/top_level.txt
No differences were encountered in the control files