Bump importlib-metadata from 6.8.0 to 7.1.0 (#2286)

* Bump importlib-metadata from 6.8.0 to 7.1.0

Bumps [importlib-metadata](https://github.com/python/importlib_metadata) from 6.8.0 to 7.1.0.
- [Release notes](https://github.com/python/importlib_metadata/releases)
- [Changelog](https://github.com/python/importlib_metadata/blob/main/NEWS.rst)
- [Commits](https://github.com/python/importlib_metadata/compare/v6.8.0...v7.1.0)

---
updated-dependencies:
- dependency-name: importlib-metadata
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update importlib-metadata==7.1.0

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: JonnyWong16 <9099342+JonnyWong16@users.noreply.github.com>

[skip ci]
This commit is contained in:
dependabot[bot] 2024-03-24 15:29:52 -07:00 committed by GitHub
parent b01b21ae05
commit e248c13c15
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 379 additions and 156 deletions

View file

@ -1,10 +1,14 @@
from __future__ import annotations
import os import os
import re import re
import abc import abc
import csv
import sys import sys
import json
import zipp import zipp
import email import email
import types
import inspect
import pathlib import pathlib
import operator import operator
import textwrap import textwrap
@ -14,12 +18,12 @@ import itertools
import posixpath import posixpath
import collections import collections
from . import _adapters, _meta, _py39compat from . import _adapters, _meta
from .compat import py39
from ._collections import FreezableDefaultDict, Pair from ._collections import FreezableDefaultDict, Pair
from ._compat import ( from ._compat import (
NullFinder, NullFinder,
install, install,
pypy_partial,
) )
from ._functools import method_cache, pass_none from ._functools import method_cache, pass_none
from ._itertools import always_iterable, unique_everseen from ._itertools import always_iterable, unique_everseen
@ -29,8 +33,7 @@ from contextlib import suppress
from importlib import import_module from importlib import import_module
from importlib.abc import MetaPathFinder from importlib.abc import MetaPathFinder
from itertools import starmap from itertools import starmap
from typing import List, Mapping, Optional from typing import Any, Iterable, List, Mapping, Match, Optional, Set, cast
__all__ = [ __all__ = [
'Distribution', 'Distribution',
@ -51,11 +54,11 @@ __all__ = [
class PackageNotFoundError(ModuleNotFoundError): class PackageNotFoundError(ModuleNotFoundError):
"""The package was not found.""" """The package was not found."""
def __str__(self): def __str__(self) -> str:
return f"No package metadata was found for {self.name}" return f"No package metadata was found for {self.name}"
@property @property
def name(self): def name(self) -> str: # type: ignore[override]
(name,) = self.args (name,) = self.args
return name return name
@ -121,38 +124,11 @@ class Sectioned:
yield Pair(name, value) yield Pair(name, value)
@staticmethod @staticmethod
def valid(line): def valid(line: str):
return line and not line.startswith('#') return line and not line.startswith('#')
class DeprecatedTuple: class EntryPoint:
"""
Provide subscript item access for backward compatibility.
>>> recwarn = getfixture('recwarn')
>>> ep = EntryPoint(name='name', value='value', group='group')
>>> ep[:]
('name', 'value', 'group')
>>> ep[0]
'name'
>>> len(recwarn)
1
"""
# Do not remove prior to 2023-05-01 or Python 3.13
_warn = functools.partial(
warnings.warn,
"EntryPoint tuple interface is deprecated. Access members by name.",
DeprecationWarning,
stacklevel=pypy_partial(2),
)
def __getitem__(self, item):
self._warn()
return self._key()[item]
class EntryPoint(DeprecatedTuple):
"""An entry point as defined by Python packaging conventions. """An entry point as defined by Python packaging conventions.
See `the packaging docs on entry points See `the packaging docs on entry points
@ -194,34 +170,37 @@ class EntryPoint(DeprecatedTuple):
value: str value: str
group: str group: str
dist: Optional['Distribution'] = None 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) vars(self).update(name=name, value=value, group=group)
def load(self): def load(self) -> Any:
"""Load the entry point from its definition. If only a module """Load the entry point from its definition. If only a module
is indicated by the value, return that module. Otherwise, is indicated by the value, return that module. Otherwise,
return the named object. return the named object.
""" """
match = self.pattern.match(self.value) match = cast(Match, self.pattern.match(self.value))
module = import_module(match.group('module')) module = import_module(match.group('module'))
attrs = filter(None, (match.group('attr') or '').split('.')) attrs = filter(None, (match.group('attr') or '').split('.'))
return functools.reduce(getattr, attrs, module) return functools.reduce(getattr, attrs, module)
@property @property
def module(self): def module(self) -> str:
match = self.pattern.match(self.value) match = self.pattern.match(self.value)
assert match is not None
return match.group('module') return match.group('module')
@property @property
def attr(self): def attr(self) -> str:
match = self.pattern.match(self.value) match = self.pattern.match(self.value)
assert match is not None
return match.group('attr') return match.group('attr')
@property @property
def extras(self): def extras(self) -> List[str]:
match = self.pattern.match(self.value) match = self.pattern.match(self.value)
assert match is not None
return re.findall(r'\w+', match.group('extras') or '') return re.findall(r'\w+', match.group('extras') or '')
def _for(self, dist): def _for(self, dist):
@ -269,7 +248,7 @@ class EntryPoint(DeprecatedTuple):
f'group={self.group!r})' f'group={self.group!r})'
) )
def __hash__(self): def __hash__(self) -> int:
return hash(self._key()) return hash(self._key())
@ -280,7 +259,7 @@ class EntryPoints(tuple):
__slots__ = () __slots__ = ()
def __getitem__(self, name): # -> EntryPoint: def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override]
""" """
Get the EntryPoint in self matching name. Get the EntryPoint in self matching name.
""" """
@ -289,22 +268,29 @@ class EntryPoints(tuple):
except StopIteration: except StopIteration:
raise KeyError(name) raise KeyError(name)
def select(self, **params): def __repr__(self):
"""
Repr with classname and tuple constructor to
signal that we deviate from regular tuple behavior.
"""
return '%s(%r)' % (self.__class__.__name__, tuple(self))
def select(self, **params) -> EntryPoints:
""" """
Select entry points from self that match the Select entry points from self that match the
given parameters (typically group and/or name). given parameters (typically group and/or name).
""" """
return EntryPoints(ep for ep in self if _py39compat.ep_matches(ep, **params)) return EntryPoints(ep for ep in self if py39.ep_matches(ep, **params))
@property @property
def names(self): def names(self) -> Set[str]:
""" """
Return the set of all names of all entry points. Return the set of all names of all entry points.
""" """
return {ep.name for ep in self} return {ep.name for ep in self}
@property @property
def groups(self): def groups(self) -> Set[str]:
""" """
Return the set of all groups of all entry points. Return the set of all groups of all entry points.
""" """
@ -325,47 +311,92 @@ class EntryPoints(tuple):
class PackagePath(pathlib.PurePosixPath): class PackagePath(pathlib.PurePosixPath):
"""A reference to a path in a package""" """A reference to a path in a package"""
def read_text(self, encoding='utf-8'): hash: Optional[FileHash]
with self.locate().open(encoding=encoding) as stream: size: int
return stream.read() dist: Distribution
def read_binary(self): def read_text(self, encoding: str = 'utf-8') -> str: # type: ignore[override]
with self.locate().open('rb') as stream: return self.locate().read_text(encoding=encoding)
return stream.read()
def locate(self): def read_binary(self) -> bytes:
return self.locate().read_bytes()
def locate(self) -> SimplePath:
"""Return a path-like object for this path""" """Return a path-like object for this path"""
return self.dist.locate_file(self) return self.dist.locate_file(self)
class FileHash: class FileHash:
def __init__(self, spec): def __init__(self, spec: str) -> None:
self.mode, _, self.value = spec.partition('=') self.mode, _, self.value = spec.partition('=')
def __repr__(self): def __repr__(self) -> str:
return f'<FileHash mode: {self.mode} value: {self.value}>' return f'<FileHash mode: {self.mode} value: {self.value}>'
class Distribution(metaclass=abc.ABCMeta): class DeprecatedNonAbstract:
"""A Python distribution package.""" # Required until Python 3.14
def __new__(cls, *args, **kwargs):
all_names = {
name for subclass in inspect.getmro(cls) for name in vars(subclass)
}
abstract = {
name
for name in all_names
if getattr(getattr(cls, name), '__isabstractmethod__', False)
}
if abstract:
warnings.warn(
f"Unimplemented abstract methods {abstract}",
DeprecationWarning,
stacklevel=2,
)
return super().__new__(cls)
class Distribution(DeprecatedNonAbstract):
"""
An abstract Python distribution package.
Custom providers may derive from this class and define
the abstract methods to provide a concrete implementation
for their environment. Some providers may opt to override
the default implementation of some properties to bypass
the file-reading mechanism.
"""
@abc.abstractmethod @abc.abstractmethod
def read_text(self, filename): def read_text(self, filename) -> Optional[str]:
"""Attempt to load metadata file given by the name. """Attempt to load metadata file given by the name.
Python distribution metadata is organized by blobs of text
typically represented as "files" in the metadata directory
(e.g. package-1.0.dist-info). These files include things
like:
- METADATA: The distribution metadata including fields
like Name and Version and Description.
- entry_points.txt: A series of entry points as defined in
`the entry points spec <https://packaging.python.org/en/latest/specifications/entry-points/#file-format>`_.
- RECORD: A record of files according to
`this recording spec <https://packaging.python.org/en/latest/specifications/recording-installed-packages/#the-record-file>`_.
A package may provide any set of files, including those
not listed here or none at all.
:param filename: The name of the file in the distribution info. :param filename: The name of the file in the distribution info.
:return: The text if found, otherwise None. :return: The text if found, otherwise None.
""" """
@abc.abstractmethod @abc.abstractmethod
def locate_file(self, path): def locate_file(self, path: str | os.PathLike[str]) -> SimplePath:
""" """
Given a path to a file in this distribution, return a path Given a path to a file in this distribution, return a SimplePath
to it. to it.
""" """
@classmethod @classmethod
def from_name(cls, name: str): def from_name(cls, name: str) -> Distribution:
"""Return the Distribution for the given package name. """Return the Distribution for the given package name.
:param name: The name of the distribution package to search for. :param name: The name of the distribution package to search for.
@ -378,21 +409,23 @@ class Distribution(metaclass=abc.ABCMeta):
if not name: if not name:
raise ValueError("A distribution name is required.") raise ValueError("A distribution name is required.")
try: try:
return next(cls.discover(name=name)) return next(iter(cls.discover(name=name)))
except StopIteration: except StopIteration:
raise PackageNotFoundError(name) raise PackageNotFoundError(name)
@classmethod @classmethod
def discover(cls, **kwargs): def discover(
cls, *, context: Optional[DistributionFinder.Context] = None, **kwargs
) -> Iterable[Distribution]:
"""Return an iterable of Distribution objects for all packages. """Return an iterable of Distribution objects for all packages.
Pass a ``context`` or pass keyword arguments for constructing Pass a ``context`` or pass keyword arguments for constructing
a context. a context.
:context: A ``DistributionFinder.Context`` object. :context: A ``DistributionFinder.Context`` object.
:return: Iterable of Distribution objects for all packages. :return: Iterable of Distribution objects for packages matching
the context.
""" """
context = kwargs.pop('context', None)
if context and kwargs: if context and kwargs:
raise ValueError("cannot accept context and kwargs") raise ValueError("cannot accept context and kwargs")
context = context or DistributionFinder.Context(**kwargs) context = context or DistributionFinder.Context(**kwargs)
@ -401,8 +434,8 @@ class Distribution(metaclass=abc.ABCMeta):
) )
@staticmethod @staticmethod
def at(path): def at(path: str | os.PathLike[str]) -> Distribution:
"""Return a Distribution for the indicated metadata path """Return a Distribution for the indicated metadata path.
:param path: a string or path-like object :param path: a string or path-like object
:return: a concrete Distribution instance for the path :return: a concrete Distribution instance for the path
@ -411,7 +444,7 @@ class Distribution(metaclass=abc.ABCMeta):
@staticmethod @staticmethod
def _discover_resolvers(): def _discover_resolvers():
"""Search the meta_path for resolvers.""" """Search the meta_path for resolvers (MetadataPathFinders)."""
declared = ( declared = (
getattr(finder, 'find_distributions', None) for finder in sys.meta_path getattr(finder, 'find_distributions', None) for finder in sys.meta_path
) )
@ -422,9 +455,13 @@ class Distribution(metaclass=abc.ABCMeta):
"""Return the parsed metadata for this Distribution. """Return the parsed metadata for this Distribution.
The returned object will have keys that name the various bits of The returned object will have keys that name the various bits of
metadata. See PEP 566 for details. metadata per the
`Core metadata specifications <https://packaging.python.org/en/latest/specifications/core-metadata/#core-metadata>`_.
Custom providers may provide the METADATA file or override this
property.
""" """
text = ( opt_text = (
self.read_text('METADATA') self.read_text('METADATA')
or self.read_text('PKG-INFO') or self.read_text('PKG-INFO')
# This last clause is here to support old egg-info files. Its # This last clause is here to support old egg-info files. Its
@ -432,10 +469,11 @@ class Distribution(metaclass=abc.ABCMeta):
# (which points to the egg-info file) attribute unchanged. # (which points to the egg-info file) attribute unchanged.
or self.read_text('') or self.read_text('')
) )
text = cast(str, opt_text)
return _adapters.Message(email.message_from_string(text)) return _adapters.Message(email.message_from_string(text))
@property @property
def name(self): def name(self) -> str:
"""Return the 'Name' metadata for the distribution package.""" """Return the 'Name' metadata for the distribution package."""
return self.metadata['Name'] return self.metadata['Name']
@ -445,24 +483,34 @@ class Distribution(metaclass=abc.ABCMeta):
return Prepared.normalize(self.name) return Prepared.normalize(self.name)
@property @property
def version(self): def version(self) -> str:
"""Return the 'Version' metadata for the distribution package.""" """Return the 'Version' metadata for the distribution package."""
return self.metadata['Version'] return self.metadata['Version']
@property @property
def entry_points(self): def entry_points(self) -> EntryPoints:
"""
Return EntryPoints for this distribution.
Custom providers may provide the ``entry_points.txt`` file
or override this property.
"""
return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self) return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self)
@property @property
def files(self): def files(self) -> Optional[List[PackagePath]]:
"""Files in this distribution. """Files in this distribution.
:return: List of PackagePath for this distribution or None :return: List of PackagePath for this distribution or None
Result is `None` if the metadata file that enumerates files Result is `None` if the metadata file that enumerates files
(i.e. RECORD for dist-info or SOURCES.txt for egg-info) is (i.e. RECORD for dist-info, or installed-files.txt or
missing. SOURCES.txt for egg-info) is missing.
Result may be empty if the metadata exists but is empty. Result may be empty if the metadata exists but is empty.
Custom providers are recommended to provide a "RECORD" file (in
``read_text``) or override this property to allow for callers to be
able to resolve filenames provided by the package.
""" """
def make_file(name, hash=None, size_str=None): def make_file(name, hash=None, size_str=None):
@ -474,27 +522,76 @@ class Distribution(metaclass=abc.ABCMeta):
@pass_none @pass_none
def make_files(lines): def make_files(lines):
return list(starmap(make_file, csv.reader(lines))) # Delay csv import, since Distribution.files is not as widely used
# as other parts of importlib.metadata
import csv
return make_files(self._read_files_distinfo() or self._read_files_egginfo()) return starmap(make_file, csv.reader(lines))
@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): def _read_files_distinfo(self):
""" """
Read the lines of RECORD Read the lines of RECORD.
""" """
text = self.read_text('RECORD') text = self.read_text('RECORD')
return text and text.splitlines() 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 Read installed-files.txt and return lines in a similar
in quotes. 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') text = self.read_text('SOURCES.txt')
return text and map('"{}"'.format, text.splitlines()) return text and map('"{}"'.format, text.splitlines())
@property @property
def requires(self): def requires(self) -> Optional[List[str]]:
"""Generated requirements specified for this Distribution""" """Generated requirements specified for this Distribution"""
reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs() reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs()
return reqs and list(reqs) return reqs and list(reqs)
@ -545,10 +642,23 @@ class Distribution(metaclass=abc.ABCMeta):
space = url_req_space(section.value) space = url_req_space(section.value)
yield section.value + space + quoted_marker(section.name) yield section.value + space + quoted_marker(section.name)
@property
def origin(self):
return self._load_json('direct_url.json')
def _load_json(self, filename):
return pass_none(json.loads)(
self.read_text(filename),
object_hook=lambda data: types.SimpleNamespace(**data),
)
class DistributionFinder(MetaPathFinder): class DistributionFinder(MetaPathFinder):
""" """
A MetaPathFinder capable of discovering installed distributions. A MetaPathFinder capable of discovering installed distributions.
Custom providers should implement this interface in order to
supply metadata.
""" """
class Context: class Context:
@ -561,6 +671,17 @@ class DistributionFinder(MetaPathFinder):
Each DistributionFinder may expect any parameters Each DistributionFinder may expect any parameters
and should attempt to honor the canonical and should attempt to honor the canonical
parameters defined below when appropriate. parameters defined below when appropriate.
This mechanism gives a custom provider a means to
solicit additional details from the caller beyond
"name" and "path" when searching distributions.
For example, imagine a provider that exposes suites
of packages in either a "public" or "private" ``realm``.
A caller may wish to query only for distributions in
a particular realm and could call
``distributions(realm="private")`` to signal to the
custom provider to only include distributions from that
realm.
""" """
name = None name = None
@ -573,7 +694,7 @@ class DistributionFinder(MetaPathFinder):
vars(self).update(kwargs) vars(self).update(kwargs)
@property @property
def path(self): def path(self) -> List[str]:
""" """
The sequence of directory path that a distribution finder The sequence of directory path that a distribution finder
should search. should search.
@ -584,7 +705,7 @@ class DistributionFinder(MetaPathFinder):
return vars(self).get('path', sys.path) return vars(self).get('path', sys.path)
@abc.abstractmethod @abc.abstractmethod
def find_distributions(self, context=Context()): def find_distributions(self, context=Context()) -> Iterable[Distribution]:
""" """
Find distributions. Find distributions.
@ -596,11 +717,18 @@ class DistributionFinder(MetaPathFinder):
class FastPath: class FastPath:
""" """
Micro-optimized class for searching a path for Micro-optimized class for searching a root for children.
children.
Root is a path on the file system that may contain metadata
directories either as natural directories or within a zip file.
>>> FastPath('').children() >>> FastPath('').children()
['...'] ['...']
FastPath objects are cached and recycled for any given root.
>>> FastPath('foobar') is FastPath('foobar')
True
""" """
@functools.lru_cache() # type: ignore @functools.lru_cache() # type: ignore
@ -642,7 +770,19 @@ class FastPath:
class Lookup: class Lookup:
"""
A micro-optimized class for searching a (fast) path for metadata.
"""
def __init__(self, path: FastPath): def __init__(self, path: FastPath):
"""
Calculate all of the children representing metadata.
From the children in the path, calculate early all of the
children that appear to represent metadata (infos) or legacy
metadata (eggs).
"""
base = os.path.basename(path.root).lower() base = os.path.basename(path.root).lower()
base_is_egg = base.endswith(".egg") base_is_egg = base.endswith(".egg")
self.infos = FreezableDefaultDict(list) self.infos = FreezableDefaultDict(list)
@ -663,7 +803,10 @@ class Lookup:
self.infos.freeze() self.infos.freeze()
self.eggs.freeze() self.eggs.freeze()
def search(self, prepared): def search(self, prepared: Prepared):
"""
Yield all infos and eggs matching the Prepared query.
"""
infos = ( infos = (
self.infos[prepared.normalized] self.infos[prepared.normalized]
if prepared if prepared
@ -679,13 +822,28 @@ class Lookup:
class Prepared: class Prepared:
""" """
A prepared search for metadata on a possibly-named package. A prepared search query for metadata on a possibly-named package.
Pre-calculates the normalization to prevent repeated operations.
>>> none = Prepared(None)
>>> none.normalized
>>> none.legacy_normalized
>>> bool(none)
False
>>> sample = Prepared('Sample__Pkg-name.foo')
>>> sample.normalized
'sample_pkg_name_foo'
>>> sample.legacy_normalized
'sample__pkg_name.foo'
>>> bool(sample)
True
""" """
normalized = None normalized = None
legacy_normalized = None legacy_normalized = None
def __init__(self, name): def __init__(self, name: Optional[str]):
self.name = name self.name = name
if name is None: if name is None:
return return
@ -719,7 +877,10 @@ class MetadataPathFinder(NullFinder, DistributionFinder):
of Python that do not have a PathFinder find_distributions(). of Python that do not have a PathFinder find_distributions().
""" """
def find_distributions(self, context=DistributionFinder.Context()): @classmethod
def find_distributions(
cls, context=DistributionFinder.Context()
) -> Iterable[PathDistribution]:
""" """
Find distributions. Find distributions.
@ -728,7 +889,7 @@ class MetadataPathFinder(NullFinder, DistributionFinder):
(or all names if ``None`` indicated) along the paths in the list (or all names if ``None`` indicated) along the paths in the list
of directories ``context.path``. of directories ``context.path``.
""" """
found = self._search_paths(context.name, context.path) found = cls._search_paths(context.name, context.path)
return map(PathDistribution, found) return map(PathDistribution, found)
@classmethod @classmethod
@ -739,19 +900,20 @@ class MetadataPathFinder(NullFinder, DistributionFinder):
path.search(prepared) for path in map(FastPath, paths) path.search(prepared) for path in map(FastPath, paths)
) )
def invalidate_caches(cls): @classmethod
def invalidate_caches(cls) -> None:
FastPath.__new__.cache_clear() FastPath.__new__.cache_clear()
class PathDistribution(Distribution): class PathDistribution(Distribution):
def __init__(self, path: SimplePath): def __init__(self, path: SimplePath) -> None:
"""Construct a distribution. """Construct a distribution.
:param path: SimplePath indicating the metadata directory. :param path: SimplePath indicating the metadata directory.
""" """
self._path = path self._path = path
def read_text(self, filename): def read_text(self, filename: str | os.PathLike[str]) -> Optional[str]:
with suppress( with suppress(
FileNotFoundError, FileNotFoundError,
IsADirectoryError, IsADirectoryError,
@ -761,9 +923,11 @@ class PathDistribution(Distribution):
): ):
return self._path.joinpath(filename).read_text(encoding='utf-8') return self._path.joinpath(filename).read_text(encoding='utf-8')
return None
read_text.__doc__ = Distribution.read_text.__doc__ read_text.__doc__ = Distribution.read_text.__doc__
def locate_file(self, path): def locate_file(self, path: str | os.PathLike[str]) -> SimplePath:
return self._path.parent / path return self._path.parent / path
@property @property
@ -796,7 +960,7 @@ class PathDistribution(Distribution):
return name return name
def distribution(distribution_name): def distribution(distribution_name: str) -> Distribution:
"""Get the ``Distribution`` instance for the named package. """Get the ``Distribution`` instance for the named package.
:param distribution_name: The name of the distribution package as a string. :param distribution_name: The name of the distribution package as a string.
@ -805,7 +969,7 @@ def distribution(distribution_name):
return Distribution.from_name(distribution_name) return Distribution.from_name(distribution_name)
def distributions(**kwargs): def distributions(**kwargs) -> Iterable[Distribution]:
"""Get all ``Distribution`` instances in the current environment. """Get all ``Distribution`` instances in the current environment.
:return: An iterable of ``Distribution`` instances. :return: An iterable of ``Distribution`` instances.
@ -813,7 +977,7 @@ def distributions(**kwargs):
return Distribution.discover(**kwargs) return Distribution.discover(**kwargs)
def metadata(distribution_name) -> _meta.PackageMetadata: def metadata(distribution_name: str) -> _meta.PackageMetadata:
"""Get the metadata for the named package. """Get the metadata for the named package.
:param distribution_name: The name of the distribution package to query. :param distribution_name: The name of the distribution package to query.
@ -822,7 +986,7 @@ def metadata(distribution_name) -> _meta.PackageMetadata:
return Distribution.from_name(distribution_name).metadata return Distribution.from_name(distribution_name).metadata
def version(distribution_name): def version(distribution_name: str) -> str:
"""Get the version string for the named package. """Get the version string for the named package.
:param distribution_name: The name of the distribution package to query. :param distribution_name: The name of the distribution package to query.
@ -834,7 +998,7 @@ def version(distribution_name):
_unique = functools.partial( _unique = functools.partial(
unique_everseen, unique_everseen,
key=_py39compat.normalized_name, key=py39.normalized_name,
) )
""" """
Wrapper for ``distributions`` to return unique distributions by name. Wrapper for ``distributions`` to return unique distributions by name.
@ -856,7 +1020,7 @@ def entry_points(**params) -> EntryPoints:
return EntryPoints(eps).select(**params) return EntryPoints(eps).select(**params)
def files(distribution_name): def files(distribution_name: str) -> Optional[List[PackagePath]]:
"""Return a list of files for the named package. """Return a list of files for the named package.
:param distribution_name: The name of the distribution package to query. :param distribution_name: The name of the distribution package to query.
@ -865,11 +1029,11 @@ def files(distribution_name):
return distribution(distribution_name).files return distribution(distribution_name).files
def requires(distribution_name): def requires(distribution_name: str) -> Optional[List[str]]:
""" """
Return a list of requirements for the named package. 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. packaging.requirement.Requirement.
""" """
return distribution(distribution_name).requires return distribution(distribution_name).requires
@ -896,9 +1060,43 @@ def _top_level_declared(dist):
return (dist.read_text('top_level.txt') or '').split() return (dist.read_text('top_level.txt') or '').split()
def _topmost(name: PackagePath) -> Optional[str]:
"""
Return the top-most parent as long as there is a parent.
"""
top, *rest = name.parts
return top if rest else None
def _get_toplevel_name(name: PackagePath) -> str:
"""
Infer a possibly importable module name from a name presumed on
sys.path.
>>> _get_toplevel_name(PackagePath('foo.py'))
'foo'
>>> _get_toplevel_name(PackagePath('foo'))
'foo'
>>> _get_toplevel_name(PackagePath('foo.pyc'))
'foo'
>>> _get_toplevel_name(PackagePath('foo/__init__.py'))
'foo'
>>> _get_toplevel_name(PackagePath('foo.pth'))
'foo.pth'
>>> _get_toplevel_name(PackagePath('foo.dist-info'))
'foo.dist-info'
"""
return _topmost(name) or (
# python/typeshed#10328
inspect.getmodulename(name) # type: ignore
or str(name)
)
def _top_level_inferred(dist): def _top_level_inferred(dist):
return { opt_names = set(map(_get_toplevel_name, always_iterable(dist.files)))
f.parts[0] if len(f.parts) > 1 else f.with_suffix('').name
for f in always_iterable(dist.files) def importable_name(name):
if f.suffix == ".py" return '.' not in name
}
return filter(importable_name, opt_names)

View file

@ -54,7 +54,7 @@ class Message(email.message.Message):
def __getitem__(self, item): def __getitem__(self, item):
""" """
Warn users that a ``KeyError`` can be expected when a Warn users that a ``KeyError`` can be expected when a
mising key is supplied. Ref python/importlib_metadata#371. missing key is supplied. Ref python/importlib_metadata#371.
""" """
res = super().__getitem__(item) res = super().__getitem__(item)
if res is None: if res is None:

View file

@ -2,14 +2,7 @@ import sys
import platform import platform
__all__ = ['install', 'NullFinder', 'Protocol'] __all__ = ['install', 'NullFinder']
try:
from typing import Protocol
except ImportError: # pragma: no cover
# Python 3.7 compatibility
from typing_extensions import Protocol # type: ignore
def install(cls): def install(cls):
@ -45,7 +38,7 @@ def disable_stdlib_finder():
class NullFinder: class NullFinder:
""" """
A "Finder" (aka "MetaClassFinder") that never finds any modules, A "Finder" (aka "MetaPathFinder") that never finds any modules,
but may find distributions. but may find distributions.
""" """
@ -53,14 +46,6 @@ class NullFinder:
def find_spec(*args, **kwargs): def find_spec(*args, **kwargs):
return None return None
# In Python 2, the import system requires finders
# to have a find_module() method, but this usage
# is deprecated in Python 3 in favor of find_spec().
# For the purposes of this finder (i.e. being present
# on sys.meta_path but having no other import
# system functionality), the two methods are identical.
find_module = find_spec
def pypy_partial(val): def pypy_partial(val):
""" """

View file

@ -1,24 +1,38 @@
from ._compat import Protocol from __future__ import annotations
from typing import Any, Dict, Iterator, List, TypeVar, Union
import os
from typing import Protocol
from typing import Any, Dict, Iterator, List, Optional, TypeVar, Union, overload
_T = TypeVar("_T") _T = TypeVar("_T")
class PackageMetadata(Protocol): class PackageMetadata(Protocol):
def __len__(self) -> int: def __len__(self) -> int: ... # pragma: no cover
... # pragma: no cover
def __contains__(self, item: str) -> bool: def __contains__(self, item: str) -> bool: ... # pragma: no cover
... # pragma: no cover
def __getitem__(self, key: str) -> str: def __getitem__(self, key: str) -> str: ... # pragma: no cover
... # pragma: no cover
def __iter__(self) -> Iterator[str]: def __iter__(self) -> Iterator[str]: ... # pragma: no cover
... # 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. Return all values associated with a possibly multi-valued key.
""" """
@ -30,20 +44,24 @@ class PackageMetadata(Protocol):
""" """
class SimplePath(Protocol[_T]): class SimplePath(Protocol):
""" """
A minimal subset of pathlib.Path required by PathDistribution. A minimal subset of pathlib.Path required by Distribution.
""" """
def joinpath(self) -> _T: def joinpath(
... # pragma: no cover self, other: Union[str, os.PathLike[str]]
) -> SimplePath: ... # pragma: no cover
def __truediv__(self, other: Union[str, _T]) -> _T: def __truediv__(
... # pragma: no cover self, other: Union[str, os.PathLike[str]]
) -> SimplePath: ... # pragma: no cover
@property @property
def parent(self) -> _T: def parent(self) -> SimplePath: ... # pragma: no cover
... # pragma: no cover
def read_text(self) -> str: def read_text(self, encoding=None) -> str: ... # pragma: no cover
... # pragma: no cover
def read_bytes(self) -> bytes: ... # pragma: no cover
def exists(self) -> bool: ... # pragma: no cover

View file

@ -1,11 +1,12 @@
""" """
Compatibility layer with Python 3.8/3.9 Compatibility layer with Python 3.8/3.9
""" """
from typing import TYPE_CHECKING, Any, Optional from typing import TYPE_CHECKING, Any, Optional
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
# Prevent circular imports on runtime. # Prevent circular imports on runtime.
from . import Distribution, EntryPoint from .. import Distribution, EntryPoint
else: else:
Distribution = EntryPoint = Any Distribution = EntryPoint = Any
@ -17,7 +18,7 @@ def normalized_name(dist: Distribution) -> Optional[str]:
try: try:
return dist._normalized_name return dist._normalized_name
except AttributeError: except AttributeError:
from . import Prepared # -> delay to prevent circular imports. from .. import Prepared # -> delay to prevent circular imports.
return Prepared.normalize(getattr(dist, "name", None) or dist.metadata['Name']) return Prepared.normalize(getattr(dist, "name", None) or dist.metadata['Name'])
@ -29,7 +30,7 @@ def ep_matches(ep: EntryPoint, **params) -> bool:
try: try:
return ep.matches(**params) return ep.matches(**params)
except AttributeError: except AttributeError:
from . import EntryPoint # -> delay to prevent circular imports. from .. import EntryPoint # -> delay to prevent circular imports.
# Reconstruct the EntryPoint object to make sure it is compatible. # Reconstruct the EntryPoint object to make sure it is compatible.
return EntryPoint(ep.name, ep.value, ep.group).matches(**params) return EntryPoint(ep.name, ep.value, ep.group).matches(**params)

View file

@ -0,0 +1,21 @@
import sys
from . import Distribution
def inspect(path):
print("Inspecting", path)
dists = list(Distribution.discover(path=[path]))
if not dists:
return
print("Found", len(dists), "packages:", end=' ')
print(', '.join(dist.name for dist in dists))
def run():
for path in sys.path:
inspect(path)
if __name__ == '__main__':
run()

View file

@ -1,5 +1,5 @@
apscheduler==3.10.1 apscheduler==3.10.1
importlib-metadata==6.8.0 importlib-metadata==7.1.0
importlib-resources==6.4.0 importlib-resources==6.4.0
pyinstaller==6.4.0 pyinstaller==6.4.0
pyopenssl==24.0.0 pyopenssl==24.0.0

View file

@ -18,7 +18,7 @@ gntp==1.0.3
html5lib==1.1 html5lib==1.1
httpagentparser==1.9.5 httpagentparser==1.9.5
idna==3.4 idna==3.4
importlib-metadata==6.8.0 importlib-metadata==7.1.0
importlib-resources==6.4.0 importlib-resources==6.4.0
git+https://github.com/Tautulli/ipwhois.git@master#egg=ipwhois git+https://github.com/Tautulli/ipwhois.git@master#egg=ipwhois
IPy==1.01 IPy==1.01