+"""
+APIs exposing metadata from third-party Python packages.
+
+This codebase is shared between importlib.metadata in the stdlib
+and importlib_metadata in PyPI. See
+https://github.com/python/importlib_metadata/wiki/Development-Methodology
+for more detail.
+"""
+
from __future__ import annotations
-import os
-import re
import abc
-import sys
-import json
+import collections
import email
-import types
-import inspect
-import pathlib
-import zipfile
-import operator
-import textwrap
import functools
import itertools
+import operator
+import os
+import pathlib
import posixpath
-import collections
+import re
+import sys
+import textwrap
+import types
+from collections.abc import Iterable, Mapping
+from contextlib import suppress
+from importlib import import_module
+from importlib.abc import MetaPathFinder
+from itertools import starmap
+from typing import Any
from . import _meta
from ._collections import FreezableDefaultDict, Pair
from ._functools import method_cache, pass_none
from ._itertools import always_iterable, bucket, unique_everseen
from ._meta import PackageMetadata, SimplePath
-
-from contextlib import suppress
-from importlib import import_module
-from importlib.abc import MetaPathFinder
-from itertools import starmap
-from typing import Any, Iterable, List, Mapping, Match, Optional, Set, cast
+from ._typing import md_none
__all__ = [
'Distribution',
return f"No package metadata was found for {self.name}"
@property
- def name(self) -> str: # type: ignore[override]
+ def name(self) -> str: # type: ignore[override] # make readonly
(name,) = self.args
return name
return line and not line.startswith('#')
+class _EntryPointMatch(types.SimpleNamespace):
+ module: str
+ attr: str
+ extras: str
+
+
class EntryPoint:
"""An entry point as defined by Python packaging conventions.
'attr'
>>> ep.extras
['extra1', 'extra2']
+
+ If the value package or module are not valid identifiers, a
+ ValueError is raised on access.
+
+ >>> EntryPoint(name=None, group=None, value='invalid-name').module
+ Traceback (most recent call last):
+ ...
+ ValueError: ('Invalid object reference...invalid-name...
+ >>> EntryPoint(name=None, group=None, value='invalid-name').attr
+ Traceback (most recent call last):
+ ...
+ ValueError: ('Invalid object reference...invalid-name...
+ >>> EntryPoint(name=None, group=None, value='invalid-name').extras
+ Traceback (most recent call last):
+ ...
+ ValueError: ('Invalid object reference...invalid-name...
+
+ The same thing happens on construction.
+
+ >>> EntryPoint(name=None, group=None, value='invalid-name')
+ Traceback (most recent call last):
+ ...
+ ValueError: ('Invalid object reference...invalid-name...
+
"""
pattern = re.compile(
value: str
group: str
- dist: Optional[Distribution] = None
+ dist: Distribution | None = None
def __init__(self, name: str, value: str, group: str) -> None:
vars(self).update(name=name, value=value, group=group)
+ self.module
def load(self) -> Any:
"""Load the entry point from its definition. If only a module
is indicated by the value, return that module. Otherwise,
return the named object.
"""
- match = cast(Match, self.pattern.match(self.value))
- module = import_module(match.group('module'))
- attrs = filter(None, (match.group('attr') or '').split('.'))
+ module = import_module(self.module)
+ attrs = filter(None, (self.attr or '').split('.'))
return functools.reduce(getattr, attrs, module)
@property
def module(self) -> str:
- match = self.pattern.match(self.value)
- assert match is not None
- return match.group('module')
+ return self._match.module
@property
def attr(self) -> str:
- match = self.pattern.match(self.value)
- assert match is not None
- return match.group('attr')
+ return self._match.attr
@property
- def extras(self) -> List[str]:
+ def extras(self) -> list[str]:
+ return re.findall(r'\w+', self._match.extras or '')
+
+ @functools.cached_property
+ def _match(self) -> _EntryPointMatch:
match = self.pattern.match(self.value)
- assert match is not None
- return re.findall(r'\w+', match.group('extras') or '')
+ if not match:
+ raise ValueError(
+ 'Invalid object reference. '
+ 'See https://packaging.python.org'
+ '/en/latest/specifications/entry-points/#data-model',
+ self.value,
+ )
+ return _EntryPointMatch(**match.groupdict())
def _for(self, dist):
vars(self).update(dist=dist)
>>> ep.matches(attr='bong')
True
"""
+ self._disallow_dist(params)
attrs = (getattr(self, param) for param in params)
return all(map(operator.eq, params.values(), attrs))
+ @staticmethod
+ def _disallow_dist(params):
+ """
+ Querying by dist is not allowed (dist objects are not comparable).
+ >>> EntryPoint(name='fan', value='fav', group='fag').matches(dist='foo')
+ Traceback (most recent call last):
+ ...
+ ValueError: "dist" is not suitable for matching...
+ """
+ if "dist" in params:
+ raise ValueError(
+ '"dist" is not suitable for matching. '
+ "Instead, use Distribution.entry_points.select() on a "
+ "located distribution."
+ )
+
def _key(self):
return self.name, self.value, self.group
__slots__ = ()
- def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override]
+ def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override] # Work with str instead of int
"""
Get the EntryPoint in self matching name.
"""
return EntryPoints(ep for ep in self if ep.matches(**params))
@property
- def names(self) -> Set[str]:
+ def names(self) -> set[str]:
"""
Return the set of all names of all entry points.
"""
return {ep.name for ep in self}
@property
- def groups(self) -> Set[str]:
+ def groups(self) -> set[str]:
"""
Return the set of all groups of all entry points.
"""
class PackagePath(pathlib.PurePosixPath):
"""A reference to a path in a package"""
- hash: Optional[FileHash]
+ hash: FileHash | None
size: int
dist: Distribution
- def read_text(self, encoding: str = 'utf-8') -> str: # type: ignore[override]
+ def read_text(self, encoding: str = 'utf-8') -> str:
return self.locate().read_text(encoding=encoding)
def read_binary(self) -> bytes:
"""
@abc.abstractmethod
- def read_text(self, filename) -> Optional[str]:
+ def read_text(self, filename) -> str | None:
"""Attempt to load metadata file given by the name.
Python distribution metadata is organized by blobs of text
"""
Given a path to a file in this distribution, return a SimplePath
to it.
+
+ This method is used by callers of ``Distribution.files()`` to
+ locate files within the distribution. If it's possible for a
+ Distribution to represent files in the distribution as
+ ``SimplePath`` objects, it should implement this method
+ to resolve such objects.
+
+ Some Distribution providers may elect not to resolve SimplePath
+ objects within the distribution by raising a
+ NotImplementedError, but consumers of such a Distribution would
+ be unable to invoke ``Distribution.files()``.
"""
@classmethod
@classmethod
def discover(
- cls, *, context: Optional[DistributionFinder.Context] = None, **kwargs
+ cls, *, context: DistributionFinder.Context | None = None, **kwargs
) -> Iterable[Distribution]:
"""Return an iterable of Distribution objects for all packages.
return filter(None, declared)
@property
- def metadata(self) -> _meta.PackageMetadata:
+ def metadata(self) -> _meta.PackageMetadata | None:
"""Return the parsed metadata for this Distribution.
The returned object will have keys that name the various bits of
Custom providers may provide the METADATA file or override this
property.
"""
- # deferred for performance (python/cpython#109829)
- from . import _adapters
- opt_text = (
+ text = (
self.read_text('METADATA')
or self.read_text('PKG-INFO')
# This last clause is here to support old egg-info files. Its
# (which points to the egg-info file) attribute unchanged.
or self.read_text('')
)
- text = cast(str, opt_text)
+ return self._assemble_message(text)
+
+ @staticmethod
+ @pass_none
+ def _assemble_message(text: str) -> _meta.PackageMetadata:
+ # deferred for performance (python/cpython#109829)
+ from . import _adapters
+
return _adapters.Message(email.message_from_string(text))
@property
def name(self) -> str:
"""Return the 'Name' metadata for the distribution package."""
- return self.metadata['Name']
+ return md_none(self.metadata)['Name']
@property
def _normalized_name(self):
@property
def version(self) -> str:
"""Return the 'Version' metadata for the distribution package."""
- return self.metadata['Version']
+ return md_none(self.metadata)['Version']
@property
def entry_points(self) -> EntryPoints:
return EntryPoints._from_text_for(self.read_text('entry_points.txt'), self)
@property
- def files(self) -> Optional[List[PackagePath]]:
+ def files(self) -> list[PackagePath] | None:
"""Files in this distribution.
:return: List of PackagePath for this distribution or None
return text and map('"{}"'.format, text.splitlines())
@property
- def requires(self) -> Optional[List[str]]:
+ def requires(self) -> list[str] | None:
"""Generated requirements specified for this Distribution"""
reqs = self._read_dist_info_reqs() or self._read_egg_info_reqs()
return reqs and list(reqs)
return self._load_json('direct_url.json')
def _load_json(self, filename):
+ # Deferred for performance (python/importlib_metadata#503)
+ import json
+
return pass_none(json.loads)(
self.read_text(filename),
object_hook=lambda data: types.SimpleNamespace(**data),
vars(self).update(kwargs)
@property
- def path(self) -> List[str]:
+ def path(self) -> list[str]:
"""
The sequence of directory path that a distribution finder
should search.
True
"""
- @functools.lru_cache() # type: ignore
+ @functools.lru_cache() # type: ignore[misc]
def __new__(cls, root):
return super().__new__(cls)
return []
def zip_children(self):
+ # deferred for performance (python/importlib_metadata#502)
+ import zipfile
+
zip_path = zipfile.Path(self.root)
names = zip_path.root.namelist()
self.joinpath = zip_path.joinpath
normalized = None
legacy_normalized = None
- def __init__(self, name: Optional[str]):
+ def __init__(self, name: str | None):
self.name = name
if name is None:
return
"""
self._path = path
- def read_text(self, filename: str | os.PathLike[str]) -> Optional[str]:
+ def read_text(self, filename: str | os.PathLike[str]) -> str | None:
with suppress(
FileNotFoundError,
IsADirectoryError,
return Distribution.discover(**kwargs)
-def metadata(distribution_name: str) -> _meta.PackageMetadata:
+def metadata(distribution_name: str) -> _meta.PackageMetadata | None:
"""Get the metadata for the named package.
:param distribution_name: The name of the distribution package to query.
return EntryPoints(eps).select(**params)
-def files(distribution_name: str) -> Optional[List[PackagePath]]:
+def files(distribution_name: str) -> list[PackagePath] | None:
"""Return a list of files for the named package.
:param distribution_name: The name of the distribution package to query.
return distribution(distribution_name).files
-def requires(distribution_name: str) -> Optional[List[str]]:
+def requires(distribution_name: str) -> list[str] | None:
"""
Return a list of requirements for the named package.
return distribution(distribution_name).requires
-def packages_distributions() -> Mapping[str, List[str]]:
+def packages_distributions() -> Mapping[str, list[str]]:
"""
Return a mapping of top-level packages to their
distributions.
pkg_to_dist = collections.defaultdict(list)
for dist in distributions():
for pkg in _top_level_declared(dist) or _top_level_inferred(dist):
- pkg_to_dist[pkg].append(dist.metadata['Name'])
+ pkg_to_dist[pkg].append(md_none(dist.metadata)['Name'])
return dict(pkg_to_dist)
return (dist.read_text('top_level.txt') or '').split()
-def _topmost(name: PackagePath) -> Optional[str]:
+def _topmost(name: PackagePath) -> str | None:
"""
Return the top-most parent as long as there is a parent.
"""
>>> _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)
- )
+ # Defer import of inspect for performance (python/cpython#118761)
+ import inspect
+
+ return _topmost(name) or inspect.getmodulename(name) or str(name)
def _top_level_inferred(dist):
-# from jaraco.path 3.7
+# from jaraco.path 3.7.2
+
+from __future__ import annotations
import functools
import pathlib
-from typing import Dict, Protocol, Union
-from typing import runtime_checkable
+from collections.abc import Mapping
+from typing import TYPE_CHECKING, Protocol, Union, runtime_checkable
+
+if TYPE_CHECKING:
+ from typing_extensions import Self
class Symlink(str):
"""
-FilesSpec = Dict[str, Union[str, bytes, Symlink, 'FilesSpec']] # type: ignore
+FilesSpec = Mapping[str, Union[str, bytes, Symlink, 'FilesSpec']]
@runtime_checkable
class TreeMaker(Protocol):
- def __truediv__(self, *args, **kwargs): ... # pragma: no cover
-
- def mkdir(self, **kwargs): ... # pragma: no cover
-
- def write_text(self, content, **kwargs): ... # pragma: no cover
-
- def write_bytes(self, content): ... # pragma: no cover
-
- def symlink_to(self, target): ... # pragma: no cover
+ def __truediv__(self, other, /) -> Self: ...
+ def mkdir(self, *, exist_ok) -> object: ...
+ def write_text(self, content, /, *, encoding) -> object: ...
+ def write_bytes(self, content, /) -> object: ...
+ def symlink_to(self, target, /) -> object: ...
-def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker:
- return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore
+def _ensure_tree_maker(obj: str | TreeMaker) -> TreeMaker:
+ return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj)
def build(
spec: FilesSpec,
- prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore
+ prefix: str | TreeMaker = pathlib.Path(),
):
"""
Build a set of files/directories, as described by the spec.
@functools.singledispatch
-def create(content: Union[str, bytes, FilesSpec], path):
+def create(content: str | bytes | FilesSpec, path: TreeMaker) -> None:
path.mkdir(exist_ok=True)
- build(content, prefix=path) # type: ignore
+ # Mypy only looks at the signature of the main singledispatch method. So it must contain the complete Union
+ build(content, prefix=path) # type: ignore[arg-type] # python/mypy#11727
@create.register
-def _(content: bytes, path):
+def _(content: bytes, path: TreeMaker) -> None:
path.write_bytes(content)
@create.register
-def _(content: str, path):
+def _(content: str, path: TreeMaker) -> None:
path.write_text(content, encoding='utf-8')
@create.register
-def _(content: Symlink, path):
+def _(content: Symlink, path: TreeMaker) -> None:
path.symlink_to(content)