Skip to content

Commit

Permalink
#87: recognize empty async methods.
Browse files Browse the repository at this point in the history
Tidy up docs.
  • Loading branch information
tim-mitchell committed Aug 22, 2022
1 parent be346fb commit 0a0a389
Show file tree
Hide file tree
Showing 4 changed files with 83 additions and 67 deletions.
29 changes: 13 additions & 16 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pure-interface
pure_interface
==============

A Python interface library that disallows function body content on interfaces and supports adaption.
Expand All @@ -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
------------------
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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``.
Expand Down Expand Up @@ -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
Expand All @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
106 changes: 56 additions & 50 deletions pure_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import abc
from abc import abstractmethod, abstractclassmethod, abstractstaticmethod
import collections
import dataclasses
import dis
import functools
import inspect
Expand All @@ -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 = []
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down Expand Up @@ -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)
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -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
Expand Down
13 changes: 13 additions & 0 deletions tests/test_no_content_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down

0 comments on commit 0a0a389

Please sign in to comment.