Skip to content

Commit

Permalink
Strict typing and py.typed
Browse files Browse the repository at this point in the history
  • Loading branch information
Avasam committed Aug 29, 2024
1 parent 1bf2ede commit 9dc87de
Show file tree
Hide file tree
Showing 7 changed files with 63 additions and 34 deletions.
13 changes: 12 additions & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,15 @@
packaging=('https://packaging.pypa.io/en/stable/', None),
)

nitpick_ignore.append(('py:class', 'importlib.metadata.Distribution'))
nitpick_ignore += [
# jaraco/nspektr#3
('py:class', 'Requirement'),
('py:class', 'Marker'),
('py:class', 'importlib.metadata.EntryPoint'),
('py:class', 'itertools.chain'),
('py:class', 'metadata.EntryPoint'),
('py:class', 'itertools.filterfalse'),
('py:class', 'Requirement'),
('py:class', 'nspektr._T'),
('py:class', 'importlib.metadata.Distribution'),
]
2 changes: 1 addition & 1 deletion mypy.ini
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[mypy]
# Is the project well-typed?
strict = False
strict = True

# Early opt-in even when strict = False
warn_unused_ignores = True
Expand Down
1 change: 1 addition & 0 deletions newsfragments/3.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Complete annotations and add ``py.typed`` marker -- by :user:`Avasam`
54 changes: 35 additions & 19 deletions nspektr/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
import itertools
import functools
from __future__ import annotations

import contextlib
import functools
import itertools
from collections.abc import Callable, Iterable
from typing import TYPE_CHECKING, Iterator, TypeVar

from packaging.requirements import Requirement
from packaging.version import Version
from more_itertools import always_iterable
from jaraco.context import suppress
from jaraco.functools import apply
from more_itertools import always_iterable
from packaging.markers import Marker
from packaging.requirements import Requirement
from packaging.version import Version

from ._compat import metadata, repair_extras

if TYPE_CHECKING:
from typing_extensions import Literal, Self

_T = TypeVar("_T")


def resolve(req: Requirement) -> metadata.Distribution:
"""
Expand All @@ -25,13 +35,13 @@ def resolve(req: Requirement) -> metadata.Distribution:
dist = metadata.distribution(req.name)
if not req.specifier.contains(Version(dist.version), prereleases=True):
raise metadata.PackageNotFoundError(str(req))
dist.extras = req.extras # type: ignore
dist.extras = req.extras # type: ignore[attr-defined] # Adding extras as if this was an EntryPoint
return dist


@apply(bool)
@suppress(metadata.PackageNotFoundError)
def is_satisfied(req: Requirement):
def is_satisfied(req: Requirement) -> metadata.Distribution:
return resolve(req)


Expand All @@ -40,21 +50,23 @@ def is_satisfied(req: Requirement):

class NullMarker:
@classmethod
def wrap(cls, req: Requirement):
def wrap(cls, req: Requirement) -> Marker | Self:
return req.marker or cls()

def evaluate(self, *args, **kwargs):
def evaluate(self, *args: object, **kwargs: object) -> Literal[True]:
return True


def find_direct_dependencies(dist, extras=None):
def find_direct_dependencies(
dist: metadata.Distribution, extras: str | Iterable[str | None] | None = None
) -> itertools.chain[Requirement]:
"""
Find direct, declared dependencies for dist.
"""
simple = (
req
for req in map(Requirement, always_iterable(dist.requires))
if NullMarker.wrap(req).evaluate(dict(extra=None))
if NullMarker.wrap(req).evaluate(dict(extra=""))
)
extra_deps = (
req
Expand All @@ -65,7 +77,7 @@ def find_direct_dependencies(dist, extras=None):
return itertools.chain(simple, extra_deps)


def traverse(items, visit):
def traverse(items: Iterator[_T], visit: Callable[[_T], Iterable[_T]]) -> Iterator[_T]:
"""
Given an iterable of items, traverse the items.
Expand All @@ -81,13 +93,15 @@ def traverse(items, visit):
items = itertools.chain(items, visit(item))


def find_req_dependencies(req):
def find_req_dependencies(req: Requirement) -> Iterator[Requirement]:
with contextlib.suppress(metadata.PackageNotFoundError):
dist = resolve(req)
yield from find_direct_dependencies(dist)


def find_dependencies(dist, extras=None):
def find_dependencies(
dist: metadata.Distribution, extras: str | Iterable[str | None] | None = None
) -> Iterator[Requirement]:
"""
Find all reachable dependencies for dist.
Expand All @@ -104,7 +118,9 @@ def find_dependencies(dist, extras=None):
True
"""

def visit(req, seen=set()):
def visit(
req: Requirement, seen: set[Requirement] = set()
) -> tuple[()] | Iterator[Requirement]:
if req in seen:
return ()
seen.add(req)
Expand All @@ -114,18 +130,18 @@ def visit(req, seen=set()):


class Unresolved(Exception):
def __iter__(self):
def __iter__(self) -> Iterator[Requirement]:
return iter(self.args[0])


def missing(ep):
def missing(ep: metadata.EntryPoint) -> itertools.filterfalse[Requirement]:
"""
Generate the unresolved dependencies (if any) of ep.
"""
return unsatisfied(find_dependencies(ep.dist, repair_extras(ep.extras)))
return unsatisfied(find_dependencies(ep.dist, repair_extras(ep.extras))) # type: ignore[arg-type] # FIXME


def check(ep):
def check(ep: metadata.EntryPoint) -> None:
"""
>>> ep, = metadata.entry_points(group='console_scripts', name='pytest')
>>> check(ep)
Expand Down
23 changes: 14 additions & 9 deletions nspektr/_compat.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
import contextlib
import sys
from __future__ import annotations

import sys
from collections.abc import Iterable
from re import Match

if sys.version_info >= (3, 10):
import importlib.metadata as metadata
else:
import importlib_metadata as metadata # type: ignore # noqa: F401
import importlib.metadata as _metadata
else: # pragma: no cover #jaraco/skeleton#130
import importlib_metadata as _metadata # noqa: F401

metadata = _metadata # Explicit re-export


def repair_extras(extras):
def repair_extras(extras: list[str] | Iterable[Match[str]]) -> list[str]:
"""
Repair extras that appear as match objects.
python/importlib_metadata#369 revealed a flaw in the EntryPoint
implementation. This function wraps the extras to ensure
they are proper strings even on older implementations.
"""
with contextlib.suppress(AttributeError):
return list(item.group(0) for item in extras)
return extras
try:
return list(item.group(0) for item in extras) # type: ignore[union-attr] # Explicitly repairing this error
except AttributeError:
return extras # type: ignore[return-value] # On a single failure we assume it's all strings
Empty file added nspektr/py.typed
Empty file.
4 changes: 0 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,3 @@ type = [


[tool.setuptools_scm]


[tool.pytest-enabler.mypy]
# Disabled due to jaraco/skeleton#143

0 comments on commit 9dc87de

Please sign in to comment.