diff --git a/README.rst b/README.rst index 8e219ec..6cf7ee5 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -pure-interface +pure_interface ============== A Python interface library that disallows function body content on interfaces and supports adaption. @@ -15,7 +15,7 @@ Features * ``Interface.adapt()`` can return an implementation wrapper that provides *only* the attributes and methods defined by ``Interface``. * Warns if ``provided_by`` did a structural type check when inheritance would work. -* Supports python 2.7 and 3.5+ +* Supports python 3.7+ A note on the name ------------------ @@ -49,7 +49,7 @@ leaving all method bodies empty:: Like Protocols, class annotations are considered part of the interface. -In Python versions earlier than 3.6 you can use the following alternate syntax:: +In for historical reasons, you can also use the following alternate syntax:: class IAnimal(Interface): height = None @@ -330,8 +330,6 @@ Structural Type Checking Structural_ type checking checks if an object has the attributes and methods defined by the interface. -.. _Structural: https://en.wikipedia.org/wiki/Structural_type_system - As interfaces are inherited, you can usually use ``isinstance(obj, MyInterface)`` to check if an interface is provided. An alternative to ``isinstance()`` is the ``Interface.provided_by(obj)`` classmethod which will fall back to structural type checking if the instance is not an actual subclass. This can be controlled by the ``allow_implicit`` parameter which defaults to ``True``. @@ -380,8 +378,7 @@ class, interface pair. For example:: Dataclass Support ================= -dataclasses_ were added in Python 3.7. When used in this and later versions of Python, ``pure_interface`` provides a -``dataclass`` decorator. This decorator can be used to create a dataclass that implements an interface. For example:: +``pure_interface`` provides a ``dataclass`` decorator. This decorator can be used to create a dataclass that implements an interface. For example:: class IAnimal2(Interface): height: float @@ -391,7 +388,7 @@ dataclasses_ were added in Python 3.7. When used in this and later versions of pass @dataclass - class Animal(Concrete, IAnimal2): + class Animal(IAnimal2, object): def speak(self): print('Hello, I am a {} metre tall {}', self.height, self.species) @@ -467,10 +464,10 @@ are imported or else the change will not have any effect. If ``is_development`` if ``False`` then: - * Signatures of overriding methods are not checked - * No warnings are issued by the adaption functions - * No incomplete implementation warnings are issued - * The default value of ``interface_only`` is set to ``False``, so that interface wrappers are not created. +* Signatures of overriding methods are not checked +* No warnings are issued by the adaption functions +* No incomplete implementation warnings are issued +* The default value of ``interface_only`` is set to ``False``, so that interface wrappers are not created. Reference @@ -582,12 +579,10 @@ Functions Returns a ``frozenset`` of names of class attributes and annotations defined by the interface If *cls* is not a ``Interface`` subtype then an empty set is returned. -**dataclass** *(_cls=None, init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)* +**dataclass** *(...)* This function is a re-implementation of the standard Python ``dataclasses.dataclass`` decorator. In addition to the fields on the decorated class, all annotations on interface base classes are added as fields. - See the Python dataclasses_ documentation for more details. - - 3.7+ Only + See the Python dataclasses_ documentation for details on the arguments, they are exactly the same. Exceptions @@ -628,5 +623,7 @@ Module Attributes .. _cx_Freeze: https://pypi.python.org/pypi/cx_Freeze .. _dataclasses: https://docs.python.org/3/library/dataclasses.html .. _mock_protocol: https://pypi.org/project/mock-protocol/ +.. _Structural: https://en.wikipedia.org/wiki/Structural_type_system + .. [*] We don't talk about the methods on the base ``Interface`` class. In earlier versions they were all on the meta class but then practicality got in the way. diff --git a/pure_interface.py b/pure_interface.py index f3144a2..6aa173e 100644 --- a/pure_interface.py +++ b/pure_interface.py @@ -6,6 +6,7 @@ import abc from abc import abstractmethod, abstractclassmethod, abstractstaticmethod import collections +import dataclasses import dis import functools import inspect @@ -17,7 +18,7 @@ import warnings import weakref -__version__ = '6.0.0' +__version__ = '6.0.1' is_development = not hasattr(sys, 'frozen') missing_method_warnings = [] @@ -170,8 +171,14 @@ def _is_empty_function(func, unwrap=False): # quick check byte_code = code_obj.co_code + if byte_code.startswith(b'\x81\x01'): + byte_code = byte_code[2:] # remove GEN_START async def opcode + if byte_code.startswith(b'K\x00'): + byte_code = byte_code[2:] # remove RETURN_GENERATOR async def opcode in py311 + if byte_code.startswith(b'\x01'): + byte_code = byte_code[2:] # remove POP_TOP if byte_code.startswith(b'\x97\x00'): - byte_code = byte_code[2:] # RESUME opcode added in 3.11 + byte_code = byte_code[2:] # remove RESUME opcode added in 3.11 if byte_code in (b'd\x00\x00S', b'd\x00S\x00') and code_obj.co_consts[0] is None: return True if byte_code in (b'd\x01\x00S', b'd\x01S\x00') and code_obj.co_consts[1] is None: @@ -180,6 +187,12 @@ def _is_empty_function(func, unwrap=False): instructions = _get_instructions(code_obj) if len(instructions) < 2: return True # this never happens + if instructions[0].opname == 'GEN_START': + instructions.pop(0) + if instructions[0].opname == 'RETURN_GENERATOR': + instructions.pop(0) + if instructions[0].opname == 'POP_TOP': + instructions.pop(0) if instructions[0].opname == 'RESUME': instructions.pop(0) if instructions[-1].opname == 'RETURN_VALUE': # returns TOS (top of stack) @@ -1001,57 +1014,50 @@ def wrapped(*args, **kwargs): return decorator -try: - import dataclasses +_dataclass_defaults = dict(init=True, repr=True, eq=True, order=False, + unsafe_hash=False, frozen=False, match_args=True, kw_only=False, + slots=False, weakref_slot=False) - _dataclass_defaults = dict(init=True, repr=True, eq=True, order=False, - unsafe_hash=False, frozen=False, match_args=True, kw_only=False, - slots=False, weakref_slot=False) +def _get_interface_annotions(cls): + annos = collections.OrderedDict() + for subcls in get_type_interfaces(cls)[::-1]: + sc_annos = typing.get_type_hints(subcls) + sc_names = get_interface_attribute_names(subcls) + for key, value in sc_annos.items(): # sc_annos has the correct ordering + if key in sc_names: + annos[key] = sc_annos[key] + return annos - def _get_interface_annotions(cls): - annos = collections.OrderedDict() - for subcls in get_type_interfaces(cls)[::-1]: - sc_annos = typing.get_type_hints(subcls) - sc_names = get_interface_attribute_names(subcls) - for key, value in sc_annos.items(): # sc_annos has the correct ordering - if key in sc_names: - annos[key] = sc_annos[key] - return annos - - if sys.version_info[:2] < (3, 10): - _dataclass_args = ('init', 'repr', 'eq', 'order', 'unsafe_hash', 'frozen') - elif sys.version_info[:2] < (3, 11): - _dataclass_args = ('init', 'repr', 'eq', 'order', 'unsafe_hash', 'frozen', - 'match_args', 'kw_only', 'slots') - else: - _dataclass_args = ('init', 'repr', 'eq', 'order', 'unsafe_hash', 'frozen', - 'match_args', 'kw_only', 'slots', 'weakref_slot') +if sys.version_info[:2] < (3, 10): + _dataclass_args = ('init', 'repr', 'eq', 'order', 'unsafe_hash', 'frozen') +elif sys.version_info[:2] < (3, 11): + _dataclass_args = ('init', 'repr', 'eq', 'order', 'unsafe_hash', 'frozen', + 'match_args', 'kw_only', 'slots') +else: + _dataclass_args = ('init', 'repr', 'eq', 'order', 'unsafe_hash', 'frozen', + 'match_args', 'kw_only', 'slots', 'weakref_slot') - def dataclass(_cls=None, **kwargs): - """Returns the same class as was passed in, with dunder methods - added based on the fields defined in the class. - """ - arg_tuple = tuple(kwargs.get(arg_name, _dataclass_defaults[arg_name]) for arg_name in _dataclass_args) - - def wrap(cls): - # dataclasses only operates on annotations in the current class - # get all interface attributes and add them to this class - interface_annos = _get_interface_annotions(cls) - annos = cls.__dict__.get('__annotations__', {}) - interface_annos.update(annos) - cls.__annotations__ = interface_annos - return dataclasses._process_class(cls, *arg_tuple) - - # See if we're being called as @dataclass or @dataclass(). - if _cls is None: - # We're called with parens. - return wrap - # We're called as @dataclass without parens. - return wrap(_cls) - - -except ImportError: - pass +def dataclass(_cls=None, **kwargs): + """Returns the same class as was passed in, with dunder methods + added based on the fields defined in the class. + """ + arg_tuple = tuple(kwargs.get(arg_name, _dataclass_defaults[arg_name]) for arg_name in _dataclass_args) + + def wrap(cls): + # dataclasses only operates on annotations in the current class + # get all interface attributes and add them to this class + interface_annos = _get_interface_annotions(cls) + annos = cls.__dict__.get('__annotations__', {}) + interface_annos.update(annos) + cls.__annotations__ = interface_annos + return dataclasses._process_class(cls, *arg_tuple) + + # See if we're being called as @dataclass or @dataclass(). + if _cls is None: + # We're called with parens. + return wrap + # We're called as @dataclass without parens. + return wrap(_cls) diff --git a/setup.cfg b/setup.cfg index 2d54f1f..7149fc9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pure_interface -version = 6.0.0 +version = 6.0.1 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 diff --git a/tests/test_no_content_checks.py b/tests/test_no_content_checks.py index 1273a48..04362f0 100644 --- a/tests/test_no_content_checks.py +++ b/tests/test_no_content_checks.py @@ -44,6 +44,19 @@ def sleep(self, duration): msg = 'msg'.format(self.__class__.__name__) raise NotImplementedError(msg) + def test_async_function_passes(self): + class IAnimal(pure_interface.Interface): + async def speak(self, volume): + pass + + async def move(self, to): + """ a comment """ + raise NotImplementedError('subclass must provide') + + async def sleep(self, duration): + "a comment" + pass + def test_raise_other_fails(self): with self.assertRaises(pure_interface.InterfaceError): class INotAnimal(pure_interface.Interface):