From 0c16cb30626eb5312fea1d9b152203a00ce3c23f Mon Sep 17 00:00:00 2001 From: "Tim.Mitchell" Date: Wed, 17 Aug 2022 13:44:12 +1200 Subject: [PATCH] #85: Remove pycontracts support. Make tests pass on python 3.10 and 3.11rc1 --- README.rst | 20 --------- pure_contracts.py | 27 ------------ pure_interface.py | 75 ++++++++++++++++++++------------- requirements_dev.txt | 2 - setup.cfg | 11 ++--- tests/test_no_content_checks.py | 14 ++++-- tests/test_pure_contracts.py | 48 --------------------- tox.ini | 5 +-- 8 files changed, 62 insertions(+), 140 deletions(-) delete mode 100644 pure_contracts.py delete mode 100644 tests/test_pure_contracts.py diff --git a/README.rst b/README.rst index a55dfad..8e219ec 100644 --- a/README.rst +++ b/README.rst @@ -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 diff --git a/pure_contracts.py b/pure_contracts.py deleted file mode 100644 index 8e360ec..0000000 --- a/pure_contracts.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, division, print_function, unicode_literals - -import warnings - -import pure_interface -from pure_interface import InterfaceError # alias for convenience - -try: - import contracts # https://pypi.python.org/pypi/PyContracts - - class ContractType(pure_interface.InterfaceType, contracts.ContractsMeta): - # we need to unwrap the decorators because otherwise we fail the empty function body test - # inspecting the wrapper. - _pi_unwrap_decorators = True - pass - -except ImportError: - warnings.warn('PyContracts not found') - - class ContractType(pure_interface.InterfaceType): - _pi_unwrap_decorators = True - pass - - -class ContractInterface(pure_interface.Interface, metaclass=ContractType): - pass diff --git a/pure_interface.py b/pure_interface.py index 24dcbf7..f3144a2 100644 --- a/pure_interface.py +++ b/pure_interface.py @@ -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 = [] @@ -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. @@ -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 @@ -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})' @@ -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 @@ -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() @@ -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() @@ -951,6 +957,7 @@ def my_func(foo: IFoo, bar: Optional[IBar] = None): pass """ + def decorator(func): @functools.wraps(func) def wrapped(*args, **kwargs): @@ -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: @@ -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]: @@ -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 @@ -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: @@ -1036,5 +1052,6 @@ def wrap(cls): # We're called as @dataclass without parens. return wrap(_cls) + except ImportError: pass diff --git a/requirements_dev.txt b/requirements_dev.txt index 4eefc2f..bc04b49 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,3 +1 @@ -r requirements.txt -dataclasses; python_version < '3.7' -pycontracts diff --git a/setup.cfg b/setup.cfg index be93c0c..2d54f1f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -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 @@ -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 diff --git a/tests/test_no_content_checks.py b/tests/test_no_content_checks.py index a22b4e2..1273a48 100644 --- a/tests/test_no_content_checks.py +++ b/tests/test_no_content_checks.py @@ -44,12 +44,20 @@ 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): @@ -57,7 +65,7 @@ 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): @@ -65,7 +73,7 @@ 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): diff --git a/tests/test_pure_contracts.py b/tests/test_pure_contracts.py deleted file mode 100644 index c755986..0000000 --- a/tests/test_pure_contracts.py +++ /dev/null @@ -1,48 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import absolute_import, division, print_function, unicode_literals - -import unittest - -try: - import contracts - have_contracts = True -except ImportError: - have_contracts = False - -import pure_contracts - -if have_contracts: - class IPlant(pure_contracts.ContractInterface): - @contracts.contract(height=float, returns=float) - def set_height(self, height): - return None - - - class Plant(IPlant, object): - def set_height(self, height): - return height - - - class TestPureContracts(unittest.TestCase): - def test_base_class_is_interface(self): - with self.assertRaises(TypeError): - IPlant() - - def test_contracts_honoured(self): - p = Plant() - with self.assertRaises(contracts.ContractNotRespected): - p.set_height('hello') - - self.assertEqual(5.0, p.set_height(5.0)) - - def test_content_fails(self): - with self.assertRaises(pure_contracts.InterfaceError): - class IAnimal(pure_contracts.ContractInterface): - @contracts.contract(volume=int, returns=str) - def speak(self, volume): - if volume > 0: - return 'hello' + '!'*volume -else: - class TestPureContracts(unittest.TestCase): - def test_pycontracts_exists(self): - self.fail('PyContracts not found') diff --git a/tox.ini b/tox.ini index 6ebaa44..01334f1 100644 --- a/tox.ini +++ b/tox.ini @@ -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*"