Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 65 additions & 28 deletions importlib_metadata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@
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, Iterable, List, Mapping, Match, Optional, Set, cast
from typing import Any, cast

from . import _meta
from ._collections import FreezableDefaultDict, Pair
Expand Down Expand Up @@ -133,6 +134,12 @@ def valid(line: str):
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.

Expand All @@ -148,6 +155,30 @@ class EntryPoint:
'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(
Expand Down Expand Up @@ -175,38 +206,44 @@ class EntryPoint:
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)
Expand Down Expand Up @@ -305,14 +342,14 @@ def select(self, **params) -> EntryPoints:
return EntryPoints(ep for ep in self if py39.ep_matches(ep, **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.
"""
Expand All @@ -333,7 +370,7 @@ def _from_text(text):
class PackagePath(pathlib.PurePosixPath):
"""A reference to a path in a package"""

hash: Optional[FileHash]
hash: FileHash | None
size: int
dist: Distribution

Expand Down Expand Up @@ -368,7 +405,7 @@ class Distribution(metaclass=abc.ABCMeta):
"""

@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
Expand Down Expand Up @@ -428,7 +465,7 @@ def from_name(cls, name: str) -> Distribution:

@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.

Expand Down Expand Up @@ -524,7 +561,7 @@ 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
Expand Down Expand Up @@ -616,7 +653,7 @@ def _read_files_egginfo_sources(self):
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)
Expand Down Expand Up @@ -722,7 +759,7 @@ def __init__(self, **kwargs):
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.
Expand Down Expand Up @@ -874,7 +911,7 @@ class Prepared:
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
Expand Down Expand Up @@ -944,7 +981,7 @@ def __init__(self, path: SimplePath) -> None:
"""
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,
Expand Down Expand Up @@ -1051,7 +1088,7 @@ def entry_points(**params) -> EntryPoints:
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.
Expand All @@ -1060,7 +1097,7 @@ def files(distribution_name: str) -> Optional[List[PackagePath]]:
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.

Expand All @@ -1070,7 +1107,7 @@ def requires(distribution_name: str) -> Optional[List[str]]:
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.
Expand All @@ -1091,7 +1128,7 @@ def _top_level_declared(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.
"""
Expand Down
6 changes: 5 additions & 1 deletion importlib_metadata/_collections.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import collections
import typing


# from jaraco.collections 3.3
Expand All @@ -24,7 +25,10 @@ def freeze(self):
self._frozen = lambda key: self.default_factory()


class Pair(collections.namedtuple('Pair', 'name value')):
class Pair(typing.NamedTuple):
name: str
value: str

@classmethod
def parse(cls, text):
return cls(*map(str.strip, text.split("=", 1)))
20 changes: 8 additions & 12 deletions importlib_metadata/_meta.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
from __future__ import annotations

import os
from collections.abc import Iterator
from typing import (
Any,
Dict,
Iterator,
List,
Optional,
Protocol,
TypeVar,
Union,
overload,
)

Expand All @@ -28,25 +24,25 @@ def __iter__(self) -> Iterator[str]: ... # pragma: no cover
@overload
def get(
self, name: str, failobj: None = None
) -> Optional[str]: ... # pragma: no cover
) -> str | None: ... # pragma: no cover

@overload
def get(self, name: str, failobj: _T) -> Union[str, _T]: ... # pragma: no cover
def get(self, name: str, failobj: _T) -> 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
) -> list[Any] | None: ... # pragma: no cover

@overload
def get_all(self, name: str, failobj: _T) -> Union[List[Any], _T]:
def get_all(self, name: str, failobj: _T) -> list[Any] | _T:
"""
Return all values associated with a possibly multi-valued key.
"""

@property
def json(self) -> Dict[str, Union[str, List[str]]]:
def json(self) -> dict[str, str | list[str]]:
"""
A JSON-compatible form of the metadata.
"""
Expand All @@ -58,11 +54,11 @@ class SimplePath(Protocol):
"""

def joinpath(
self, other: Union[str, os.PathLike[str]]
self, other: str | os.PathLike[str]
) -> SimplePath: ... # pragma: no cover

def __truediv__(
self, other: Union[str, os.PathLike[str]]
self, other: str | os.PathLike[str]
) -> SimplePath: ... # pragma: no cover

@property
Expand Down
6 changes: 4 additions & 2 deletions importlib_metadata/compat/py39.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
Compatibility layer with Python 3.8/3.9
"""

from typing import TYPE_CHECKING, Any, Optional
from __future__ import annotations

from typing import TYPE_CHECKING, Any

if TYPE_CHECKING: # pragma: no cover
# Prevent circular imports on runtime.
Expand All @@ -11,7 +13,7 @@
Distribution = EntryPoint = Any


def normalized_name(dist: Distribution) -> Optional[str]:
def normalized_name(dist: Distribution) -> str | None:
"""
Honor name normalization for distributions that don't provide ``_normalized_name``.
"""
Expand Down
3 changes: 2 additions & 1 deletion tests/_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

import functools
import pathlib
from typing import TYPE_CHECKING, Mapping, Protocol, Union, runtime_checkable
from collections.abc import Mapping
from typing import TYPE_CHECKING, Protocol, Union, runtime_checkable

if TYPE_CHECKING:
from typing_extensions import Self
Expand Down