Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add type hints #10

Merged
merged 17 commits into from
Feb 7, 2024
Merged
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
10 changes: 10 additions & 0 deletions docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@

# Be strict about any broken references
nitpicky = True
nitpick_ignore = [
('py:class', 'Self'),
('py:class', '_T'),
('py:obj', 'jaraco.classes.properties._T'),
('py:class', '_ClassPropertyAttribute'),
('py:class', '_GetterCallable'),
('py:class', '_GetterClassMethod'),
('py:class', '_SetterCallable'),
('py:class', '_SetterClassMethod'),
]

# Include Python intersphinx mapping to prevent failures
# jaraco/skeleton#51
Expand Down
18 changes: 13 additions & 5 deletions jaraco/classes/ancestry.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,18 @@
of an object and its parent classes.
"""

from __future__ import annotations

from typing import TYPE_CHECKING, cast

from more_itertools import unique_everseen

if TYPE_CHECKING:
from collections.abc import Iterator
from typing import Any


def all_bases(c):
def all_bases(c: type[object]) -> list[type[Any]]:
"""
return a tuple of all base classes the class c has as a parent.
>>> object in all_bases(list)
Expand All @@ -15,7 +23,7 @@ def all_bases(c):
return c.mro()[1:]


def all_classes(c):
def all_classes(c: type[object]) -> list[type[Any]]:
"""
return a tuple of all classes to which c belongs
>>> list in all_classes(list)
Expand All @@ -28,7 +36,7 @@ def all_classes(c):
# http://code.activestate.com/recipes/576949-find-all-subclasses-of-a-given-class/


def iter_subclasses(cls):
def iter_subclasses(cls: type[object]) -> Iterator[type[Any]]:
"""
Generator over all subclasses of a given class, in depth-first order.

Expand Down Expand Up @@ -58,11 +66,11 @@ def iter_subclasses(cls):
return unique_everseen(_iter_all_subclasses(cls))


def _iter_all_subclasses(cls):
def _iter_all_subclasses(cls: type[object]) -> Iterator[type[Any]]:
try:
subs = cls.__subclasses__()
except TypeError: # fails only when cls is type
subs = cls.__subclasses__(cls)
subs = cast('type[type]', cls).__subclasses__(cls)
for sub in subs:
yield sub
yield from iter_subclasses(sub)
23 changes: 21 additions & 2 deletions jaraco/classes/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
Some useful metaclasses.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Any


class LeafClassesMeta(type):
"""
Expand All @@ -26,7 +33,14 @@ class LeafClassesMeta(type):
1
"""

def __init__(cls, name, bases, attrs):
_leaf_classes: set[type[Any]]

def __init__(
cls,
name: str,
bases: tuple[type[object], ...],
attrs: dict[str, object],
) -> None:
if not hasattr(cls, '_leaf_classes'):
cls._leaf_classes = set()
leaf_classes = getattr(cls, '_leaf_classes')
Expand Down Expand Up @@ -56,7 +70,12 @@ class TagRegistered(type):

attr_name = 'tag'

def __init__(cls, name, bases, namespace):
def __init__(
cls,
name: str,
bases: tuple[type[object], ...],
namespace: dict[str, object],
) -> None:
super(TagRegistered, cls).__init__(name, bases, namespace)
if not hasattr(cls, '_registry'):
cls._registry = {}
Expand Down
94 changes: 82 additions & 12 deletions jaraco/classes/properties.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,28 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Generic, TypeVar, cast, overload

_T = TypeVar('_T')

if TYPE_CHECKING:
from collections.abc import Callable
from typing import Any, Protocol

from typing_extensions import Self, TypeAlias
bswck marked this conversation as resolved.
Show resolved Hide resolved

# TODO(coherent-oss/granary#4): Migrate to PEP 695 by 2027-10.
_GetterCallable: TypeAlias = Callable[..., _T]
_GetterClassMethod: TypeAlias = classmethod[Any, [], _T]

_SetterCallable: TypeAlias = Callable[[type[Any], _T], None]
_SetterClassMethod: TypeAlias = classmethod[Any, [_T], None]

class _ClassPropertyAttribute(Protocol[_T]):
def __get__(self, obj: object, objtype: type[Any] | None = None) -> _T: ...

def __set__(self, obj: object, value: _T) -> None: ...


class NonDataProperty:
"""Much like the property builtin, but only implements __get__,
making it a non-data property, and can be subsequently reset.
Expand All @@ -21,18 +46,36 @@ class NonDataProperty:
<....properties.NonDataProperty object at ...>
"""

def __init__(self, fget):
def __init__(self, fget: Callable[[object], object]) -> None:
assert fget is not None, "fget cannot be none"
assert callable(fget), "fget must be callable"
self.fget = fget

def __get__(self, obj, objtype=None):
@overload
def __get__(
self,
obj: None,
objtype: type[object] | None = None,
) -> Self: ...

@overload
def __get__(
self,
obj: object,
objtype: type[object] | None = None,
) -> object: ...

def __get__(
self,
obj: object | None,
objtype: type[object] | None = None,
) -> Self | object:
if obj is None:
return self
return self.fget(obj)


class classproperty:
class classproperty(Generic[_T]):
"""
Like @property but applies at the class level.

Expand Down Expand Up @@ -135,36 +178,63 @@ class classproperty:
4
"""

fget: _ClassPropertyAttribute[_GetterClassMethod[_T]]
fset: _ClassPropertyAttribute[_SetterClassMethod[_T] | None]

class Meta(type):
def __setattr__(self, key, value):
def __setattr__(self, key: str, value: object) -> None:
obj = self.__dict__.get(key, None)
if type(obj) is classproperty:
return obj.__set__(self, value)
return super().__setattr__(key, value)

def __init__(self, fget, fset=None):
def __init__(
self,
fget: _GetterCallable[_T] | _GetterClassMethod[_T],
fset: _SetterCallable[_T] | _SetterClassMethod[_T] | None = None,
) -> None:
self.fget = self._ensure_method(fget)
self.fset = fset
self.fset = fset # type: ignore[assignment] # Corrected in the next line.
fset and self.setter(fset)

def __get__(self, instance, owner=None):
def __get__(self, instance: object, owner: type[object] | None = None) -> _T:
return self.fget.__get__(None, owner)()

def __set__(self, owner, value):
def __set__(self, owner: object, value: _T) -> None:
if not self.fset:
raise AttributeError("can't set attribute")
if type(owner) is not classproperty.Meta:
owner = type(owner)
return self.fset.__get__(None, owner)(value)
return self.fset.__get__(None, cast('type[object]', owner))(value)

def setter(self, fset):
def setter(self, fset: _SetterCallable[_T] | _SetterClassMethod[_T]) -> Self:
self.fset = self._ensure_method(fset)
return self

@overload
@classmethod
def _ensure_method(
cls,
fn: _GetterCallable[_T] | _GetterClassMethod[_T],
) -> _GetterClassMethod[_T]: ...

@overload
@classmethod
def _ensure_method(
cls,
fn: _SetterCallable[_T] | _SetterClassMethod[_T],
) -> _SetterClassMethod[_T]: ...

@classmethod
def _ensure_method(cls, fn):
def _ensure_method(
cls,
fn: _GetterCallable[_T]
| _GetterClassMethod[_T]
| _SetterCallable[_T]
| _SetterClassMethod[_T],
) -> _GetterClassMethod[_T] | _SetterClassMethod[_T]:
"""
Ensure fn is a classmethod or staticmethod.
"""
needs_method = not isinstance(fn, (classmethod, staticmethod))
return classmethod(fn) if needs_method else fn
return classmethod(fn) if needs_method else fn # type: ignore[arg-type,return-value]
Empty file added jaraco/classes/py.typed
Empty file.
Loading