import abc
import csv
import sys
+import json
import email
+import types
+import inspect
import pathlib
import zipfile
import operator
import itertools
import posixpath
import collections
-import inspect
from . import _adapters, _meta
from ._collections import FreezableDefaultDict, Pair
from importlib import import_module
from importlib.abc import MetaPathFinder
from itertools import starmap
-from typing import List, Mapping, Optional, cast
-
+from typing import Iterable, List, Mapping, Optional, Set, Union, cast
__all__ = [
'Distribution',
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
yield Pair(name, value)
@staticmethod
- def valid(line):
+ def valid(line: str):
return line and not line.startswith('#')
-class DeprecatedTuple:
- """
- Provide subscript item access for backward compatibility.
-
- >>> recwarn = getfixture('recwarn')
- >>> ep = EntryPoint(name='name', value='value', group='group')
- >>> ep[:]
- ('name', 'value', 'group')
- >>> ep[0]
- 'name'
- >>> len(recwarn)
- 1
- """
-
- # Do not remove prior to 2023-05-01 or Python 3.13
- _warn = functools.partial(
- warnings.warn,
- "EntryPoint tuple interface is deprecated. Access members by name.",
- DeprecationWarning,
- stacklevel=2,
- )
-
- def __getitem__(self, item):
- self._warn()
- return self._key()[item]
-
-
-class EntryPoint(DeprecatedTuple):
+class EntryPoint:
"""An entry point as defined by Python packaging conventions.
See `the packaging docs on entry points
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):
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):
f'group={self.group!r})'
)
- def __hash__(self):
+ def __hash__(self) -> int:
return hash(self._key())
__slots__ = ()
- def __getitem__(self, name): # -> EntryPoint:
+ def __getitem__(self, name: str) -> EntryPoint: # type: ignore[override]
"""
Get the EntryPoint in self matching name.
"""
except StopIteration:
raise KeyError(name)
+ def __repr__(self):
+ """
+ Repr with classname and tuple constructor to
+ signal that we deviate from regular tuple behavior.
+ """
+ return '%s(%r)' % (self.__class__.__name__, tuple(self))
+
def select(self, **params):
"""
Select entry points from self that match the
return EntryPoints(ep for ep in self if ep.matches(**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.
"""
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}>'
"""
@abc.abstractmethod
- def locate_file(self, path):
+ def locate_file(self, path: Union[str, os.PathLike[str]]) -> 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.
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
)
@staticmethod
- def at(path):
+ def at(path: Union[str, os.PathLike[str]]) -> "Distribution":
"""Return a Distribution for the indicated metadata path
:param path: a string or path-like object
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']
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
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)
space = url_req_space(section.value)
yield section.value + space + quoted_marker(section.name)
+ @property
+ def origin(self):
+ return self._load_json('direct_url.json')
+
+ def _load_json(self, filename):
+ return pass_none(json.loads)(
+ self.read_text(filename),
+ object_hook=lambda data: types.SimpleNamespace(**data),
+ )
+
class DistributionFinder(MetaPathFinder):
"""
vars(self).update(kwargs)
@property
- def path(self):
+ def path(self) -> List[str]:
"""
The sequence of directory path that a distribution finder
should search.
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.
class MetadataPathFinder(DistributionFinder):
@classmethod
- def find_distributions(cls, context=DistributionFinder.Context()):
+ def find_distributions(
+ cls, context=DistributionFinder.Context()
+ ) -> Iterable["PathDistribution"]:
"""
Find distributions.
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: Union[str, os.PathLike[str]]) -> Optional[str]:
with suppress(
FileNotFoundError,
IsADirectoryError,
):
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: Union[str, os.PathLike[str]]) -> pathlib.Path:
return self._path.parent / path
@property
return name
-def distribution(distribution_name):
+def distribution(distribution_name: str) -> Distribution:
"""Get the ``Distribution`` instance for the named package.
:param distribution_name: The name of the distribution package as a string.
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.
return Distribution.discover(**kwargs)
-def metadata(distribution_name) -> _meta.PackageMetadata:
+def metadata(distribution_name: str) -> _meta.PackageMetadata:
"""Get the metadata for the named package.
:param distribution_name: The name of the distribution package to query.
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.
:param distribution_name: The name of the distribution package to query.
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.
:param distribution_name: The name of the distribution package to query.
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: An iterator of requirements, suitable for
+ :return: An iterable of requirements, suitable for
packaging.requirement.Requirement.
"""
return distribution(distribution_name).requires
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):
- opt_names = {
- f.parts[0] if len(f.parts) > 1 else inspect.getmodulename(f)
- for f in always_iterable(dist.files)
- }
+ opt_names = set(map(_get_toplevel_name, always_iterable(dist.files)))
- @pass_none
def importable_name(name):
return '.' not in name
import os
import sys
import copy
+import json
import shutil
import pathlib
import tempfile
self.fixtures.enter_context(self.add_sys_path(self.site_dir))
-class DistInfoPkg(OnSysPath, SiteDir):
+class SiteBuilder(SiteDir):
+ def setUp(self):
+ super().setUp()
+ for cls in self.__class__.mro():
+ with contextlib.suppress(AttributeError):
+ build_files(cls.files, prefix=self.site_dir)
+
+
+class DistInfoPkg(OnSysPath, SiteBuilder):
files: FilesSpec = {
"distinfo_pkg-1.0.0.dist-info": {
"METADATA": """
""",
}
- def setUp(self):
- super().setUp()
- build_files(DistInfoPkg.files, self.site_dir)
-
def make_uppercase(self):
"""
Rewrite metadata with everything uppercase.
build_files(files, self.site_dir)
-class DistInfoPkgWithDot(OnSysPath, SiteDir):
+class DistInfoPkgEditable(DistInfoPkg):
+ """
+ Package with a PEP 660 direct_url.json.
+ """
+
+ some_hash = '524127ce937f7cb65665130c695abd18ca386f60bb29687efb976faa1596fdcc'
+ files: FilesSpec = {
+ 'distinfo_pkg-1.0.0.dist-info': {
+ 'direct_url.json': json.dumps(
+ {
+ "archive_info": {
+ "hash": f"sha256={some_hash}",
+ "hashes": {"sha256": f"{some_hash}"},
+ },
+ "url": "file:///path/to/distinfo_pkg-1.0.0.editable-py3-none-any.whl",
+ }
+ )
+ },
+ }
+
+
+class DistInfoPkgWithDot(OnSysPath, SiteBuilder):
files: FilesSpec = {
"pkg_dot-1.0.0.dist-info": {
"METADATA": """
},
}
- def setUp(self):
- super().setUp()
- build_files(DistInfoPkgWithDot.files, self.site_dir)
-
-class DistInfoPkgWithDotLegacy(OnSysPath, SiteDir):
+class DistInfoPkgWithDotLegacy(OnSysPath, SiteBuilder):
files: FilesSpec = {
"pkg.dot-1.0.0.dist-info": {
"METADATA": """
},
}
- def setUp(self):
- super().setUp()
- build_files(DistInfoPkgWithDotLegacy.files, self.site_dir)
-
-class DistInfoPkgOffPath(SiteDir):
- def setUp(self):
- super().setUp()
- build_files(DistInfoPkg.files, self.site_dir)
+class DistInfoPkgOffPath(SiteBuilder):
+ files = DistInfoPkg.files
-class EggInfoPkg(OnSysPath, SiteDir):
+class EggInfoPkg(OnSysPath, SiteBuilder):
files: FilesSpec = {
"egginfo_pkg.egg-info": {
"PKG-INFO": """
""",
}
- def setUp(self):
- super().setUp()
- build_files(EggInfoPkg.files, prefix=self.site_dir)
-
-class EggInfoPkgPipInstalledNoToplevel(OnSysPath, SiteDir):
+class EggInfoPkgPipInstalledNoToplevel(OnSysPath, SiteBuilder):
files: FilesSpec = {
"egg_with_module_pkg.egg-info": {
"PKG-INFO": "Name: egg_with_module-pkg",
""",
}
- def setUp(self):
- super().setUp()
- build_files(EggInfoPkgPipInstalledNoToplevel.files, prefix=self.site_dir)
-
-class EggInfoPkgPipInstalledNoModules(OnSysPath, SiteDir):
+class EggInfoPkgPipInstalledNoModules(OnSysPath, SiteBuilder):
files: FilesSpec = {
"egg_with_no_modules_pkg.egg-info": {
"PKG-INFO": "Name: egg_with_no_modules-pkg",
},
}
- def setUp(self):
- super().setUp()
- build_files(EggInfoPkgPipInstalledNoModules.files, prefix=self.site_dir)
-
-class EggInfoPkgSourcesFallback(OnSysPath, SiteDir):
+class EggInfoPkgSourcesFallback(OnSysPath, SiteBuilder):
files: FilesSpec = {
"sources_fallback_pkg.egg-info": {
"PKG-INFO": "Name: sources_fallback-pkg",
""",
}
- def setUp(self):
- super().setUp()
- build_files(EggInfoPkgSourcesFallback.files, prefix=self.site_dir)
-
-class EggInfoFile(OnSysPath, SiteDir):
+class EggInfoFile(OnSysPath, SiteBuilder):
files: FilesSpec = {
"egginfo_file.egg-info": """
Metadata-Version: 1.0
""",
}
- def setUp(self):
- super().setUp()
- build_files(EggInfoFile.files, prefix=self.site_dir)
-
# dedent all text strings before writing
orig = _path.create.registry[str]