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

More details

Full run details