Skip to content

Commit

Permalink
Merge pull request #86 from seequent/#85-python-3-10-support
Browse files Browse the repository at this point in the history
#85: Remove pycontracts support.
  • Loading branch information
tim-mitchell authored Aug 17, 2022
2 parents 46a2de2 + 0c16cb3 commit be346fb
Show file tree
Hide file tree
Showing 8 changed files with 62 additions and 140 deletions.
20 changes: 0 additions & 20 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -473,26 +473,6 @@ If ``is_development`` if ``False`` then:
* The default value of ``interface_only`` is set to ``False``, so that interface wrappers are not created.


PyContracts Integration
=======================

You can use ``pure_interface`` with PyContracts_

.. _PyContracts: https://pypi.python.org/pypi/PyContracts

Simply import the ``pure_contracts`` module and use the ``ContractInterface`` class defined there as you
would the ``Interface`` class described above.
For example::

from pure_contracts import ContractInterface
from contracts import contract

class ISpeaker(ContractInterface):
@contract(volume=int, returns=unicode)
def speak(self, volume):
pass


Reference
=========
Classes
Expand Down
27 changes: 0 additions & 27 deletions pure_contracts.py

This file was deleted.

75 changes: 46 additions & 29 deletions pure_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@
import warnings
import weakref


__version__ = '5.0.1'

__version__ = '6.0.0'

is_development = not hasattr(sys, 'frozen')
missing_method_warnings = []
Expand Down Expand Up @@ -49,6 +47,7 @@ def no_adaption(obj):

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
# abstractproperties are checked for at instantiation.
Expand Down Expand Up @@ -170,28 +169,33 @@ def _is_empty_function(func, unwrap=False):
return True

# quick check
if code_obj.co_code == b'd\x00\x00S' and code_obj.co_consts[0] is None:
byte_code = code_obj.co_code
if byte_code.startswith(b'\x97\x00'):
byte_code = byte_code[2:] # 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 code_obj.co_code == b'd\x01\x00S' and code_obj.co_consts[1] is None:
if byte_code in (b'd\x01\x00S', b'd\x01S\x00') and code_obj.co_consts[1] is None:
return True
# convert bytes to instructions
instructions = _get_instructions(code_obj)
if len(instructions) < 2:
return True # this never happens as there is always the implicit return None which is 2 instructions
assert instructions[-1].opname == 'RETURN_VALUE' # returns TOS (top of stack)
instruction = instructions[-2]
if not (instruction.opname == 'LOAD_CONST' and code_obj.co_consts[instruction.arg] is None): # TOS is None
return False # return is not None
instructions = instructions[:-2]
return True # this never happens
if instructions[0].opname == 'RESUME':
instructions.pop(0)
if instructions[-1].opname == 'RETURN_VALUE': # returns TOS (top of stack)
instruction = instructions[-2]
if not (instruction.opname == 'LOAD_CONST' and code_obj.co_consts[instruction.arg] is None): # TOS is None
return False # return is not None
instructions = instructions[:-2]
if len(instructions) == 0:
return True
# look for raise NotImplementedError
if instructions[-1].opname == 'RAISE_VARARGS':
# the thing we are raising should be the result of __call__ (instantiating exception object)
if instructions[-2].opname == 'CALL_FUNCTION':
for instr in instructions[:-2]:
if instr.opname == 'LOAD_GLOBAL' and code_obj.co_names[instr.arg] == 'NotImplementedError':
return True
if instructions[-2].opname in ('CALL_FUNCTION', 'CALL'):
for instr in instructions[-3::-1]:
if instr.opname == 'LOAD_GLOBAL':
return instr.argval == 'NotImplementedError'

return False

Expand Down Expand Up @@ -463,7 +467,7 @@ def _class_structural_type_check(cls, subclass):
if is_development:
stacklevel = 2
stack = inspect.stack()
while stacklevel < len(stack) and 'pure_interface' in stack[stacklevel-1][1]:
while stacklevel < len(stack) and 'pure_interface' in stack[stacklevel - 1][1]:
stacklevel += 1
warnings.warn('Class {module}.{sub_name} implements {cls_name}.\n'
'Consider inheriting {cls_name} or using {cls_name}.register({sub_name})'
Expand Down Expand Up @@ -529,7 +533,7 @@ def __new__(mcs, clsname, bases, attributes, **kwargs):
interface_method_signatures = dict()
interface_attribute_names = set()
abstract_properties = set()
for i in range(len(bases)-1, -1, -1): # start at back end
for i in range(len(bases) - 1, -1, -1): # start at back end
base, base_is_interface = base_types[i]
if base is object:
continue
Expand Down Expand Up @@ -569,7 +573,8 @@ def __new__(mcs, clsname, bases, attributes, **kwargs):
continue
if not _is_empty_function(func, unwrap):
raise InterfaceError('Function "{}" is not empty.\n'
'Did you forget to inherit from object to make the class concrete?'.format(func.__name__))
'Did you forget to inherit from object to make the class concrete?'.format(
func.__name__))
else: # concrete sub-type
namespace = attributes
class_properties = set()
Expand Down Expand Up @@ -808,6 +813,7 @@ class AdapterTracker(object):
want the overhead of lots of copies. This class provides adapt() and adapt_or_none() methods that track adaptions.
Thus if `x is b` is `True` then `adapter.adapt(x, I) is adapter.adapt(b, I)` is `True`.
"""

def __init__(self, mapping_factory=dict):
self._factory = mapping_factory
self._adapters = mapping_factory()
Expand Down Expand Up @@ -951,6 +957,7 @@ def my_func(foo: IFoo, bar: Optional[IBar] = None):
pass
"""

def decorator(func):
@functools.wraps(func)
def wrapped(*args, **kwargs):
Expand All @@ -960,6 +967,7 @@ def wrapped(*args, **kwargs):
adapted_kwargs[name] = InterfaceType.optional_adapt(interface, kwarg)

return func(**adapted_kwargs)

return wrapped

if func_arg:
Expand Down Expand Up @@ -996,6 +1004,11 @@ def wrapped(*args, **kwargs):
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)


def _get_interface_annotions(cls):
annos = collections.OrderedDict()
for subcls in get_type_interfaces(cls)[::-1]:
Expand All @@ -1006,19 +1019,22 @@ def _get_interface_annotions(cls):
annos[key] = sc_annos[key]
return annos

def dataclass(_cls=None, init=True, repr=True, eq=True, order=False,
unsafe_hash=False, frozen=False):
"""Returns the same class as was passed in, with dunder methods
added based on the fields defined in the class.

Examines PEP 526 __annotations__ to determine fields.
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 init is true, an __init__() method is added to the class. If
repr is true, a __repr__() method is added. If order is true, rich
comparison dunder methods are added. If unsafe_hash is true, a
__hash__() method function is added. If frozen is true, fields may
not be assigned to after instance creation.
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
Expand All @@ -1027,7 +1043,7 @@ def wrap(cls):
annos = cls.__dict__.get('__annotations__', {})
interface_annos.update(annos)
cls.__annotations__ = interface_annos
return dataclasses._process_class(cls, init, repr, eq, order, unsafe_hash, frozen)
return dataclasses._process_class(cls, *arg_tuple)

# See if we're being called as @dataclass or @dataclass().
if _cls is None:
Expand All @@ -1036,5 +1052,6 @@ def wrap(cls):
# We're called as @dataclass without parens.
return wrap(_cls)


except ImportError:
pass
2 changes: 0 additions & 2 deletions requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
-r requirements.txt
dataclasses; python_version < '3.7'
pycontracts
11 changes: 4 additions & 7 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = pure_interface
version = 5.0.1
version = 6.0.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
Expand All @@ -9,21 +9,18 @@ url = https://github.com/seequent/pure_interface
download_url = https://pypi.org/project/pure-interface/
long_description = file: README.rst
long_description_content_type = text/markdown
python_requires = >=3.6
python_requires = >=3.7
classifiers =
Development Status :: 5 - Production/Stable
Intended Audience :: Developers
Topic :: Software Development :: Libraries
License :: OSI Approved :: MIT License
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
license = MIT
license_file = LICENSE

[options]
py_modules = pure_interface, pure_contracts

[options.extras_require]
contracts = PyContracts>=1.7
py_modules = pure_interface
14 changes: 11 additions & 3 deletions tests/test_no_content_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,28 +44,36 @@ def sleep(self, duration):
msg = 'msg'.format(self.__class__.__name__)
raise NotImplementedError(msg)

def test_raise_other_fails(self):
with self.assertRaises(pure_interface.InterfaceError):
class INotAnimal(pure_interface.Interface):
def bad_method(self):
"""a comment"""
msg = NotImplementedError('msg'.format(self.__class__.__name__))
raise RuntimeError()

def test_function_with_body_fails(self):
with self.assertRaises(pure_interface.InterfaceError):
class IAnimal(pure_interface.Interface):
def speak(self, volume):
if volume > 0:
print('hello' + '!'*int(volume))
print('hello' + '!' * int(volume))

def test_abstract_function_with_body_fails(self):
with self.assertRaises(pure_interface.InterfaceError):
class IAnimal(pure_interface.Interface):
@pure_interface.abstractmethod
def speak(self, volume):
if volume > 0:
print('hello' + '!'*int(volume))
print('hello' + '!' * int(volume))

def test_abstract_classmethod_with_body_fails(self):
with self.assertRaises(pure_interface.InterfaceError):
class IAnimal(pure_interface.Interface):
@pure_interface.abstractclassmethod
def speak(cls, volume):
if volume > 0:
print('hello' + '!'*int(volume))
print('hello' + '!' * int(volume))

def test_property_with_body_fails(self):
with self.assertRaises(pure_interface.InterfaceError):
Expand Down
48 changes: 0 additions & 48 deletions tests/test_pure_contracts.py

This file was deleted.

5 changes: 1 addition & 4 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,8 @@
# and then run "tox" from this directory.

[tox]
envlist = py36, py37, py38, py39
envlist = py37, py38, py39, py310

[testenv]
deps =
pycontracts
dataclasses; python_version < '3.7'
commands =
python -m unittest discover -p "test*"

0 comments on commit be346fb

Please sign in to comment.