From ec873b0ceb8efdf5129f1acaa48d53f5a9c89e16 Mon Sep 17 00:00:00 2001 From: "Tim.Mitchell" Date: Thu, 11 May 2023 12:39:35 +1200 Subject: [PATCH 1/2] 91: Modernise all type-hints. Add more type hints Add mypy test. Add py.typed file. --- pure_interface/adaption.py | 20 +++--- pure_interface/data_classes.py | 2 +- pure_interface/delegation.py | 20 +++--- pure_interface/interface.py | 126 ++++++++++++++++----------------- pure_interface/py.typed | 0 requirements_dev.txt | 1 + setup.cfg | 3 + tests/test_func_sigs3.py | 3 +- tests/test_function_sigs.py | 3 +- tox.ini | 8 ++- 10 files changed, 95 insertions(+), 91 deletions(-) create mode 100644 pure_interface/py.typed diff --git a/pure_interface/adaption.py b/pure_interface/adaption.py index 705e4c1..1b9fbac 100644 --- a/pure_interface/adaption.py +++ b/pure_interface/adaption.py @@ -3,17 +3,16 @@ import functools import inspect import types -from typing import Any, Type, Callable +from typing import Any, Type, Callable, Optional import typing import warnings from .errors import InterfaceError, AdaptionError -from .interface import PI, Interface, InterfaceType +from .interface import AnInterface, Interface, InterfaceType, AnInterfaceType from .interface import get_type_interfaces, get_pi_attribute -def adapts(from_type, to_interface=None): - # type: (Any, Type[PI]) -> Callable[[Any], Any] +def adapts(from_type: Any, to_interface: Optional[Type[AnInterface]] = None) -> Callable[[Any], Any]: """Class or function decorator for declaring an adapter from a type to an interface. E.g. @adapts(MyClass, MyInterface) @@ -47,8 +46,7 @@ def decorator(cls): return decorator -def register_adapter(adapter, from_type, to_interface): - # type: (Callable, Any, Type[Interface]) -> None +def register_adapter(adapter: Callable, from_type: Type, to_interface: AnInterfaceType) -> None: """ Registers adapter to convert instances of from_type to objects that provide to_interface for the to_interface.adapt() method. @@ -79,25 +77,25 @@ def __init__(self, mapping_factory=dict): self._factory = mapping_factory self._adapters = mapping_factory() - def adapt(self, obj, interface): + def adapt(self, obj: Any, interface: Type[AnInterface]) -> AnInterface: """ Adapts `obj` to `interface`""" try: return self._adapters[interface][obj] except KeyError: return self._adapt(obj, interface) - def adapt_or_none(self, obj, interface): + def adapt_or_none(self, obj: Any, interface: Type[AnInterface]) -> Optional[AnInterface]: """ Adapt obj to interface returning None on failure.""" try: return self.adapt(obj, interface) except ValueError: return None - def clear(self): + def clear(self) -> None: """ Clears the cached adapters.""" self._adapters = self._factory() - def _adapt(self, obj, interface): + def _adapt(self, obj: Any, interface: Type[AnInterface]) -> AnInterface: adapted = interface.adapt(obj) try: adapters = self._adapters[interface] @@ -107,7 +105,7 @@ def _adapt(self, obj, interface): return adapted -def _interface_from_anno(annotation): +def _interface_from_anno(annotation: Any) -> Optional[AnInterfaceType]: """ Typically the annotation is the interface, but if a default value of None is given the annotation is a typing.Union[interface, None] a.k.a. Optional[interface]. Lets be nice and support those too. """ diff --git a/pure_interface/data_classes.py b/pure_interface/data_classes.py index f5dff15..aff78dd 100644 --- a/pure_interface/data_classes.py +++ b/pure_interface/data_classes.py @@ -32,7 +32,7 @@ def _get_interface_annotions(cls): 'match_args', 'kw_only', 'slots', 'weakref_slot') -def dataclass(_cls=None, **kwargs): +def dataclass(_cls: typing.Union[type, None] = None, **kwargs): """Returns the same class as was passed in, with dunder methods added based on the fields defined in the class. """ diff --git a/pure_interface/delegation.py b/pure_interface/delegation.py index 459379f..7864a46 100644 --- a/pure_interface/delegation.py +++ b/pure_interface/delegation.py @@ -1,17 +1,17 @@ from __future__ import division, absolute_import, print_function import operator +from typing import Dict, Union, Sequence, Type, Set, Tuple, Any -import pure_interface from .errors import InterfaceError -from .interface import get_interface_names, type_is_interface, get_type_interfaces, InterfaceType +from .interface import get_interface_names, type_is_interface, get_type_interfaces, AnInterfaceType, AnInterface -_composed_types_map = {} +_composed_types_map: Dict[Tuple[Type, ...], Type] = {} _letters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' class _Delegated: - def __init__(self, dotted_name): + def __init__(self, dotted_name: str): self._getter = operator.attrgetter(dotted_name) self._impl_name, self._attr_name = dotted_name.rsplit('.', 1) @@ -111,14 +111,14 @@ def __init__(self, impl): """ pi_attr_fallback = None - pi_attr_delegates = {} - pi_attr_mapping = {} + pi_attr_delegates: Dict[str, Union[str, type]] = {} + pi_attr_mapping: Dict[str, Sequence[str]] = {} def __init_subclass__(cls, **kwargs): # get non-interface base class ignoring abc.ABC and object. non_interface_bases = [base for base in cls.mro()[:-2] if not type_is_interface(base)] - def i_have_attribute(attrib): + def i_have_attribute(attrib: str) -> bool: for klass in non_interface_bases: if attrib in klass.__dict__: return True @@ -149,7 +149,7 @@ def i_have_attribute(attrib): setattr(cls, attr, _Delegated(dotted_name)) @classmethod - def provided_by(cls, obj): + def provided_by(cls, obj: Any): if not hasattr(cls, 'pi_composed_interfaces'): raise InterfaceError('provided_by() can only be called on composed types') if isinstance(obj, cls): @@ -169,7 +169,7 @@ def __composed_init__(self, *args): setattr(self, attr, impl) -def composed_type(*interface_types: InterfaceType) -> type: +def composed_type(*interface_types: AnInterfaceType) -> type: """Returns a new class which implements all the passed interfaces. If the interfaces have duplicate attribute or method names, the first enountered implementation is used. Instances of the returned type are passed implementations of the given interfaces in the same order. @@ -200,7 +200,7 @@ class B(IB, object): if c_type is not None: return c_type delegates = {} - all_names = set() + all_names: Set[str] = set() for i, interface in enumerate(interface_types): if not type_is_interface(interface): raise ValueError('all arguments to composed_type must be Interface classes') diff --git a/pure_interface/interface.py b/pure_interface/interface.py index 86dfe03..568ebfd 100644 --- a/pure_interface/interface.py +++ b/pure_interface/interface.py @@ -10,7 +10,7 @@ import inspect from inspect import signature, Signature, Parameter import types -from typing import Any, Callable, List, Optional, Iterable, FrozenSet, Type, TypeVar, Generic +from typing import Any, Callable, List, Optional, Iterable, FrozenSet, Type, TypeVar, Generic, Dict, Set, Tuple, Union import sys import warnings import weakref @@ -18,71 +18,77 @@ from .errors import InterfaceError, AdaptionError is_development = not hasattr(sys, 'frozen') -missing_method_warnings = [] +missing_method_warnings: List[str] = [] +_T = TypeVar('_T') -def set_is_development(is_dev): + +def set_is_development(is_dev: bool) -> None: global is_development is_development = is_dev -def get_is_development(): +def get_is_development() -> bool: return is_development -def get_missing_method_warnings(): +def get_missing_method_warnings() -> List[str]: return missing_method_warnings -def no_adaption(obj): +def no_adaption(obj: _T) -> _T: return obj -PI = TypeVar('PI', bound='Interface') +AnInterface = TypeVar('AnInterface', bound='Interface') +AnInterfaceType = TypeVar('AnInterfaceType', bound=Type['Interface']) class _PIAttributes(object): """ rather than clutter the class namespace with lots of _pi_XXX attributes, collect them all here""" - def __init__(self, type_is_interface, abstract_properties, interface_method_signatures, interface_attribute_names): - self.type_is_interface = type_is_interface + def __init__(self, type_is_interface: bool, + abstract_properties: Set[str], + interface_method_signatures: Dict[str, Signature], + interface_attribute_names: Set[str]): + self.type_is_interface: bool = type_is_interface # abstractproperties are checked for at instantiation. # When concrete classes use a @property then they are removed from this set self.abstractproperties = frozenset(abstract_properties) - self.interface_method_names = frozenset(interface_method_signatures.keys()) # type: FrozenSet[str] - self.interface_attribute_names = frozenset(interface_attribute_names) # type: FrozenSet[str] + self.interface_method_names = frozenset(interface_method_signatures.keys()) + self.interface_attribute_names = frozenset(interface_attribute_names) self.interface_method_signatures = interface_method_signatures - self.adapters = weakref.WeakKeyDictionary() - self.structural_subclasses = set() - self.impl_wrapper_type = None + self.adapters = weakref.WeakKeyDictionary() # type: ignore + self.structural_subclasses: Set[type] = set() + self.impl_wrapper_type: Optional[type] = None @property - def interface_names(self): + def interface_names(self) -> FrozenSet[str]: return self.interface_method_names.union(self.interface_attribute_names) class _ImplementationWrapper(object): - def __init__(self, implementation, interface): + def __init__(self, implementation: Any, interface: AnInterfaceType): object.__setattr__(self, '_ImplementationWrapper__impl', implementation) object.__setattr__(self, '_ImplementationWrapper__interface', interface) object.__setattr__(self, '_ImplementationWrapper__interface_attrs', interface._pi.interface_names) object.__setattr__(self, '_ImplementationWrapper__interface_name', interface.__name__) - def __getattr__(self, attr): + def __getattr__(self, attr: str) -> Any: impl = self.__impl if attr in self.__interface_attrs: return getattr(impl, attr) else: raise AttributeError("'{}' interface has no attribute '{}'".format(self.__interface_name, attr)) - def __setattr__(self, key, value): + def __setattr__(self, key: str, value: Any) -> None: if key in self.__interface_attrs: setattr(self.__impl, key, value) else: raise AttributeError("'{}' interface has no attribute '{}'".format(self.__interface_name, key)) -def _builtin_attrs(name): +def _builtin_attrs(name: str) -> bool: """ These attributes are ignored when checking ABC types for emptyness. """ return name in ('__doc__', '__module__', '__qualname__', '__abstractmethods__', '__dict__', @@ -91,14 +97,14 @@ def _builtin_attrs(name): '_pi', '_pi_unwrap_decorators') -def get_pi_attribute(cls, attr_name, default=None): +def get_pi_attribute(cls: Type, attr_name: str, default: Any = None) -> Any: if hasattr(cls, '_pi'): return getattr(cls._pi, attr_name) else: return default -def _type_is_interface(cls): +def _type_is_interface(cls: type) -> bool: """ Return True if cls is a pure interface or an empty ABC class""" if cls is object: return False @@ -122,9 +128,9 @@ def _type_is_interface(cls): return False -def _get_abc_interface_props_and_funcs(cls): - properties = set() - function_sigs = {} +def _get_abc_interface_props_and_funcs(cls: Type[abc.ABC]) -> Tuple[Set[str], Dict[str, Signature]]: + properties: Set[str] = set() + function_sigs: Dict[str, Signature] = {} if not hasattr(cls, '__abstractmethods__'): return properties, function_sigs for name in cls.__abstractmethods__: @@ -142,7 +148,7 @@ def _get_abc_interface_props_and_funcs(cls): return properties, function_sigs -def _unwrap_function(func): +def _unwrap_function(func: Any) -> Any: """ Look for decorated functions and return the wrapped function. """ while hasattr(func, '__wrapped__'): @@ -150,7 +156,7 @@ def _unwrap_function(func): return func -def _is_empty_function(func, unwrap=False): +def _is_empty_function(func: Any, unwrap: bool = False) -> bool: """ Return True if func is considered empty. All functions with no return statement have an implicit return None - this is explicit in the code object. """ @@ -217,7 +223,7 @@ def _is_empty_function(func, unwrap=False): if instructions[-2].opname in ('CALL_FUNCTION', 'CALL'): for instr in instructions[-3::-1]: if instr.opname == 'LOAD_GLOBAL': - return instr.argval == 'NotImplementedError' + return bool(instr.argval == 'NotImplementedError') return False @@ -225,7 +231,7 @@ def _is_empty_function(func, unwrap=False): _Instruction = collections.namedtuple('_Instruction', ('opcode', 'opname', 'arg', 'argval')) -def _get_instructions(code_obj): +def _get_instructions(code_obj: Any) -> Union[List[_Instruction], List[dis.Instruction]]: if hasattr(dis, 'get_instructions'): return list(dis.get_instructions(code_obj)) @@ -248,12 +254,13 @@ def _get_instructions(code_obj): return instructions -def _is_descriptor(obj): # in our context we only care about __get__ +def _is_descriptor(obj: Any) -> bool: # in our context we only care about __get__ return hasattr(obj, '__get__') class _ParamTypes(object): - def __init__(self, pos_only, pos_or_kw, vararg, kw_only, varkw): + def __init__(self, pos_only: List[Parameter], pos_or_kw: List[Parameter], + vararg: List[Parameter], kw_only: List[Parameter], varkw: List[Parameter]): self.pos_only = pos_only self.pos_or_kw = pos_or_kw self.vararg = vararg @@ -263,8 +270,7 @@ def __init__(self, pos_only, pos_or_kw, vararg, kw_only, varkw): self.keyword = pos_or_kw + kw_only -def _signature_info(arg_spec): - # type: (List[Parameter]) -> _ParamTypes +def _signature_info(arg_spec: Iterable[Parameter]) -> _ParamTypes: param_types = collections.defaultdict(list) for param in arg_spec: param_types[param.kind].append(param) @@ -277,7 +283,7 @@ def _signature_info(arg_spec): ) -def _required_params(param_list): +def _required_params(param_list: List[Parameter]) -> List[Parameter]: """ return params without a default""" # params with defaults come last for i, p in enumerate(param_list): @@ -346,8 +352,7 @@ def _keyword_args_match(func_list, base_list, varkw, num_pos): return True -def _signatures_are_consistent(func_sig, base_sig): - # type: (Signature, Signature) -> bool +def _signatures_are_consistent(func_sig: Signature, base_sig: Signature) -> bool: """ :param func_sig: Signature of overriding function :param base_sig: Signature of base class function @@ -498,11 +503,10 @@ def _class_structural_type_check(cls, subclass): return True -def _get_adapter(cls, obj_type): - # type: (Type[PI], Type[Any]) -> Optional[Callable] +def _get_adapter(cls: AnInterfaceType, obj_type: Type) -> Optional[Callable]: """ Returns a callable that adapts objects of type obj_type to this interface or None if no adapter exists. """ - adapters = {} + adapters = {} # type: ignore candidate_interfaces = [cls] + cls.__subclasses__() candidate_interfaces.reverse() # prefer this class over sub-class adapters for subcls in candidate_interfaces: @@ -704,10 +708,10 @@ def optional_adapt(cls, obj, allow_implicit=False, interface_only=None): class Interface(abc.ABC, metaclass=InterfaceType): # These methods don't need to be here, as they would resolve to the meta-class methods anyway. # However including them here means we can add type hints that would otherwise be ambiguous on the meta-class. + _pi: _PIAttributes @classmethod - def provided_by(cls, obj, allow_implicit=True): - # type: (Any, bool) -> bool + def provided_by(cls, obj, allow_implicit: bool = True) -> bool: """ Returns True if obj provides this interface. provided_by(cls, obj) is equivalent to isinstance(obj, cls) unless allow_implicit is True If allow_implicit is True then returns True if interface duck-type check passes. @@ -716,14 +720,13 @@ def provided_by(cls, obj, allow_implicit=True): return InterfaceType.provided_by(cls, obj, allow_implicit=allow_implicit) @classmethod - def interface_only(cls, implementation): - # type: (Type[PI], PI) -> PI + def interface_only(cls: Type[AnInterface], implementation: AnInterface) -> AnInterface: """ Returns a wrapper around implementation that provides ONLY this interface. """ return InterfaceType.interface_only(cls, implementation) @classmethod - def adapt(cls, obj, allow_implicit=False, interface_only=None): - # type: (Type[PI], Any, bool, Optional[bool]) -> PI + def adapt(cls: Type[AnInterface], obj: Any, + allow_implicit: bool = False, interface_only: Optional[bool] = None) -> AnInterface: """ Adapts obj to interface, returning obj if to_interface.provided_by(obj, allow_implicit) is True and raising ValueError if no adapter is found If interface_only is True, or interface_only is None and is_development is True then the @@ -732,20 +735,19 @@ def adapt(cls, obj, allow_implicit=False, interface_only=None): return InterfaceType.adapt(cls, obj, allow_implicit=allow_implicit, interface_only=interface_only) @classmethod - def adapt_or_none(cls, obj, allow_implicit=False, interface_only=None): - # type: (Type[PI], Any, bool, Optional[bool]) -> Optional[PI] + def adapt_or_none(cls: Type[AnInterface], obj, + allow_implicit: bool = False, interface_only: Optional[bool] = None) -> Optional[AnInterface]: """ Adapt obj to to_interface or return None if adaption fails """ return InterfaceType.adapt_or_none(cls, obj, allow_implicit=allow_implicit, interface_only=interface_only) @classmethod - def can_adapt(cls, obj, allow_implicit=False): - # type: (Any, bool) -> bool + def can_adapt(cls, obj, allow_implicit: bool = False) -> bool: """ Returns True if adapt(obj, allow_implicit) will succeed.""" return InterfaceType.can_adapt(cls, obj, allow_implicit=allow_implicit) @classmethod - def filter_adapt(cls, objects, allow_implicit=False, interface_only=None): - # type: (Type[PI], Iterable[Any], bool, Optional[bool]) -> Iterable[PI] + def filter_adapt(cls: Type[AnInterface], objects: Iterable, + allow_implicit: bool = False, interface_only: Optional[bool] = None) -> Iterable[AnInterface]: """ Generates adaptions of the given objects to this interface. Objects that cannot be adapted to this interface are silently skipped. """ @@ -753,14 +755,13 @@ def filter_adapt(cls, objects, allow_implicit=False, interface_only=None): interface_only=interface_only) @classmethod - def optional_adapt(cls, obj, allow_implicit=False, interface_only=None): - # type: (Type[PI], Any, bool, Optional[bool]) -> Optional[PI] + def optional_adapt(cls: Type[AnInterface], obj, + allow_implicit: bool = False, interface_only: Optional[bool] = None) -> Optional[AnInterface]: """ Adapt obj to to_interface or return None if adaption fails """ return InterfaceType.optional_adapt(cls, obj, allow_implicit=allow_implicit, interface_only=interface_only) -def type_is_interface(cls): - # type: (Type[Any]) -> bool +def type_is_interface(cls: Type) -> bool: # -> TypeGuard[AnInterfaceType] """ Return True if cls is a pure interface""" try: if not issubclass(cls, Interface): @@ -770,23 +771,22 @@ def type_is_interface(cls): return get_pi_attribute(cls, 'type_is_interface', False) -def type_is_pure_interface(cls): +def type_is_pure_interface(cls: Type): warnings.warn('type_is_pure_interface has been renamed to type_is_interface.') return type_is_pure_interface(cls) -def get_type_interfaces(cls): - # type: (Type[Any]) -> List[Type[Interface]] +def get_type_interfaces(cls: Type) -> List[AnInterfaceType]: """ Returns all interfaces in the cls mro including cls itself if it is an interface """ try: bases = cls.mro() except AttributeError: # handle non-classes return [] - return [base for base in bases if type_is_interface(base) and base is not Interface] + # type_is_interface ensures returned types are Interface subclasses by mypy doesn't know this + return [base for base in bases if type_is_interface(base) and base is not Interface] # type: ignore [misc] -def get_interface_names(interface): - # type: (Type[Interface]) -> FrozenSet[str] +def get_interface_names(interface: Type) -> FrozenSet[str]: """ returns a frozen set of names (methods and attributes) defined by the interface. if interface is not a Interface subtype then an empty set is returned. """ @@ -796,8 +796,7 @@ def get_interface_names(interface): return frozenset() -def get_interface_method_names(interface): - # type: (Type[Interface]) -> FrozenSet[str] +def get_interface_method_names(interface: Type) -> FrozenSet[str]: """ returns a frozen set of names of methods defined by the interface. if interface is not a Interface subtype then an empty set is returned """ @@ -807,8 +806,7 @@ def get_interface_method_names(interface): return frozenset() -def get_interface_attribute_names(interface): - # type: (Type[Interface]) -> FrozenSet[str] +def get_interface_attribute_names(interface: Type) -> FrozenSet[str]: """ returns a frozen set of names of attributes defined by the interface if interface is not a Interface subtype then an empty set is returned """ diff --git a/pure_interface/py.typed b/pure_interface/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/requirements_dev.txt b/requirements_dev.txt index bc04b49..3d64b3b 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1 +1,2 @@ -r requirements.txt +mypy diff --git a/setup.cfg b/setup.cfg index d3d447f..a2c4d26 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,3 +29,6 @@ packages = find: [options.packages.find] where = . include = pure_interface + +[options.package_data] +pure_interface = py.typed diff --git a/tests/test_func_sigs3.py b/tests/test_func_sigs3.py index 3206b2d..131903e 100644 --- a/tests/test_func_sigs3.py +++ b/tests/test_func_sigs3.py @@ -92,8 +92,7 @@ def _test_call(spec_func, spec_sig, impl_func, impl_sig, args, kwargs): return True -def test_call(spec_func, impl_func): - # type: (types.FunctionType, types.FunctionType) -> bool +def test_call(spec_func: types.FunctionType, impl_func: types.FunctionType) -> bool: """ call the function with parameters as indicated by the parameter list """ spec_sig = pure_interface.interface.signature(spec_func) diff --git a/tests/test_function_sigs.py b/tests/test_function_sigs.py index 87eb04b..a9f26a4 100644 --- a/tests/test_function_sigs.py +++ b/tests/test_function_sigs.py @@ -83,8 +83,7 @@ def _test_call(spec_func, impl_func, impl_sig, args, kwargs): return True -def test_call(spec_func, impl_func): - # type: (types.FunctionType, types.FunctionType) -> bool +def test_call(spec_func: types.FunctionType, impl_func: types.FunctionType) -> bool: """ call the function with parameters as indicated by the parameter list """ spec_sig = pure_interface.interface.signature(spec_func) diff --git a/tox.ini b/tox.ini index 32b2697..95e9cdf 100644 --- a/tox.ini +++ b/tox.ini @@ -4,8 +4,14 @@ # and then run "tox" from this directory. [tox] -envlist = py37, py38, py39, py310, py311 +envlist = py37, py38, py39, py310, py311, mypy [testenv] commands = python -m unittest discover -p "test*" + +[testenv:mypy] +basepython = python3.10 +deps = + mypy +commands = mypy pure_interface From e0f0bf34afccdfc84cf536e0768bde0849c8e20c Mon Sep 17 00:00:00 2001 From: "Tim.Mitchell" Date: Thu, 11 May 2023 12:42:17 +1200 Subject: [PATCH 2/2] 91: bump version --- pure_interface/__init__.py | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pure_interface/__init__.py b/pure_interface/__init__.py index a1fe039..c90547d 100644 --- a/pure_interface/__init__.py +++ b/pure_interface/__init__.py @@ -11,4 +11,4 @@ except ImportError: pass -__version__ = '7.1.0' +__version__ = '7.2.0' diff --git a/setup.cfg b/setup.cfg index a2c4d26..9b21208 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pure_interface -version = 7.1.0 +version = 7.2.0 description = A Python interface library that disallows function body content on interfaces and supports adaption. keywords = abc interface adapt adaption mapper structural typing dataclass author = Tim Mitchell