diff --git a/.gitignore b/.gitignore
index eef735d..55f2429 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,6 +9,7 @@ __pycache__/
*$py.class
build/
dist/
+docs/_build/
# Project settings
.idea/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 0c95dff..1a330ea 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -6,7 +6,6 @@ repos:
- id: end-of-file-fixer
- id: check-yaml
- id: debug-statements
- - id: flake8
- repo: local
hooks:
- id: rst
diff --git a/.project b/.project
new file mode 100644
index 0000000..60d6595
--- /dev/null
+++ b/.project
@@ -0,0 +1,17 @@
+
+
+ oop-ext
+
+
+
+
+
+ org.python.pydev.PyDevBuilder
+
+
+
+
+
+ org.python.pydev.pythonNature
+
+
diff --git a/.pydevproject b/.pydevproject
new file mode 100644
index 0000000..d001f0a
--- /dev/null
+++ b/.pydevproject
@@ -0,0 +1,5 @@
+
+
+Default
+python interpreter
+
diff --git a/.travis.yml b/.travis.yml
index 8fb146b..91158da 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,54 +1,40 @@
language: python
-
stages:
- test
- name: deploy
if: repo = ESSS/oop_ext AND tag IS present
-
install:
- pip install -U pip
- pip install -U tox
-
script:
- tox
-
jobs:
include:
- - python: '3.6'
- env: TOXENV=linting
-
- - python: '3.6'
- env: TOXENV=docs
-
- - python: '2.7'
- env: TOXENV=py27
-
- - python: '3.6'
- env: TOXENV=py36
-
- - python: '3.7'
- env: TOXENV=py37
- sudo: required
- dist: xenial
-
- - stage: deploy
- python: '3.6'
- env:
- install: pip install -U setuptools setuptools_scm
- script: skip
- deploy:
- provider: pypi
- distributions: sdist bdist_wheel
- user: lincrosenbach
- password:
- # Install travis-ci CLI tool and run the command travis encrypt to generate
- # the secure password
- secure: REPLACE
- on:
- tags: true
- repo: ESSS/oop_ext
- python: 3.6
-
+ - python: '3.6'
+ env: TOXENV=linting
+ - python: '3.6'
+ env: TOXENV=docs
+ - python: '3.6'
+ env: TOXENV=py36
+ - python: '3.7'
+ env: TOXENV=py37
+ sudo: required
+ dist: xenial
+ - stage: deploy
+ python: '3.6'
+ env:
+ install: pip install -U setuptools setuptools_scm
+ script: skip
+ deploy:
+ provider: pypi
+ distributions: sdist bdist_wheel
+ user: lincrosenbach
+ password:
+ secure: j5/b7JtmPg4xCHShWKgNLDbXTqHI2aRzrGA/ItJmR+k6GQkeiIZy12N72F6vbx/b3HSJi9vfElVM9zzWQFAfxAk86FFpzDQ1K7kFv9haLDYG/z76j6x1Kq4QD1VYHiqY9G1XRBvTEUrwBROa76/vxC5vX0Mg8cqXpj4TLRD/qw6jCzUwmSY6garFRciAaa5ppyLdxgTSQEYZN04nT0YnxvIeNaXhQSWGI44HFAJTlxk9VRmbb5BGRP/bX2UIt//pdLsYfFMNIvR0qymQBeeFOf5H8yB4uGpKKmco1Qv415Pe7vvuZgeiL8ou1R2ZkmkdGu8taZzK8WPkzZ2Fe+KWl4joqgrU/jsYuNnwK0v7PHUGZEW7zj5lZcm1+68hjY90XsItrJy/RT6xquBZvfyOZeiOkKFtB5K8VgmnxnG0V5t9eflv0Kk3lwydFV+8fGKJA7CxMT0Qo+DMabbYlRSg6xy+AfiLEzE1hj5azs9ox2JYyoPSIcquHjxh8u5D3kmqh0E7R6KWtOIHSghHJ7MY3VbPb/QgnyvHVTOI+NvMP9uXVXAke0j1bZAscKZFsYdXnG58wqjKSmA0cXEgDyFUw8pPj46EC5ecbqZTGkRlY0C7tPXCaZx0eK79byS8tZL4D/55ffYbbVD70t8Glr0k11yOdSqK2WlXI/UXUQGCGaA=
+ on:
+ tags: true
+ repo: ESSS/oop_ext
+ python: 3.6
branches:
only:
- master
diff --git a/README.rst b/README.rst
index 06c5097..c6505be 100644
--- a/README.rst
+++ b/README.rst
@@ -1,5 +1,5 @@
======================================================================
-Oop ext
+OOP Extensions
======================================================================
@@ -21,10 +21,10 @@ Oop ext
.. image:: https://img.shields.io/readthedocs/pip.svg
:target: https://oop-ext.readthedocs.io/en/latest/
-What is Oop ext ?
+What is OOP Extensions ?
================================================================================
-OOP Extensions is a set of utilities for object orienting programming which is missing on Python core libraries.
+OOP Extensions is a set of utilities for object oriented programming which is missing on Python core libraries.
Contributing
@@ -40,7 +40,7 @@ Release
-------
A reminder for the maintainers on how to make a new release.
-Note that the VERSION should folow the semantic versioning as X.Y.Z
+Note that the VERSION should follow the semantic versioning as X.Y.Z
Ex.: v1.0.5
1. Create a ``release-VERSION`` branch from ``upstream/master``.
diff --git a/appveyor.yml b/appveyor.yml
index e2e9070..0e3e17f 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -8,9 +8,6 @@ environment:
- PYTHON: "C:\\Python36"
TOXENV: "docs"
- - PYTHON: "C:\\Python27"
- TOXENV: "py27"
-
- PYTHON: "C:\\Python36"
TOXENV: "py36"
diff --git a/docs/changelog.rst b/docs/changelog.rst
index 491ea73..069c0fb 100644
--- a/docs/changelog.rst
+++ b/docs/changelog.rst
@@ -1 +1 @@
-.. include:: ../CHANGELOG
+.. include:: ../CHANGELOG.RST
diff --git a/setup.cfg b/setup.cfg
index 2bc1350..1ad91cc 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,9 +1,6 @@
[bdist_wheel]
universal = 1
-[flake8]
-exclude = docs
-
[aliases]
test = pytest
diff --git a/setup.py b/setup.py
index 365d7d8..7d9e384 100644
--- a/setup.py
+++ b/setup.py
@@ -5,7 +5,7 @@
with io.open("README.rst", encoding="UTF-8") as readme_file:
readme = readme_file.read()
-with io.open("CHANGELOG", encoding="UTF-8") as changelog_file:
+with io.open("CHANGELOG.RST", encoding="UTF-8") as changelog_file:
history = changelog_file.read()
requirements = []
@@ -20,7 +20,6 @@
'Development Status :: 2 - Pre-Alpha',
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
- "Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
],
@@ -30,7 +29,7 @@
license="MIT license",
long_description=readme + "\n\n" + history,
include_package_data=True,
- python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*",
+ python_requires=">=3.6",
keywords="oop_ext",
name="oop-ext",
packages=setuptools.find_packages(where="src"),
diff --git a/src/oop_ext/conftest.py b/src/oop_ext/conftest.py
new file mode 100644
index 0000000..5456af3
--- /dev/null
+++ b/src/oop_ext/conftest.py
@@ -0,0 +1,120 @@
+import pytest
+
+
+class _ShowHandledExceptionsError:
+ '''
+ Helper class to deal with handled exceptions.
+ '''
+
+ def __init__(self):
+ self._handled_exceptions = []
+ self._handled_exceptions_types = []
+
+ def _OnHandledException(self):
+ '''
+ Called when a handled exceptions was found.
+ '''
+ import traceback
+ from io import StringIO
+ s = StringIO()
+ traceback.print_exc(file=s)
+ self._handled_exceptions_types.append(sys.exc_info()[0])
+ self._handled_exceptions.append(s.getvalue())
+
+ def __enter__(self, *args, **kwargs):
+ from oop_ext.foundation import handle_exception
+ handle_exception.on_exception_handled.Register(self._OnHandledException)
+ return self
+
+ def __exit__(self, *args, **kwargs):
+ from oop_ext.foundation import handle_exception
+ handle_exception.on_exception_handled.Unregister(self._OnHandledException)
+
+ def ClearHandledExceptions(self):
+ '''
+ Clears the handled exceptions
+ '''
+ del self._handled_exceptions_types[:]
+ del self._handled_exceptions[:]
+
+ def GetHandledExceptionTypes(self):
+ '''
+ :return list(type):
+ Returns a list with the exception types we found.
+ '''
+ return self._handled_exceptions_types
+
+ def GetHandledExceptions(self):
+ '''
+ :return list(str):
+ Returns a list with the representation of the handled exceptions.
+ '''
+ return self._handled_exceptions
+
+ def RaiseFoundExceptions(self):
+ '''
+ Raises error for the handled exceptions found.
+ '''
+
+ def ToString(s):
+ if not isinstance(s, str):
+ s = s.decode('utf-8', 'replace')
+ return s
+
+ if self._handled_exceptions:
+ raise AssertionError('\n'.join(ToString(i) for i in self._handled_exceptions))
+
+
+@pytest.yield_fixture(scope="function", autouse=True)
+def handled_exceptions():
+ '''
+ This method will be called for all the functions automatically.
+
+ For users which expect handled exceptions, it's possible to declare the fixture and
+ say that the errors are expected and clear them later.
+
+ I.e.:
+
+ from oop_ext.foundation.handle_exception import IgnoringHandleException
+ from oop_ext.foundation import handle_exception
+
+ def testSomething(handled_exceptions):
+ with IgnoringHandleException():
+ try:
+ raise RuntimeError('test')
+ except:
+ handle_exception.HandleException()
+
+ # Check that they're there...
+ assert len(handled_exceptions.GetHandledExceptions()) == 1
+
+ # Clear them
+ handled_exceptions.ClearHandledExceptions()
+
+ Note that test-cases can still deal with this API without using a fixture by importing handled_exceptions
+ and using it as an object.
+
+ I.e.:
+
+ from oop_ext.fixtures import handled_exceptions
+ handled_exceptions.GetHandledExceptions()
+ handled_exceptions.ClearHandledExceptions()
+ '''
+ try:
+ with _ShowHandledExceptionsError() as show_handled_exceptions_error:
+ handled_exceptions.ClearHandledExceptions = \
+ show_handled_exceptions_error.ClearHandledExceptions
+
+ handled_exceptions.GetHandledExceptions = \
+ show_handled_exceptions_error.GetHandledExceptions
+
+ handled_exceptions.GetHandledExceptionTypes = \
+ show_handled_exceptions_error.GetHandledExceptionTypes
+
+ yield show_handled_exceptions_error
+ finally:
+ handled_exceptions.ClearHandledExceptions = None
+ handled_exceptions.GetHandledExceptions = None
+ handled_exceptions.GetHandledExceptionTypes = None
+
+ show_handled_exceptions_error.RaiseFoundExceptions()
diff --git a/src/oop_ext/foundation/__init__.py b/src/oop_ext/foundation/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/oop_ext/foundation/_tests/__init__.py b/src/oop_ext/foundation/_tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/oop_ext/foundation/_tests/test_cached_method.py b/src/oop_ext/foundation/_tests/test_cached_method.py
new file mode 100644
index 0000000..1ccb247
--- /dev/null
+++ b/src/oop_ext/foundation/_tests/test_cached_method.py
@@ -0,0 +1,163 @@
+
+import pytest
+
+from oop_ext.foundation.cached_method import (
+ AttributeBasedCachedMethod, CachedMethod, LastResultCachedMethod)
+
+
+def testCacheMethod(_cached_obj):
+ cache = MyMethod = CachedMethod(_cached_obj.CachedMethod)
+
+ MyMethod(1)
+ _cached_obj.CheckCounts(cache, method=1, miss=1)
+
+ MyMethod(1)
+ _cached_obj.CheckCounts(cache, hit=1)
+
+ MyMethod(2)
+ _cached_obj.CheckCounts(cache, method=1, miss=1)
+
+ MyMethod(2)
+ _cached_obj.CheckCounts(cache, hit=1)
+
+ # ALL results are stored, so these calls are HITs
+ MyMethod(1)
+ _cached_obj.CheckCounts(cache, hit=1)
+
+ MyMethod(2)
+ _cached_obj.CheckCounts(cache, hit=1)
+
+
+def testCacheMethodEnabled(_cached_obj):
+ cache = MyMethod = CachedMethod(_cached_obj.CachedMethod)
+
+ MyMethod(1)
+ _cached_obj.CheckCounts(cache, method=1, miss=1)
+
+ MyMethod(1)
+ _cached_obj.CheckCounts(cache, hit=1)
+
+ MyMethod.enabled = False
+
+ MyMethod(1)
+ _cached_obj.CheckCounts(cache, method=1, miss=1)
+
+ MyMethod.enabled = True
+
+ MyMethod(1)
+ _cached_obj.CheckCounts(cache, hit=1)
+
+
+def testCacheMethodLastResultCachedMethod(_cached_obj):
+ cache = MyMethod = LastResultCachedMethod(_cached_obj.CachedMethod)
+
+ MyMethod(1)
+ _cached_obj.CheckCounts(cache, method=1, miss=1)
+
+ MyMethod(1)
+ _cached_obj.CheckCounts(cache, hit=1)
+
+ MyMethod(2)
+ _cached_obj.CheckCounts(cache, method=1, miss=1)
+
+ MyMethod(2)
+ _cached_obj.CheckCounts(cache, hit=1)
+
+ # Only the LAST result is stored, so this call is a MISS.
+ MyMethod(1)
+ _cached_obj.CheckCounts(cache, method=1, miss=1)
+
+
+def testCacheMethodObjectInKey(_cached_obj):
+ cache = MyMethod = CachedMethod(_cached_obj.CachedMethod)
+
+ class MyObject:
+
+ def __init__(self):
+ self.name = 'alpha'
+ self.id = 1
+
+ def __str__(self):
+ return '%s %d' % (self.name, self.id)
+
+ alpha = MyObject()
+
+ MyMethod(alpha)
+ _cached_obj.CheckCounts(cache, method=1, miss=1)
+
+ MyMethod(alpha)
+ _cached_obj.CheckCounts(cache, hit=1)
+
+ alpha.name = 'bravo'
+ alpha.id = 2
+
+ MyMethod(alpha)
+ _cached_obj.CheckCounts(cache, method=1, miss=1)
+
+
+def testCacheMethodAttributeBasedCachedMethod():
+
+ class TestObject:
+
+ def __init__(self):
+ self.name = 'alpha'
+ self.id = 1
+ self.n_calls = 0
+
+ def Foo(self, par):
+ self.n_calls += 1
+ return '%s %d' % (par, self.id)
+
+ alpha = TestObject()
+ alpha.Foo = AttributeBasedCachedMethod(alpha.Foo, 'id', cache_size=3)
+ alpha.Foo('test1')
+ alpha.Foo('test1')
+
+ assert alpha.n_calls == 1
+
+ alpha.Foo('test2')
+ assert alpha.n_calls == 2
+ assert len(alpha.Foo._results) == 2
+
+ alpha.id = 3
+ alpha.Foo('test2')
+ assert alpha.n_calls == 3
+
+ assert len(alpha.Foo._results) == 3
+
+ alpha.Foo('test3')
+ assert alpha.n_calls == 4
+ assert len(alpha.Foo._results) == 3
+
+
+@pytest.fixture
+def _cached_obj():
+ '''
+ A test_object common to many cached_method tests.
+ '''
+
+ class TestObj:
+
+ def __init__(self):
+ self.method_count = 0
+
+ def CachedMethod(self, *args, **kwargs):
+ self.method_count += 1
+ return self.method_count
+
+ def CheckCounts(self, cache, method=0, miss=0, hit=0):
+
+ if not hasattr(cache, 'check_counts'):
+ cache.check_counts = dict(method=0, miss=0, hit=0, call=0)
+
+ cache.check_counts['method'] += method
+ cache.check_counts['miss'] += miss
+ cache.check_counts['hit'] += hit
+ cache.check_counts['call'] += (miss + hit)
+
+ assert self.method_count == cache.check_counts['method']
+ assert cache.miss_count == cache.check_counts['miss']
+ assert cache.hit_count == cache.check_counts['hit']
+ assert cache.call_count == cache.check_counts['call']
+
+ return TestObj()
diff --git a/src/oop_ext/foundation/_tests/test_decorators.py b/src/oop_ext/foundation/_tests/test_decorators.py
new file mode 100644
index 0000000..de86ebf
--- /dev/null
+++ b/src/oop_ext/foundation/_tests/test_decorators.py
@@ -0,0 +1,152 @@
+
+import warnings
+
+import pytest
+
+from oop_ext.foundation import is_frozen
+from oop_ext.foundation.decorators import Abstract, Deprecated, Implements, Override
+
+
+def testImplements():
+ with pytest.raises(AssertionError):
+
+ class IFoo:
+
+ def DoIt(self):
+ ''
+
+ class Implementation:
+
+ @Implements(IFoo.DoIt)
+ def DoNotDoIt(self):
+ ''
+
+ class IFoo:
+
+ def Foo(self):
+ '''
+ docstring
+ '''
+
+ class Impl:
+
+ @Implements(IFoo.Foo)
+ def Foo(self):
+ return self.__class__.__name__
+
+ assert IFoo.Foo.__doc__ == Impl.Foo.__doc__
+
+ # Just for 100% coverage.
+ assert Impl().Foo() == 'Impl'
+
+
+def testOverride():
+
+ def TestOK():
+
+ class A:
+
+ def Method(self):
+ '''
+ docstring
+ '''
+
+ class B(A):
+
+ @Override(A.Method)
+ def Method(self):
+ return 2
+
+ b = B()
+ assert b.Method() == 2
+ assert A.Method.__doc__ == B.Method.__doc__
+
+ def TestERROR():
+
+ class A:
+
+ def MyMethod(self):
+ ''
+
+ class B(A):
+
+ @Override(A.Method) # it will raise an error at this point
+ def Method(self):
+ ''
+
+ def TestNoMatch():
+
+ class A:
+
+ def Method(self):
+ ''
+
+ class B(A):
+
+ @Override(A.Method)
+ def MethodNoMatch(self):
+ ''
+
+ TestOK()
+ with pytest.raises(AttributeError):
+ TestERROR()
+
+ with pytest.raises(AssertionError):
+ TestNoMatch()
+
+
+def testDeprecated(monkeypatch):
+
+ def MyWarn(*args, **kwargs):
+ warn_params.append((args, kwargs))
+
+ monkeypatch.setattr(warnings, 'warn', MyWarn)
+
+ was_development = is_frozen.SetIsDevelopment(True)
+ try:
+ # Emit messages when in development
+ warn_params = []
+
+ # ... deprecation with alternative
+ @Deprecated('OtherMethod')
+ def Method1():
+ pass
+
+ # ... deprecation without alternative
+ @Deprecated()
+ def Method2():
+ pass
+
+ Method1()
+ Method2()
+ assert warn_params == [
+ (("DEPRECATED: 'Method1' is deprecated, use 'OtherMethod' instead",), {'stacklevel': 2}),
+ (("DEPRECATED: 'Method2' is deprecated",), {'stacklevel': 2})
+ ]
+
+ # No messages on release code
+ is_frozen.SetIsDevelopment(False)
+
+ warn_params = []
+
+ @Deprecated()
+ def FrozenMethod():
+ pass
+
+ FrozenMethod()
+ assert warn_params == []
+ finally:
+ is_frozen.SetIsDevelopment(was_development)
+
+
+def testAbstract():
+
+ class Alpha:
+
+ @Abstract
+ def Method(self):
+ ''
+
+ alpha = Alpha()
+ with pytest.raises(NotImplementedError):
+ alpha.Method()
diff --git a/src/oop_ext/foundation/_tests/test_immutable.py b/src/oop_ext/foundation/_tests/test_immutable.py
new file mode 100644
index 0000000..43adb69
--- /dev/null
+++ b/src/oop_ext/foundation/_tests/test_immutable.py
@@ -0,0 +1,109 @@
+from copy import copy, deepcopy
+
+import pytest
+
+from oop_ext.foundation.immutable import AsImmutable, IdentityHashableRef, ImmutableDict
+
+
+def testImmutable():
+
+ class MyClass:
+ pass
+
+ d = AsImmutable(dict(a=1, b=dict(b=2)))
+ assert d == {'a': 1, 'b': {'b': 2}}
+ with pytest.raises(NotImplementedError):
+ d.__setitem__('a', 2)
+
+ assert d['b'].AsMutable() == dict(b=2)
+ AsImmutable(d, return_str_if_not_expected=False)
+ d = d.AsMutable()
+ d['a'] = 2
+
+ c = deepcopy(d)
+ assert c == d
+
+ c = copy(d)
+ assert c == d
+ assert AsImmutable({1, 2, 3}) == {1, 2, 3}
+ assert AsImmutable(([1, 2], [2, 3])) == ((1, 2), (2, 3))
+ assert AsImmutable(None) is None
+ assert isinstance(AsImmutable({1, 2, 4}), frozenset)
+ assert isinstance(AsImmutable(frozenset([1, 2, 4])), frozenset)
+ assert isinstance(AsImmutable([1, 2, 4]), tuple)
+ assert isinstance(AsImmutable((1, 2, 4)), tuple)
+
+ # Primitive non-container types
+ def AssertIsSame(value):
+ assert AsImmutable(value) is value
+
+ AssertIsSame(True)
+ AssertIsSame(1.)
+ AssertIsSame(1)
+ AssertIsSame('a') # native string
+ AssertIsSame(b'b') # native bytes
+ AssertIsSame(str('a')) # future's str compatibility type
+ AssertIsSame(bytes(b'b')) # future's byte compatibility type
+
+ # Dealing with derived values
+ a = MyClass()
+ assert AsImmutable(a, return_str_if_not_expected=True) == str(a)
+ with pytest.raises(RuntimeError):
+ AsImmutable(a, return_str_if_not_expected=False)
+
+ # Derived basics
+ class MyStr(str):
+ pass
+
+ assert AsImmutable(MyStr('alpha')) == 'alpha'
+
+ class MyList(list):
+ pass
+
+ assert AsImmutable(MyList()) == ()
+
+ class MySet(set):
+ pass
+
+ assert AsImmutable(MySet()) == frozenset()
+
+
+def testImmutableDict():
+ d = ImmutableDict(alpha=1, bravo=2)
+
+ with pytest.raises(NotImplementedError):
+ d['charlie'] = 3
+
+ with pytest.raises(NotImplementedError):
+ del d['alpha']
+
+ with pytest.raises(NotImplementedError):
+ d.clear()
+
+ with pytest.raises(NotImplementedError):
+ d.setdefault('charlie', 3)
+
+ with pytest.raises(NotImplementedError):
+ d.popitem()
+
+ with pytest.raises(NotImplementedError):
+ d.update({'charlie':3})
+
+
+def testIdentityHashableRef():
+ a = {1: 2}
+ b = {1: 2}
+
+ assert IdentityHashableRef(a)() is a
+ assert a == b
+ assert IdentityHashableRef(a) != IdentityHashableRef(b)
+ assert IdentityHashableRef(a) == IdentityHashableRef(a)
+
+ set_a = {IdentityHashableRef(a)}
+ assert IdentityHashableRef(a) in set_a
+ assert IdentityHashableRef(b) not in set_a
+
+ dict_b = {IdentityHashableRef(b): 7}
+ assert IdentityHashableRef(a) not in dict_b
+ assert IdentityHashableRef(b) in dict_b
+ assert dict_b[IdentityHashableRef(b)] == 7
diff --git a/src/oop_ext/foundation/_tests/test_is_frozen.py b/src/oop_ext/foundation/_tests/test_is_frozen.py
new file mode 100644
index 0000000..2657cf2
--- /dev/null
+++ b/src/oop_ext/foundation/_tests/test_is_frozen.py
@@ -0,0 +1,29 @@
+
+from oop_ext.foundation.is_frozen import IsDevelopment, IsFrozen, SetIsDevelopment, SetIsFrozen
+
+
+def testIsFrozenIsDevelopment():
+ # Note: this test is checking if we're always running tests while not in frozen mode,
+ # still, we have to do a try..finally to make sure we restore things to the proper state.
+ was_frozen = IsFrozen()
+ try:
+ assert IsFrozen() == False
+ assert IsDevelopment() == True
+
+ SetIsDevelopment(False)
+ assert IsFrozen() == False
+ assert IsDevelopment() == False
+
+ SetIsDevelopment(True)
+ assert IsFrozen() == False
+ assert IsDevelopment() == True
+
+ SetIsFrozen(True)
+ assert IsFrozen() == True
+ assert IsDevelopment() == True
+
+ SetIsFrozen(False)
+ assert IsFrozen() == False
+ assert IsDevelopment() == True
+ finally:
+ SetIsFrozen(was_frozen)
diff --git a/src/oop_ext/foundation/_tests/test_odict.py b/src/oop_ext/foundation/_tests/test_odict.py
new file mode 100644
index 0000000..b5fd684
--- /dev/null
+++ b/src/oop_ext/foundation/_tests/test_odict.py
@@ -0,0 +1,31 @@
+
+from oop_ext.foundation.odict import odict
+
+
+def testInsert():
+ d = odict()
+ d[1] = 'alpha'
+ d[3] = 'charlie'
+
+ assert list(d.items()) == [(1, 'alpha'), (3, 'charlie')]
+
+ d.insert(0, 0, 'ZERO')
+ assert list(d.items()) == [(0, 'ZERO'), (1, 'alpha'), (3, 'charlie')]
+
+ d.insert(2, 2, 'bravo')
+ assert list(d.items()) == [(0, 'ZERO'), (1, 'alpha'), (2, 'bravo'), (3, 'charlie')]
+
+ d.insert(99, 4, 'echo')
+ assert list(d.items()) == [(0, 'ZERO'), (1, 'alpha'), (2, 'bravo'), (3, 'charlie'), (4, 'echo')]
+
+
+def testDelWithSlices():
+ d = odict()
+ d[1] = 1
+ d[2] = 2
+ d[3] = 3
+
+ del d[1:]
+
+ assert len(d) == 1
+ assert d[1] == 1
diff --git a/src/oop_ext/foundation/_tests/test_singleton.py b/src/oop_ext/foundation/_tests/test_singleton.py
new file mode 100644
index 0000000..3d91103
--- /dev/null
+++ b/src/oop_ext/foundation/_tests/test_singleton.py
@@ -0,0 +1,155 @@
+
+import pytest
+
+from oop_ext.foundation.callback import After
+from oop_ext.foundation.decorators import Override
+from oop_ext.foundation.singleton import (
+ PushPopSingletonError, Singleton, SingletonAlreadySetError, SingletonNotSetError)
+
+
+def CheckCurrentSingleton(singleton_class, value):
+ singleton = singleton_class.GetSingleton()
+ assert singleton.value == value
+
+
+def testSingleton():
+
+ class MySingleton(Singleton):
+
+ def __init__(self, value):
+ self.value = value
+
+ @classmethod
+ @Override(Singleton.CreateDefaultSingleton)
+ def CreateDefaultSingleton(cls):
+ return MySingleton(value=0)
+
+ # Default singleton (created automatically and also put in the stack)
+ CheckCurrentSingleton(MySingleton, 0)
+ default_singleton = MySingleton.GetSingleton()
+ default_singleton.value = 10
+
+ # SetSingleton must be called only when there is no singleton set. In this case,
+ # GetSingleton already set the singleton.
+ with pytest.raises(SingletonAlreadySetError):
+ MySingleton.SetSingleton(MySingleton(value=999))
+ CheckCurrentSingleton(MySingleton, 10)
+
+ # push a new instance and test it
+ MySingleton.PushSingleton(MySingleton(2000))
+ CheckCurrentSingleton(MySingleton, 2000)
+
+ # Calling SetSingleton after using Push/Pop is an error: we do this so that
+ # in tests we know someone is doing a SetSingleton when they shouldn't
+ with pytest.raises(PushPopSingletonError):
+ MySingleton.SetSingleton(MySingleton(value=10))
+
+ # pop, returns to the initial
+ MySingleton.PopSingleton()
+ CheckCurrentSingleton(MySingleton, 10)
+
+ # SetSingleton given SingletonAlreadySet when outside Push/Pop
+ with pytest.raises(SingletonAlreadySetError):
+ MySingleton.SetSingleton(MySingleton(value=999))
+ CheckCurrentSingleton(MySingleton, 10)
+
+ # The singleton set with "SetSingleton" or created automatically by "GetSingleton" is not
+ # part of the stack
+ with pytest.raises(PushPopSingletonError):
+ MySingleton.PopSingleton()
+
+
+def testSetSingleton():
+
+ class MySingleton(Singleton):
+
+ def __init__(self, value=None):
+ self.value = value
+
+ assert not MySingleton.HasSingleton()
+
+ MySingleton.SetSingleton(MySingleton(value=999))
+ assert MySingleton.HasSingleton()
+ CheckCurrentSingleton(MySingleton, 999)
+
+ with pytest.raises(SingletonAlreadySetError):
+ MySingleton.SetSingleton(MySingleton(value=999))
+
+ MySingleton.ClearSingleton()
+ assert not MySingleton.HasSingleton()
+
+ with pytest.raises(SingletonNotSetError):
+ MySingleton.ClearSingleton()
+
+
+def testPushPop():
+
+ class MySingleton(Singleton):
+
+ def __init__(self, value=None):
+ self.value = value
+
+ MySingleton.PushSingleton()
+
+ assert MySingleton.GetStackCount() == 1
+
+ with pytest.raises(PushPopSingletonError):
+ MySingleton.ClearSingleton()
+
+ MySingleton.PushSingleton()
+ assert MySingleton.GetStackCount() == 2
+
+ MySingleton.PopSingleton()
+ assert MySingleton.GetStackCount() == 1
+
+ MySingleton.PopSingleton()
+ assert MySingleton.GetStackCount() == 0
+
+ with pytest.raises(PushPopSingletonError):
+ MySingleton.PopSingleton()
+
+
+def testSingletonOptimization():
+
+ class MySingleton(Singleton):
+ pass
+
+ class MockClass:
+ called = False
+
+ def ObtainStack(self, *args, **kwargs):
+ self.called = True
+
+ obj = MockClass()
+ After(MySingleton._ObtainStack, obj.ObtainStack)
+
+ obj.called = False
+ MySingleton.GetSingleton()
+ assert obj.called
+
+ obj.called = False
+ MySingleton.GetSingleton()
+ assert not obj.called
+
+
+def testGetSingletonThreadSafe(mocker):
+ from threading import Event, Thread
+
+ class MySingleton(Singleton):
+
+ @classmethod
+ def SlowConstructor(cls, event):
+ event.wait(1)
+ return MySingleton()
+
+ thrlist = [Thread(target=MySingleton.GetSingleton) for _ in range(3)]
+ create_singleton_mock = mocker.patch.object(MySingleton, "CreateDefaultSingleton")
+
+ event = Event()
+ create_singleton_mock.side_effect = lambda: MySingleton.SlowConstructor(event)
+ for thread in thrlist:
+ thread.start()
+ event.set()
+ for thread in thrlist:
+ thread.join()
+ assert create_singleton_mock.call_count == 1
diff --git a/src/oop_ext/foundation/_tests/test_types.py b/src/oop_ext/foundation/_tests/test_types.py
new file mode 100644
index 0000000..f092c47
--- /dev/null
+++ b/src/oop_ext/foundation/_tests/test_types.py
@@ -0,0 +1,60 @@
+
+import copy
+
+from oop_ext.foundation.types_ import Null
+
+
+def testNull():
+ # constructing and calling
+
+ dummy = Null()
+ dummy = Null('value')
+ n = Null('value', param='value')
+
+ n()
+ n('value')
+ n('value', param='value')
+
+ # attribute handling
+ n.attr1
+ n.attr1.attr2
+ n.method1()
+ n.method1().method2()
+ n.method('value')
+ n.method(param='value')
+ n.method('value', param='value')
+ n.attr1.method1()
+ n.method1().attr1
+
+ n.attr1 = 'value'
+ n.attr1.attr2 = 'value'
+
+ del n.attr1
+ del n.attr1.attr2.attr3
+
+ # Iteration
+ for _ in n:
+ 'Not executed'
+
+ # representation and conversion to a string
+ assert repr(n) == ''
+ assert str(n) == 'Null'
+
+ # truth value
+ assert bool(n) == False
+ assert bool(n.foo()) == False
+
+ dummy = Null()
+ # context manager
+ with dummy:
+ assert dummy.__name__ == 'Null' # Name should return a str
+
+ # Null objects are always equal to other null object
+ assert n != 1
+ assert n == dummy
+
+
+def testNullCopy():
+ n = Null()
+ n1 = copy.copy(n)
+ assert str(n) == str(n1)
diff --git a/src/oop_ext/foundation/_tests/test_weak_ref.py b/src/oop_ext/foundation/_tests/test_weak_ref.py
new file mode 100644
index 0000000..d6f88e0
--- /dev/null
+++ b/src/oop_ext/foundation/_tests/test_weak_ref.py
@@ -0,0 +1,532 @@
+
+import sys
+import weakref
+
+import pytest
+
+from oop_ext.foundation.weak_ref import (
+ GetRealObj, GetWeakProxy, GetWeakRef, IsSame, IsWeakProxy, IsWeakRef, WeakList, WeakMethodProxy,
+ WeakMethodRef, WeakSet)
+
+
+#===================================================================================================
+# _Stub
+#===================================================================================================
+class _Stub:
+
+ def __hash__(self):
+ return 1
+
+ def __eq__(self, o):
+ return True # always equal
+
+ def __ne__(self, o):
+ return not self == o
+
+ def Method(self):
+ pass
+
+
+class Obj:
+
+ def __init__(self, name):
+ self.name = name
+
+ def __repr__(self):
+ return self.name
+
+
+def testStub():
+ a = _Stub()
+ b = _Stub()
+ assert not a != a
+ assert not a != b
+ assert a == a
+ assert a == b
+ a.Method()
+
+
+def testIsSame():
+ s1 = _Stub()
+ s2 = _Stub()
+
+ r1 = weakref.ref(s1)
+ r2 = weakref.ref(s2)
+
+ p1 = weakref.proxy(s1)
+ p2 = weakref.proxy(s2)
+
+ assert IsSame(s1, s1)
+ assert not IsSame(s1, s2)
+
+ assert IsSame(s1, r1)
+ assert IsSame(s1, p1)
+
+ assert not IsSame(s1, r2)
+ assert not IsSame(s1, p2)
+
+ assert IsSame(p2, r2)
+ assert IsSame(r1, p1)
+ assert not IsSame(r1, p2)
+
+ with pytest.raises(ReferenceError):
+ IsSame(p1, p2)
+
+
+def testGetWeakRef():
+ b = GetWeakRef(None)
+ assert b() is None
+
+
+def testGeneral():
+ b = _Stub()
+ r = GetWeakRef(b.Method)
+ assert r() is not None # should not be a regular weak ref here (but a weak method ref)
+
+ assert IsWeakRef(r)
+ assert not IsWeakProxy(r)
+
+ r = GetWeakProxy(b.Method)
+ r()
+ assert IsWeakProxy(r)
+ assert not IsWeakRef(r)
+
+ r = weakref.ref(b)
+ b2 = _Stub()
+ r2 = weakref.ref(b2)
+ assert r == r2
+ assert hash(r) == hash(r2)
+
+ r = GetWeakRef(b.Method)
+ r2 = GetWeakRef(b.Method)
+ assert r == r2
+ assert hash(r) == hash(r2)
+
+
+def testGetRealObj():
+ b = _Stub()
+ r = GetWeakRef(b)
+ assert GetRealObj(r) is b
+
+ r = GetWeakRef(None)
+ assert GetRealObj(r) is None
+
+
+def testGetWeakProxyFromWeakRef():
+ b = _Stub()
+ r = GetWeakRef(b)
+ proxy = GetWeakProxy(r)
+ assert IsWeakProxy(proxy)
+
+
+def testWeakSet():
+ weak_set = WeakSet()
+ s1 = _Stub()
+ s2 = _Stub()
+
+ weak_set.add(s1)
+ assert isinstance(next(iter(weak_set)), _Stub)
+
+ assert s1 in weak_set
+ CustomAssertEqual(len(weak_set), 1)
+ del s1
+ CustomAssertEqual(len(weak_set), 0)
+
+ weak_set.add(s2)
+ CustomAssertEqual(len(weak_set), 1)
+ weak_set.remove(s2)
+ CustomAssertEqual(len(weak_set), 0)
+
+ weak_set.add(s2)
+ weak_set.clear()
+ CustomAssertEqual(len(weak_set), 0)
+
+ weak_set.add(s2)
+ weak_set.add(s2)
+ weak_set.add(s2)
+ CustomAssertEqual(len(weak_set), 1)
+ del s2
+ CustomAssertEqual(len(weak_set), 0)
+
+ # >>> Testing with FUNCTION
+
+ # Adding twice, having one
+ def function():
+ pass
+
+ weak_set.add(function)
+ weak_set.add(function)
+ CustomAssertEqual(len(weak_set), 1)
+
+
+def testRemove():
+ weak_set = WeakSet()
+
+ s1 = _Stub()
+
+ CustomAssertEqual(len(weak_set), 0)
+
+ # Trying remove, raises KeyError
+ with pytest.raises(KeyError):
+ weak_set.remove(s1)
+ CustomAssertEqual(len(weak_set), 0)
+
+ # Trying discard, no exception raised
+ weak_set.discard(s1)
+ CustomAssertEqual(len(weak_set), 0)
+
+
+def testWeakSet2():
+ weak_set = WeakSet()
+
+ # >>> Removing with DEL
+ s1 = _Stub()
+ weak_set.add(s1.Method)
+ CustomAssertEqual(len(weak_set), 1)
+ del s1
+ CustomAssertEqual(len(weak_set), 0)
+
+ # >>> Removing with REMOVE
+ s2 = _Stub()
+ weak_set.add(s2.Method)
+ CustomAssertEqual(len(weak_set), 1)
+ weak_set.remove(s2.Method)
+ CustomAssertEqual(len(weak_set), 0)
+
+
+def testWeakSetUnionWithWeakSet():
+ ws1, ws2 = WeakSet(), WeakSet()
+ a, b, c = Obj('a'), Obj('b'), Obj('c')
+
+ ws1.add(a)
+ ws1.add(b)
+
+ ws2.add(a)
+ ws2.add(c)
+
+ ws3 = ws1.union(ws2)
+ assert set(ws3) == set(ws2.union(ws1)) == {a, b, c}
+
+ del c
+ assert set(ws3) == set(ws2.union(ws1)) == {a, b}
+
+
+def testWeakSetUnionWithSet():
+ ws = WeakSet()
+ a, b, c = Obj('a'), Obj('b'), Obj('c')
+
+ ws.add(a)
+ ws.add(b)
+
+ s = {a, c}
+
+ ws3 = ws.union(s)
+ assert set(ws3) == set(s.union(set(ws))) == {a, b, c}
+
+ del b
+ assert set(ws3) == set(s.union(set(ws))) == {a, c}
+
+
+def testWeakSetSubWithWeakSet():
+ ws1, ws2 = WeakSet(), WeakSet()
+ a, b, c = Obj('a'), Obj('b'), Obj('c')
+
+ ws1.add(a)
+ ws1.add(b)
+
+ ws2.add(a)
+ ws2.add(c)
+ assert set(ws1 - ws2) == {b}
+ assert set(ws2 - ws1) == {c}
+
+ del c
+ assert set(ws1 - ws2) == {b}
+ assert set(ws2 - ws1) == set()
+
+
+def testWeakSetSubWithSet():
+ ws = WeakSet()
+ s = set()
+ a, b, c = Obj('a'), Obj('b'), Obj('c')
+
+ ws.add(a)
+ ws.add(b)
+
+ s.add(a)
+ s.add(c)
+
+ assert set(ws - s) == {b}
+ assert s - ws == {c}
+
+ del b
+ assert set(ws - s) == set()
+ assert s - ws == {c}
+
+
+def testWithError():
+ weak_set = WeakSet()
+
+ # Not WITH, everything ok
+ s1 = _Stub()
+ weak_set.add(s1.Method)
+ CustomAssertEqual(len(weak_set), 1)
+ del s1
+ CustomAssertEqual(len(weak_set), 0)
+
+ # Using WITH, s2 is not deleted from weak_set
+ s2 = _Stub()
+ with pytest.raises(KeyError):
+ raise KeyError('key')
+ CustomAssertEqual(len(weak_set), 0)
+
+ weak_set.add(s2.Method)
+ CustomAssertEqual(len(weak_set), 1)
+ del s2
+ CustomAssertEqual(len(weak_set), 0)
+
+
+def testFunction():
+ weak_set = WeakSet()
+
+ def function():
+ 'Never called'
+
+ # Adding twice, having one.
+ weak_set.add(function)
+ weak_set.add(function)
+ CustomAssertEqual(len(weak_set), 1)
+
+ # Removing function
+ weak_set.remove(function)
+ assert len(weak_set) == 0
+
+
+def CustomAssertEqual(a, b):
+ '''
+ Avoiding using "assert a == b" because it adds another reference to the ref-count.
+ '''
+ if a == b:
+ pass
+ else:
+ assert False, "{} != {}".format(a, b)
+
+
+def SetupTestAttributes():
+
+ class C:
+
+ def f(self, y=0):
+ return self.x + y
+
+ class D:
+
+ def f(self):
+ 'Never called'
+
+ c = C()
+ c.x = 1
+ d = D()
+
+ return (C, c, d)
+
+
+def testCustomAssertEqual():
+ with pytest.raises(AssertionError) as excinfo:
+ CustomAssertEqual(1, 2)
+
+ assert str(excinfo.value) == '1 != 2\nassert False'
+
+
+def testRefcount():
+ _, c, _ = SetupTestAttributes()
+
+ CustomAssertEqual(sys.getrefcount(c), 2) # 2: one in self, and one as argument to getrefcount()
+ cf = c.f
+ CustomAssertEqual(sys.getrefcount(c), 3) # 3: as above, plus cf
+ rf = WeakMethodRef(c.f)
+ pf = WeakMethodProxy(c.f)
+ CustomAssertEqual(sys.getrefcount(c), 3)
+ del cf
+ CustomAssertEqual(sys.getrefcount(c), 2)
+ rf2 = WeakMethodRef(c.f)
+ CustomAssertEqual(sys.getrefcount(c), 2)
+ del rf
+ del rf2
+ del pf
+ CustomAssertEqual(sys.getrefcount(c), 2)
+
+
+def testDies():
+ _, c, _ = SetupTestAttributes()
+
+ rf = WeakMethodRef(c.f)
+ pf = WeakMethodProxy(c.f)
+ assert not rf.is_dead()
+ assert not pf.is_dead()
+ assert rf()() == 1
+ assert pf(2) == 3
+ c = None
+ assert rf.is_dead()
+ assert pf.is_dead()
+ assert rf() == None
+ with pytest.raises(ReferenceError):
+ pf()
+
+
+def testWorksWithFunctions():
+ SetupTestAttributes()
+
+ def foo(y):
+ return y + 1
+
+ rf = WeakMethodRef(foo)
+ pf = WeakMethodProxy(foo)
+ assert foo(1) == 2
+ assert rf()(1) == 2
+ assert pf(1) == 2
+ assert not rf.is_dead()
+ assert not pf.is_dead()
+
+
+def testWorksWithUnboundMethods():
+ C, c, _ = SetupTestAttributes()
+
+ meth = C.f
+ rf = WeakMethodRef(meth)
+ pf = WeakMethodProxy(meth)
+ assert meth(c) == 1
+ assert rf()(c) == 1
+ assert pf(c) == 1
+ assert not rf.is_dead()
+ assert not pf.is_dead()
+
+
+def testEq():
+ _, c, d = SetupTestAttributes()
+
+ rf1 = WeakMethodRef(c.f)
+ rf2 = WeakMethodRef(c.f)
+ assert rf1 == rf2
+ rf3 = WeakMethodRef(d.f)
+ assert rf1 != rf3
+ del c
+ assert rf1.is_dead()
+ assert rf2.is_dead()
+ assert rf1 == rf2
+
+
+def testProxyEq():
+ _, c, d = SetupTestAttributes()
+
+ pf1 = WeakMethodProxy(c.f)
+ pf2 = WeakMethodProxy(c.f)
+ pf3 = WeakMethodProxy(d.f)
+ assert pf1 == pf2
+ assert pf3 != pf2
+ del c
+ assert pf1 == pf2
+ assert pf1.is_dead()
+ assert pf2.is_dead()
+
+
+def testHash():
+ _, c, _ = SetupTestAttributes()
+
+ r = WeakMethodRef(c.f)
+ r2 = WeakMethodRef(c.f)
+ assert r == r2
+ h = hash(r)
+ assert hash(r) == hash(r2)
+ del c
+ assert r() is None
+ assert hash(r) == h
+
+
+def testRepr():
+ _, c, _ = SetupTestAttributes()
+
+ r = WeakMethodRef(c.f)
+ assert str(r)[:33] == ''
+
+
+def testWeakRefToWeakMethodRef():
+
+ def Foo():
+ 'Never called'
+
+ r = WeakMethodRef(Foo)
+ m_ref = weakref.ref(r)
+ assert m_ref() is r
+
+
+def testWeakList():
+ weak_list = WeakList()
+ s1 = _Stub()
+ s2 = _Stub()
+
+ weak_list.append(s1)
+ assert isinstance(weak_list[0], _Stub)
+
+ assert s1 in weak_list
+ assert 1 == len(weak_list)
+ del s1
+ assert 0 == len(weak_list)
+
+ weak_list.append(s2)
+ assert 1 == len(weak_list)
+ weak_list.remove(s2)
+ assert 0 == len(weak_list)
+
+ weak_list.append(s2)
+ del weak_list[:]
+ assert 0 == len(weak_list)
+
+ weak_list.append(s2)
+ del s2
+ del weak_list[:]
+ assert 0 == len(weak_list)
+
+ s1 = _Stub()
+ weak_list.append(s1)
+ assert 1 == len(weak_list[:])
+
+ del s1
+
+ assert 0 == len(weak_list[:])
+
+ def m1():
+ 'Never called'
+
+ weak_list.append(m1)
+ assert 1 == len(weak_list[:])
+ del m1
+ assert 0 == len(weak_list[:])
+
+ s = _Stub()
+ weak_list.append(s.Method)
+ assert 1 == len(weak_list[:])
+ s = weakref.ref(s)
+ assert 0 == len(weak_list[:])
+ assert s() is None
+
+ s0 = _Stub()
+ s1 = _Stub()
+ weak_list.extend([s0, s1])
+ assert len(weak_list) == 2
+
+
+def testSetItem():
+ weak_list = WeakList()
+ s1 = _Stub()
+ s2 = _Stub()
+ weak_list.append(s1)
+ weak_list.append(s1)
+ assert s1 == weak_list[0]
+ weak_list[0] = s2
+ assert s2 == weak_list[0]
diff --git a/src/oop_ext/foundation/cached_method.py b/src/oop_ext/foundation/cached_method.py
new file mode 100644
index 0000000..186a39d
--- /dev/null
+++ b/src/oop_ext/foundation/cached_method.py
@@ -0,0 +1,185 @@
+
+from .immutable import AsImmutable
+from .odict import odict
+from .types_ import Method
+from .weak_ref import WeakMethodRef
+
+
+#===================================================================================================
+# AbstractCachedMethod
+#===================================================================================================
+class AbstractCachedMethod(Method):
+ '''
+ Base class for cache-manager.
+ The abstract class does not implement the storage of results.
+ '''
+
+ def __init__(self, cached_method=None):
+ # REMARKS: Use WeakMethodRef to avoid cyclic reference.
+ self._method = WeakMethodRef(cached_method)
+ self.enabled = True
+ self.ResetCounters()
+
+ def __call__(self, *args, **kwargs):
+ key = self.GetCacheKey(*args, **kwargs)
+ result = None
+
+ if self.enabled and self._HasResult(key):
+ self.hit_count += 1
+ result = self._GetCacheResult(key, result)
+ else:
+ self.miss_count += 1
+ result = self._CallMethod(*args, **kwargs)
+ self._AddCacheResult(key, result)
+
+ self.call_count += 1
+ return result
+
+ def _CallMethod(self, *args, **kwargs):
+ return self._method()(*args, **kwargs)
+
+ def GetCacheKey(self, *args, **kwargs):
+ '''
+ Use the arguments to build the cache-key.
+ '''
+ if args:
+ if kwargs:
+ return AsImmutable(args), AsImmutable(kwargs)
+
+ return AsImmutable(args)
+
+ if kwargs:
+ return AsImmutable(kwargs)
+
+ def _HasResult(self, key):
+ raise NotImplementedError()
+
+ def _AddCacheResult(self, key, result):
+ raise NotImplementedError()
+
+ def DoClear(self):
+ raise NotImplementedError()
+
+ def Clear(self):
+ self.DoClear()
+ self.ResetCounters()
+
+ def ResetCounters(self):
+ self.call_count = 0
+ self.hit_count = 0
+ self.miss_count = 0
+
+ def _GetCacheResult(self, key, result):
+ raise NotImplementedError()
+
+
+#===================================================================================================
+# CachedMethod
+#===================================================================================================
+class CachedMethod(AbstractCachedMethod):
+ '''
+ Stores ALL the different results and never delete them.
+ '''
+
+ def __init__(self, cached_method=None):
+ super().__init__(cached_method)
+ self._results = {}
+
+ def _HasResult(self, key):
+ return key in self._results
+
+ def _AddCacheResult(self, key, result):
+ self._results[key] = result
+
+ def DoClear(self):
+ self._results.clear()
+
+ def _GetCacheResult(self, key, result):
+ return self._results[key]
+
+
+#===================================================================================================
+# ImmutableParamsCachedMethod
+#===================================================================================================
+class ImmutableParamsCachedMethod(CachedMethod):
+ '''
+ Expects all parameters to already be immutable
+ Considers only the positional parameters of key, ignoring the keyword arguments
+ '''
+
+ def GetCacheKey(self, *args, **kwargs):
+ '''
+ Use the arguments to build the cache-key.
+ '''
+ return args
+
+
+#===================================================================================================
+# LastResultCachedMethod
+#===================================================================================================
+class LastResultCachedMethod(AbstractCachedMethod):
+ '''
+ A cache that stores only the last result.
+ '''
+
+ def __init__(self, cached_method=None):
+ super().__init__(cached_method)
+ self._key = None
+ self._result = None
+
+ def _HasResult(self, key):
+ return self._key == key
+
+ def _AddCacheResult(self, key, result):
+ self._key = key
+ self._result = result
+
+ def DoClear(self):
+ self._key = None
+ self._result = None
+
+ def _GetCacheResult(self, key, result):
+ return self._result
+
+
+#===================================================================================================
+# AttributeBasedCachedMethod
+#===================================================================================================
+class AttributeBasedCachedMethod(CachedMethod):
+ '''
+ This cached method consider changes in object attributes
+ '''
+
+ def __init__(self, cached_method, attr_name_list, cache_size=1, results=None):
+ '''
+ :type cached_method: bound method to be cached
+ :param cached_method:
+ :type attr_name_list: attr names in a C{str} separated by spaces OR in a sequence of C{str}
+ :param attr_name_list:
+ :type cache_size: the cache size
+ :param cache_size:
+ :type results: an optional ref. to an C{odict} for keep cache results
+ :param results:
+ '''
+ CachedMethod.__init__(self, cached_method)
+ if isinstance(attr_name_list, str):
+ self._attr_name_list = attr_name_list.split()
+ else:
+ self._attr_name_list = attr_name_list
+ self._cache_size = cache_size
+ if results is None:
+ self._results = odict()
+ else:
+ self._results = results
+
+ def GetCacheKey(self, *args, **kwargs):
+ instance = self._method().__self__
+ for attr_name in self._attr_name_list:
+ kwargs['_object_%s' % attr_name] = getattr(instance, attr_name)
+ return AbstractCachedMethod.GetCacheKey(self, *args, **kwargs)
+
+ def _AddCacheResult(self, key, result):
+ CachedMethod._AddCacheResult(self, key, result)
+ if len(self._results) > self._cache_size:
+ key0 = next(iter(self._results))
+ del self._results[key0]
diff --git a/src/oop_ext/foundation/callback/__init__.py b/src/oop_ext/foundation/callback/__init__.py
new file mode 100644
index 0000000..2e6c371
--- /dev/null
+++ b/src/oop_ext/foundation/callback/__init__.py
@@ -0,0 +1,18 @@
+from ._callback import ErrorNotHandledInCallback, FunctionNotRegisteredError, HandleErrorOnCallback
+from ._callbacks import Callbacks
+from ._fast_callback import Callback
+from ._priority_callback import PriorityCallback
+from ._shortcuts import After, Before, Remove, WrapForCallback
+
+__all__ = [
+ 'ErrorNotHandledInCallback',
+ 'FunctionNotRegisteredError',
+ 'HandleErrorOnCallback',
+ 'Callbacks',
+ 'Callback',
+ 'PriorityCallback',
+ 'After',
+ 'Before',
+ 'Remove',
+ 'WrapForCallback',
+]
diff --git a/src/oop_ext/foundation/callback/_callback.py b/src/oop_ext/foundation/callback/_callback.py
new file mode 100644
index 0000000..916b585
--- /dev/null
+++ b/src/oop_ext/foundation/callback/_callback.py
@@ -0,0 +1,54 @@
+
+
+#===================================================================================================
+# FunctionNotRegisteredError
+#===================================================================================================
+class FunctionNotRegisteredError(RuntimeError):
+ pass
+
+
+#===================================================================================================
+# ErrorNotHandledInCallback
+#===================================================================================================
+class ErrorNotHandledInCallback(RuntimeError):
+ '''
+ This class identifies an error that should not be handled in the callback.
+ '''
+
+
+#===================================================================================================
+# HandleErrorOnCallback
+#===================================================================================================
+def HandleErrorOnCallback(func, *args, **kwargs):
+ '''
+ Called when there's some error calling a callback.
+
+ :param object func:
+ The callback called.
+
+ :param list args:
+ The arguments passed to the callback.
+
+ :param dict kwargs:
+ The keyword arguments passed to the callback.
+ '''
+ if hasattr(func, 'func_code'):
+ name, filename, line = (
+ func.__code__.co_name,
+ func.__code__.co_filename,
+ func.__code__.co_firstlineno
+ )
+ # Use default python trace format so that we have linking on pydev.
+ func = '\n File "{}", line {}, in {} (Called from Callback)\n'.format(filename, line, name)
+ else:
+ # That's ok, it may be that it's not really a method.
+ func = '{}\n'.format(repr(func))
+
+ msg = 'Error while trying to call {}'.format(func)
+ if args:
+ msg += 'Args: {}\n'.format(args)
+ if kwargs:
+ msg += 'Kwargs: {}\n'.format(kwargs)
+
+ from oop_ext.foundation import handle_exception
+ handle_exception.HandleException(msg)
diff --git a/src/oop_ext/foundation/callback/_callback_wrapper.py b/src/oop_ext/foundation/callback/_callback_wrapper.py
new file mode 100644
index 0000000..08af416
--- /dev/null
+++ b/src/oop_ext/foundation/callback/_callback_wrapper.py
@@ -0,0 +1,21 @@
+
+from oop_ext.foundation.types_ import Method
+
+
+#===================================================================================================
+# _CallbackWrapper
+#===================================================================================================
+class _CallbackWrapper(Method):
+
+ def __init__(self, weak_method_callback):
+ self.weak_method_callback = weak_method_callback
+
+ # Maintaining the OriginalMethod() interface that clients expect.
+ self.OriginalMethod = weak_method_callback
+
+ def __call__(self, sender, *args, **kwargs):
+ c = self.weak_method_callback()
+ if c is None:
+ raise ReferenceError('This should never happen: The sender already died, so, '\
+ 'how can this method still be called?')
+ c(sender(), *args, **kwargs)
diff --git a/src/oop_ext/foundation/callback/_callbacks.py b/src/oop_ext/foundation/callback/_callbacks.py
new file mode 100644
index 0000000..97b624d
--- /dev/null
+++ b/src/oop_ext/foundation/callback/_callbacks.py
@@ -0,0 +1,34 @@
+
+from ._shortcuts import After, Before, Remove
+
+
+#===================================================================================================
+# Callbacks
+#===================================================================================================
+class Callbacks:
+ '''
+ Holds created callbacks, making it easy to disconnect later.
+
+ Note: keeps a strong reference to the callback and the sender, thus, they won't be garbage-
+ collected while still connected in this case.
+ '''
+
+ def __init__(self):
+ self._callbacks = []
+
+ def Before(self, sender, *callbacks, **kwargs):
+ sender = Before(sender, *callbacks, **kwargs)
+ for callback in callbacks:
+ self._callbacks.append((sender, callback))
+ return sender
+
+ def After(self, sender, *callbacks, **kwargs):
+ sender = After(sender, *callbacks, **kwargs)
+ for callback in callbacks:
+ self._callbacks.append((sender, callback))
+ return sender
+
+ def RemoveAll(self):
+ for sender, callback in self._callbacks:
+ Remove(sender, callback)
+ self._callbacks[:] = []
diff --git a/src/oop_ext/foundation/callback/_fast_callback.py b/src/oop_ext/foundation/callback/_fast_callback.py
new file mode 100644
index 0000000..0f7e2bb
--- /dev/null
+++ b/src/oop_ext/foundation/callback/_fast_callback.py
@@ -0,0 +1,401 @@
+
+import functools
+import inspect
+import logging
+import types
+import weakref
+
+from oop_ext.foundation.compat import GetClassForUnboundMethod
+from oop_ext.foundation.is_frozen import IsDevelopment
+from oop_ext.foundation.odict import odict
+from oop_ext.foundation.reraise import Reraise
+from oop_ext.foundation.weak_ref import WeakMethodProxy
+
+from ._callback_wrapper import _CallbackWrapper
+
+log = logging.getLogger(__name__)
+
+
+#===================================================================================================
+# Callback
+#===================================================================================================
+class Callback:
+ '''
+ Object that provides a way for others to connect in it and later call it to call
+ those connected.
+
+ .. note:: This implementation is improved in that it works directly accessing functions based
+ on a key in an ordered dict, so, Register, Unregister and Contains are much faster than the
+ old callback.
+
+ .. note:: it only stores weakrefs to objects connected
+
+ .. note:: __slots__ added, so, it cannot have weakrefs to it (but as it stores weakrefs
+ internally, that shouldn't be a problem). If weakrefs are really needed,
+ __weakref__ should be added to the slots.
+
+ Determining kind of callable (Python 3)
+ ---------------------------------------
+
+ Many parts of callback implementation rely on identifying the kind of callable: is it a
+ free function? is it a function bound to an object?
+
+ Below there is a table to help understand how different objects are classified:
+
+ |has__self__|has__call__|has__call__self__|isbuiltin|isfunction|ismethod
+ --------------------|-----------|-----------|-----------------|---------|----------|--------
+ free function |False |True |True |False |True |False
+ bound method |True |True |True |False |False |True
+ class method |True |True |True |False |False |True
+ bound class method |True |True |True |False |False |True
+ function object |False |True |True |False |False |False
+ builtin function |True |True |True |True |False |False
+ object |True |True |True |True |False |False
+ custom object |False |False |False |False |False |False
+ string |False |False |False |False |False |False
+
+ where rows are:
+
+ ```python
+ def free_fn(foo):
+ # `free function`
+ pass
+
+ class Foo:
+
+ def bound_fn(self, foo):
+ pass
+
+ class Bar:
+
+ @classmethod
+ def class_fn(cls, foo):
+ pass
+
+ class ObjectFn:
+
+ def __call__(self, foo):
+ pass
+
+ foo = Foo() # foo is `custom object`, foo.bound_fn is `bound method`
+ bar = Bar() # Bar.class_fn is `class method`, bar.class_fn is `bound class method`
+
+ object_fn = ObjectFn() # `function object`
+
+ obj = object() # `object`
+ string = 'foo' # `string`
+ builtin_fn = string.split # `builtin function`
+ ```
+
+ and where columns are:
+
+ * isbuiltin: inspect.isbuiltin
+ * isfunction: inspect.isfunction
+ * ismethod: inspect.ismethod
+ * has__self__: hasattr(obj, '__self__')
+ * has__call__: hasattr(obj, '__call__')
+ * has__call__self__: hasattr(obj.__call__, '__self__') if hasattr(obj, '__call__') else False
+ '''
+
+ __slots__ = [
+ '_callbacks',
+ '_handle_errors',
+ '__weakref__',
+ ]
+
+ # This constant defines whether the errors will be handled by default in all Callbacks or not.
+ # Handled errors won't stop the execution if an exception happens when executing the callbacks.
+ DEFAULT_HANDLE_ERRORS = True
+
+ INFO_POS_FUNC_OBJ = 0
+ INFO_POS_FUNC_FUNC = 1
+ INFO_POS_FUNC_CLASS = 2
+
+ # Can be set to True to debug (should be removed after all applications
+ # properly test the new behavior).
+ DEBUG_NEW_WEAKREFS = False
+
+ def __init__(self, handle_errors=None):
+ '''
+ :param bool handle_errors:
+ If True, any errors raised while calling the callbacks will not stop the execution
+ flow of the application, but will call the system error handler so that error
+ does not fail silently.
+ '''
+ if handle_errors is None:
+ handle_errors = self.DEFAULT_HANDLE_ERRORS
+ self._handle_errors = handle_errors
+ # _callbacks is no longer lazily created: This makes the creation a bit slower, but
+ # everything else is faster (as having to check for hasattr each time is slow).
+ self._callbacks = odict()
+
+ def _GetKey(self, func, extra_args):
+ '''
+ :param object func:
+ The function for which we want the key.
+
+ :rtype: object
+ :returns:
+ Returns the key to be used to access the object.
+
+ .. note:: The key is guaranteed to be unique among the living objects, but if the object
+ is garbage collected, a new function may end up having the same key.
+ '''
+ if func.__class__ == _CallbackWrapper:
+ func = func.OriginalMethod()
+
+ try:
+ if func.__self__ is not None:
+ # bound method
+ return (id(func.__self__), id(func.__func__), id(func.__self__.__class__))
+ else:
+ return (id(func.__func__), id(GetClassForUnboundMethod(func)))
+
+ except AttributeError:
+ # not a method -- a callable: create a strong reference (the CallbackWrapper
+ # is depending on this behaviour... is it correct?)
+ return id(func)
+
+ def _GetInfo(self, func):
+ '''
+ :rtype: tuple(func_obj, func_func, func_class)
+ :returns:
+ Returns a tuple with the information needed to call a method later on (close to the
+ WeakMethodRef, but a bit more specialized -- and faster for this context).
+ '''
+ # Note: if it's a _CallbackWrapper, we want to register it and not the 'original method'
+ # at this point, but if it's a WeakMethodProxy, register the original method (we'll make a
+ # weak reference later anyways).
+ if func.__class__ == WeakMethodProxy:
+ func = func.GetWrappedFunction()
+
+ if _IsCallableObject(func):
+ if self.DEBUG_NEW_WEAKREFS:
+ obj_str = '{}'.format(func.__class__)
+ print('Changed behavior for: %s' % obj_str)
+
+ def ondie(r):
+ # I.e.: the hint here is that a reference may die before expected
+ print('Reference died: {}'.format(obj_str))
+
+ return (weakref.ref(func, ondie), None, None)
+ return (weakref.ref(func), None, None)
+
+ try:
+ if func.__self__ is not None and func.__func__ is not None:
+ # bound method
+ return (weakref.ref(func.__self__), func.__func__, func.__self__.__class__)
+ else:
+ # unbound method
+ return (None, func.__func__, GetClassForUnboundMethod(func))
+ except AttributeError:
+ # not a method -- a callable: create a strong reference (the CallbackWrapper
+ # is depending on this behaviour... is it correct?)
+ return (None, func, None)
+
+ def __call__(self, *args, **kwargs): # @DontTrace
+ '''
+ Calls every registered function with the given args and kwargs.
+ '''
+ callbacks = self._callbacks
+ if not callbacks:
+ return
+
+ to_call = []
+
+ for id, info_and_extra_args in list(callbacks.items()): # iterate in a copy
+ info = info_and_extra_args[0]
+ func_obj = info[self.INFO_POS_FUNC_OBJ]
+ if func_obj is not None:
+ # Ok, we have a self.
+ func_obj = func_obj()
+ if func_obj is None:
+ # self is dead
+ del callbacks[id]
+ else:
+ func_func = info[self.INFO_POS_FUNC_FUNC]
+ if func_func is None:
+ to_call.append((func_obj, info_and_extra_args[1]))
+ else:
+ to_call.append(
+ (
+ types.MethodType(func_func, func_obj),
+ info_and_extra_args[1]
+ )
+ )
+ else:
+ func_func = info[self.INFO_POS_FUNC_FUNC]
+ if func_func.__class__ == _CallbackWrapper:
+ # The instance of the _CallbackWrapper already died! (func_obj is None)
+ original_method = func_func.OriginalMethod()
+ if original_method is None:
+ del callbacks[id]
+ continue
+
+ # No self: either classmethod or just callable
+ to_call.append((func_func, info_and_extra_args[1]))
+
+ to_call = self._FilterToCall(to_call, args, kwargs)
+
+ # let's keep the 'if' outside of the iteration...
+ if self._handle_errors:
+ for func, extra_args in to_call:
+ try:
+ func(*extra_args + args, **kwargs)
+ except Exception as e:
+ from oop_ext.foundation.callback import ErrorNotHandledInCallback
+ # Note that if some error shouldn't really be handled here, clients can raise
+ # a subclass of ErrorNotHandledInCallback
+ if isinstance(e, ErrorNotHandledInCallback):
+ Reraise(e, 'Error while trying to call %r' % func)
+ else:
+ import traceback
+ from oop_ext.foundation.callback import HandleErrorOnCallback
+ # We need to log the current stack so we can at least know who called this
+ # callback.
+ log.error('Error while trying to call {!r}:\n\n{}'.format(
+ func, ''.join(traceback.format_stack())))
+ HandleErrorOnCallback(func, *extra_args + args, **kwargs)
+ else:
+ for func, extra_args in to_call:
+ try:
+ func(*extra_args + args, **kwargs)
+ except Exception as e:
+ Reraise(e, 'Error while trying to call %r' % func)
+
+ def _FilterToCall(self, to_call, args, kwargs):
+ '''
+ Provides a chance for subclasses to filter the function/extra arguments to call.
+
+ :param list(tuple(method,tuple)) to_call:
+ list(function_to_call, extra_arguments)
+
+ :param args:
+ Arguments being passed to the call.
+
+ :param kwargs:
+ Keyword arguments being passed to the call.
+
+ :return list(tuple(method,tuple):
+ Return the filtered list with the function/extra arguments to call.
+ '''
+ return to_call
+
+ _EXTRA_ARGS_CONSTANT = tuple()
+
+ def Register(self, func, extra_args=_EXTRA_ARGS_CONSTANT):
+ '''
+ Registers a function in the callback.
+
+ :param object func:
+ Method or function that will be called later.
+
+ :param list(object) extra_args:
+ A list with the objects to be used
+ '''
+ if IsDevelopment() and hasattr(func, 'im_class'):
+ # TODO: Python 3 - This can be removed after deprecating Python 2
+ if not inspect.isclass(getattr(func, 'im_class')):
+ msg = '%r object has inconsistent internal attributes and is not compatible with ' \
+ 'Callback.\nim_class = %r\n(If using a MagicMock, remember to pass spec=lambda:None).'
+ raise RuntimeError(msg % (func, getattr(func, 'im_class')))
+ if extra_args is not self._EXTRA_ARGS_CONSTANT:
+ extra_args = tuple(extra_args)
+
+ key = self._GetKey(func, extra_args)
+ callbacks = self._callbacks
+ callbacks.pop(key, None) # Remove if it exists
+ callbacks[key] = (self._GetInfo(func), extra_args)
+
+ def Contains(self, func, extra_args=_EXTRA_ARGS_CONSTANT):
+ '''
+ :param object func:
+ The function that may be contained in this callback.
+
+ :rtype: bool
+ :returns:
+ True if the function is already registered within the callbacks and False
+ otherwise.
+ '''
+ key = self._GetKey(func, extra_args)
+
+ callbacks = self._callbacks
+
+ info_and_extra_args = callbacks.get(key)
+ if info_and_extra_args is None:
+ return False
+
+ if func.__class__ == WeakMethodProxy:
+ func = func.GetWrappedFunction()
+
+ # We must check if it's actually the same, because it may be that the ids we've gotten for
+ # this object were actually from a garbage-collected function that was previously registered.
+
+ info = info_and_extra_args[0]
+ func_obj = info[self.INFO_POS_FUNC_OBJ]
+ func_func = info[self.INFO_POS_FUNC_FUNC]
+ if func_obj is not None:
+ # Ok, we have a self.
+ func_obj = func_obj()
+ if func_obj is None:
+ # self is dead
+ del callbacks[key]
+ return False
+ else:
+ return func is func_obj or (func_func is not None and func == types.MethodType(func_func, func_obj))
+ else:
+ if func_func.__class__ == _CallbackWrapper:
+ # The instance of the _CallbackWrapper already died! (func_obj is None)
+ original_method = func_func.OriginalMethod()
+ if original_method is None:
+ del callbacks[key]
+ return False
+ return original_method == func
+
+ if func_func == func:
+ return True
+ try:
+ f = func.__func__
+ except AttributeError:
+ return False
+ else:
+ return f == func_func
+
+ raise AssertionError('Should not get here!')
+
+ def Unregister(self, func, extra_args=_EXTRA_ARGS_CONSTANT):
+ '''
+ Unregister a function previously registered with Register.
+
+ :param object func:
+ The function to be unregistered.
+ '''
+ key = self._GetKey(func, extra_args)
+
+ try:
+ # As there can only be 1 instance with the same id alive, it should be OK just
+ # deleting it directly (because if there was a dead reference pointing to it it will
+ # be already dead anyways)
+ del self._callbacks[key]
+ except (KeyError, AttributeError):
+ # Even when unregistering some function that isn't registered we shouldn't trigger an
+ # exception, just do nothing
+ pass
+
+ def UnregisterAll(self):
+ '''
+ Unregisters all functions
+ '''
+ self._callbacks.clear()
+
+ def __len__(self):
+ return len(self._callbacks)
+
+
+def _IsCallableObject(func):
+ return not inspect.isbuiltin(func) and \
+ not inspect.isfunction(func) and \
+ not inspect.ismethod(func) and \
+ not func.__class__ == functools.partial and \
+ func.__class__ != _CallbackWrapper and \
+ not getattr(func, '__CALLBACK_KEEP_STRONG_REFERENCE__', False)
diff --git a/src/oop_ext/foundation/callback/_priority_callback.py b/src/oop_ext/foundation/callback/_priority_callback.py
new file mode 100644
index 0000000..0cfe35b
--- /dev/null
+++ b/src/oop_ext/foundation/callback/_priority_callback.py
@@ -0,0 +1,60 @@
+
+from oop_ext.foundation.decorators import Override
+from oop_ext.foundation.odict import odict
+
+from ._fast_callback import Callback
+
+
+#===================================================================================================
+# PriorityCallback
+#===================================================================================================
+class PriorityCallback(Callback):
+ '''
+ Class that's able to give a priority to the added callbacks when they're registered.
+ '''
+
+ INFO_POS_PRIORITY = 3
+
+ @Override(Callback._GetInfo)
+ def _GetInfo(self, func, priority):
+ '''
+ Overridden to add the priority to the info.
+
+ :param int priority:
+ The priority to be set to the added callback.
+ '''
+ info = Callback._GetInfo(self, func)
+ return info + (priority,)
+
+ @Override(Callback.Register)
+ def Register(self, func, extra_args=Callback._EXTRA_ARGS_CONSTANT, priority=5):
+ '''
+ Register a function in the callback.
+ :param object func:
+ Method or function that will be called later.
+
+ :param int priority:
+ If passed, it'll be be used to put the callback into the correct place based on the
+ priority passed (lower numbers have higher priority).
+ '''
+ if extra_args is not self._EXTRA_ARGS_CONSTANT:
+ extra_args = tuple(extra_args)
+
+ key = self._GetKey(func, extra_args)
+ try:
+ callbacks = self._callbacks
+ except AttributeError:
+ callbacks = self._callbacks = odict()
+
+ callbacks.pop(key, None) # Remove if it exists
+ new_info = self._GetInfo(func, priority)
+
+ i = 0
+ for i, (info, _extra) in enumerate(callbacks.values()):
+ if info[self.INFO_POS_PRIORITY] > priority:
+ break
+ else:
+ # Iterated all... so, go one more the last position.
+ i += 1
+
+ callbacks.insert(i, key, (new_info, extra_args))
diff --git a/src/oop_ext/foundation/callback/_shortcuts.py b/src/oop_ext/foundation/callback/_shortcuts.py
new file mode 100644
index 0000000..6cc4213
--- /dev/null
+++ b/src/oop_ext/foundation/callback/_shortcuts.py
@@ -0,0 +1,227 @@
+
+import weakref
+
+from oop_ext.foundation.types_ import Method
+from oop_ext.foundation.weak_ref import WeakMethodRef
+
+from ._callback_wrapper import _CallbackWrapper
+from ._fast_callback import Callback, GetClassForUnboundMethod
+
+
+#===================================================================================================
+# _CreateBeforeOrAfter
+#===================================================================================================
+def _CreateBeforeOrAfter(method, callback, sender_as_parameter, before=True):
+
+ wrapper = WrapForCallback(method)
+ original_method = wrapper.OriginalMethod()
+
+ extra_args = []
+
+ if sender_as_parameter:
+ try:
+ im_self = original_method.__self__
+ except AttributeError:
+ pass
+ else:
+ extra_args.append(weakref.ref(im_self))
+
+ # this is not garbage collected directly when added to the wrapper (which will create a WeakMethodRef to it)
+ # because it's not a real method, so, WeakMethodRef will actually maintain a strong reference to it.
+ callback = _CallbackWrapper(WeakMethodRef(callback))
+
+ if before:
+ wrapper.AppendBefore(callback, extra_args)
+ else:
+ wrapper.AppendAfter(callback, extra_args)
+
+ return wrapper
+
+
+#===================================================================================================
+# Before
+#===================================================================================================
+def Before(method, callback, sender_as_parameter=False):
+ '''
+ Registers the given callback to be executed before the given method is called, with the
+ same arguments.
+
+ The method can be eiher an unbound method or a bound method. If it is an unbound method,
+ *all* instances of the class will generate callbacks when method is called. If it is a bound
+ method, only the method of the instance will generate callbacks.
+
+ Remarks:
+ The function has changed its signature to accept an extra parameter (sender_as_parameter).
+ Using "*args" as before made impossible to add new parameters to the function.
+ '''
+ return _CreateBeforeOrAfter(method, callback, sender_as_parameter)
+
+
+#===================================================================================================
+# After
+#===================================================================================================
+def After(method, callback, sender_as_parameter=False):
+ '''
+ Registers the given callbacks to be execute after the given method is called, with the same
+ arguments.
+
+ The method can be eiher an unbound method or a bound method. If it is an unbound method,
+ *all* instances of the class will generate callbacks when method is called. If it is a bound
+ method, only the method of the instance will generate callbacks.
+
+ Remarks:
+ This function has changed its signature to accept an extra parameter (sender_as_parameter).
+ Using "*args" as before made impossible to add new parameters to the function.
+ '''
+ return _CreateBeforeOrAfter(method, callback, sender_as_parameter, before=False)
+
+
+#===================================================================================================
+# Remove
+#===================================================================================================
+def Remove(method, callback):
+ '''
+ Removes the given callback from a method previously connected using after or before.
+ Return true if the callback was removed, false otherwise.
+ '''
+ wrapped = _GetWrapped(method)
+ if wrapped:
+ return wrapped.Remove(callback)
+ return False
+
+
+#===================================================================================================
+# Implementation Details
+#===================================================================================================
+class _MethodWrapper(Method): # It needs to be a subclass of Method for interface checks.
+
+ __slots__ = [
+ '_before',
+ '_after',
+ '_method',
+ '_name',
+ 'OriginalMethod',
+ ]
+
+ def __init__(self, method):
+ self._before = None
+ self._after = None
+ self._method = WeakMethodRef(method)
+ self._name = method.__name__
+
+ # Maintaining the OriginalMethod() interface that clients expect.
+ self.OriginalMethod = self._method
+
+ def __repr__(self):
+ return '_MethodWrapper({}): {}'.format(id(self), self._name)
+
+ def __call__(self, *args, **kwargs):
+
+ if self._before is not None:
+ self._before(*args, **kwargs)
+
+ m = self._method()
+ if m is None:
+ raise ReferenceError(
+ "Error: the object that contained this method (%s) has already been garbage collected"
+ % self._name)
+
+ result = m(*args, **kwargs)
+
+ if self._after is not None:
+ self._after(*args, **kwargs)
+
+ return result
+
+ def AppendBefore(self, callback, extra_args=None, handle_errors=True):
+ '''
+ Append the given callbacks in the list of callback to be executed BEFORE the method.
+ '''
+ if extra_args is None:
+ extra_args = []
+
+ if self._before is None:
+ self._before = Callback(handle_errors=handle_errors)
+ self._before.Register(callback, extra_args)
+
+ def AppendAfter(self, callback, extra_args=None, handle_errors=True):
+ '''
+ Append the given callbacks in the list of callback to be executed AFTER the method.
+ '''
+ if extra_args is None:
+ extra_args = []
+
+ if self._after is None:
+ self._after = Callback(handle_errors=handle_errors)
+ self._after.Register(callback, extra_args)
+
+ def Remove(self, callback):
+ '''
+ Remove the given callback from both the BEFORE and AFTER callbacks lists.
+ '''
+ result = False
+
+ if self._before is not None and self._before.Contains(callback):
+ self._before.Unregister(callback)
+ result = True
+ if self._after is not None and self._after.Contains(callback):
+ self._after.Unregister(callback)
+ result = True
+
+ return result
+
+
+#===================================================================================================
+# _GetWrapped
+#===================================================================================================
+def _GetWrapped(method):
+ '''
+ Returns true if the given method is already wrapped.
+ '''
+ if isinstance(method, _MethodWrapper):
+ return method
+ try:
+ return method._wrapped_instance
+ except AttributeError:
+ return None
+
+
+#===================================================================================================
+# WrapForCallback
+#===================================================================================================
+def WrapForCallback(method):
+ '''Generates a wrapper for the given method, or returns the method itself
+ if it is already a wrapper.
+ '''
+ wrapped = _GetWrapped(method)
+ if wrapped is not None:
+ # its a wrapper already
+ if not hasattr(method, '__self__'):
+ return wrapped
+
+ # Taking care for the situation where we add a callback to the class and later to the
+ # instance.
+ # Note that the other way around does not work at all (i.e.: if a callback is first added
+ # to the instance, there's no way we'll find about that when adding it to the class
+ # anyways).
+ if method.__self__ is None:
+ if wrapped._method._obj is None:
+ return wrapped
+
+ wrapper = _MethodWrapper(method)
+ if not hasattr(method, '__self__') or method.__self__ is None:
+ # override the class method
+
+ # we must make it a regular call for classmethods (it MUST not be a bound
+ # method nor class when doing that).
+ def call(*args, **kwargs):
+ return wrapper(*args, **kwargs)
+
+ call.__name__ = method.__name__
+ call._wrapped_instance = wrapper
+
+ setattr(GetClassForUnboundMethod(method), method.__name__, call)
+ else:
+ # override the instance method
+ setattr(method.__self__, method.__name__, wrapper)
+ return wrapper
diff --git a/src/oop_ext/foundation/callback/_tests/__init__.py b/src/oop_ext/foundation/callback/_tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/oop_ext/foundation/callback/_tests/test_callback.py b/src/oop_ext/foundation/callback/_tests/test_callback.py
new file mode 100644
index 0000000..ebf0184
--- /dev/null
+++ b/src/oop_ext/foundation/callback/_tests/test_callback.py
@@ -0,0 +1,1084 @@
+
+import weakref
+
+import pytest
+
+from oop_ext.foundation.callback import After, Before, Callback, Callbacks, Remove
+from oop_ext.foundation.types_ import Null
+from oop_ext.foundation.weak_ref import GetWeakProxy, WeakMethodProxy, WeakMethodRef
+
+
+#===================================================================================================
+# _MyClass
+#===================================================================================================
+class _MyClass:
+
+ def SetAlpha(self, value):
+ self.alpha = value
+
+ def SetBravo(self, value):
+ self.bravo = value
+
+
+class C:
+
+ def __init__(self, test_case):
+ self.test_case = test_case
+
+ def foo(self, arg):
+ self.test_case.foo_called = (self, arg)
+ return arg
+
+
+class Stub:
+
+ def call(self, *args, **kwargs):
+ pass
+
+
+@pytest.yield_fixture(autouse=True)
+def restore_test_classes():
+ """
+ It can't reliably bind callbacks to methods in local temporary classes from Python 3 onward,
+ as callback is unable to later know to which class was bound to.
+
+ For this reason, we have to use global, shared classes for these tests. This fixture makes sure
+ these test classes used are always restored to clean state to avoid undesired side-effects in
+ other tests.
+ """
+ original_stub_call = Stub.call
+ original_c_foo = C.foo
+
+ yield
+ Stub.call = original_stub_call
+ C.foo = original_c_foo
+
+
+#===================================================================================================
+# Test
+#===================================================================================================
+class Test:
+
+ def setup_method(self, method):
+ self.foo_called = None
+
+ self.a = C(self)
+ self.b = C(self)
+
+ def after(*args):
+ self.after_called = args
+ self.after_count += 1
+
+ self.after = after
+ self.after_called = None
+ self.after_count = 0
+
+ def before(*args):
+ self.before_called = args
+ self.before_count += 1
+
+ self.before = before
+ self.before_called = None
+ self.before_count = 0
+
+ def testClassOverride(self):
+ Before(C.foo, self.before)
+ After(C.foo, self.after)
+
+ self.a.foo(1)
+ assert self.foo_called == (self.a, 1)
+ assert self.after_called == (self.a, 1)
+ assert self.after_count == 1
+ assert self.before_called == (self.a, 1)
+ assert self.before_count == 1
+
+ self.b.foo(2)
+ assert self.foo_called == (self.b, 2)
+ assert self.after_called == (self.b, 2)
+ assert self.after_count == 2
+ assert self.before_called == (self.b, 2)
+ assert self.before_count == 2
+
+ assert Remove(C.foo, self.before)
+
+ self.a.foo(3)
+ assert self.foo_called == (self.a, 3)
+ assert self.after_called == (self.a, 3)
+ assert self.after_count == 3
+ assert self.before_called == (self.b, 2)
+ assert self.before_count == 2
+
+ def testInstanceOverride(self):
+ Before(self.a.foo, self.before)
+ After(self.a.foo, self.after)
+
+ self.a.foo(1)
+ assert self.foo_called == (self.a, 1)
+ assert self.after_called == (1,)
+ assert self.before_called == (1,)
+ assert self.after_count == 1
+ assert self.before_count == 1
+
+ self.b.foo(2)
+ assert self.foo_called == (self.b, 2)
+ assert self.after_called == (1,)
+ assert self.before_called == (1,)
+ assert self.after_count == 1
+ assert self.before_count == 1
+
+ assert Remove(self.a.foo, self.before) == True
+
+ self.a.foo(2)
+ assert self.foo_called == (self.a, 2)
+ assert self.after_called == (2,)
+ assert self.before_called == (1,)
+ assert self.after_count == 2
+ assert self.before_count == 1
+
+ Before(self.a.foo, self.before)
+ Before(self.a.foo, self.before) # Registering twice has no effect the 2nd time
+
+ self.a.foo(5)
+ assert self.before_called == (5,)
+ assert self.before_count == 2
+
+ def testBoundMethodsWrong(self):
+ foo = self.a.foo
+ Before(foo, self.before)
+ After(foo, self.after)
+
+ foo(10)
+ assert 0 == self.before_count
+ assert 0 == self.after_count
+
+ def testBoundMethodsRight(self):
+ foo = self.a.foo
+ foo = Before(foo, self.before)
+ foo = After(foo, self.after)
+
+ foo(10)
+ assert self.before_count == 1
+ assert self.after_count == 1
+
+ def testReferenceDies(self):
+
+ class Receiver:
+
+ def before(dummy, *args): # @NoSelf
+ self.before_count += 1
+ self.before_args = args
+
+ rec = Receiver()
+ self.before_count = 0
+ self.before_args = None
+
+ foo = self.a.foo
+ foo = Before(foo, rec.before)
+
+ foo(10)
+ assert self.before_args == (10,)
+ assert self.before_count == 1
+
+ del rec # kill the receiver
+
+ foo(20)
+ assert self.before_args == (10,)
+ assert self.before_count == 1
+
+ def testSenderDies(self):
+
+ class Sender:
+
+ def foo(s, *args): # @NoSelf
+ s.args = args
+
+ def __del__(dummy): # @NoSelf
+ self.sender_died = True
+
+ self.sender_died = False
+ s = Sender()
+ w = weakref.ref(s)
+ Before(s.foo, self.before)
+ s.foo(10)
+ f = s.foo # hold a strong reference to s
+ assert self.before_count == 1
+ assert self.before_called == (10,)
+
+ assert not self.sender_died
+ del s
+ assert self.sender_died
+
+ with pytest.raises(ReferenceError):
+ f(10) # must have already died: we don't have a strong reference
+ assert w() is None
+
+ def testLessArgs(self):
+
+ class C:
+
+ def foo(self, _x, _y, **_kwargs):
+ pass
+
+ def after_2(x, y, *args, **kwargs):
+ self.after_2_res = x, y
+
+ def after_1(x, *args, **kwargs):
+ self.after_1_res = x
+
+ def after_0(*args, **kwargs):
+ self.after_0_res = 0
+
+ self.after_2_res = None
+ self.after_1_res = None
+ self.after_0_res = None
+
+ c = C()
+
+ After(c.foo, after_2)
+ After(c.foo, after_1)
+ After(c.foo, after_0)
+
+ c.foo(10, 20, foo=1)
+ assert self.after_2_res == (10, 20)
+ assert self.after_1_res == 10
+ assert self.after_0_res == 0
+
+ def testWithCallable(self):
+
+ class Stub:
+
+ def call(self, _b):
+ pass
+
+ class Aux:
+
+ def __call__(self, _b):
+ self.called = True
+
+ s = Stub()
+ a = Aux()
+ After(s.call, a)
+ s.call(True)
+
+ assert a.called
+
+ def testCallback(self):
+
+ self.args = [None, None]
+
+ def f1(*args):
+ self.args[0] = args
+
+ def f2(*args):
+ self.args[1] = args
+
+ c = Callback()
+ c.Register(f1)
+
+ c(1, 2)
+
+ assert self.args[0] == (1, 2)
+
+ c.Unregister(f1)
+ self.args[0] = None
+ c(10, 20)
+ assert self.args[0] == None
+
+ def foo(): pass
+
+ # self.assertNotRaises(FunctionNotRegisteredError, c.Unregister, foo)
+ c.Unregister(foo)
+
+ def test_extra_args(self):
+ '''
+ Tests the extra-args parameter in Register method.
+ '''
+ self.zulu_calls = []
+
+ def zulu_one(*args):
+ self.zulu_calls.append(args)
+
+ def zulu_too(*args):
+ self.zulu_calls.append(args)
+
+ alpha = Callback()
+ alpha.Register(zulu_one, [1, 2])
+
+ assert self.zulu_calls == []
+
+ alpha('a')
+ assert self.zulu_calls == [
+ (1, 2, 'a')
+ ]
+
+ alpha('a', 'b', 'c')
+ assert self.zulu_calls == [
+ (1, 2, 'a'),
+ (1, 2, 'a', 'b', 'c')
+ ]
+
+ # Test a second method with extra-args
+ alpha.Register(zulu_too, [9])
+
+ alpha('a')
+ assert self.zulu_calls == [
+ (1, 2, 'a'),
+ (1, 2, 'a', 'b', 'c'),
+ (1, 2, 'a'),
+ (9, 'a'),
+ ]
+
+ def test_sender_as_parameter(self):
+ self.zulu_calls = []
+
+ def zulu_one(*args):
+ self.zulu_calls.append(args)
+
+ def zulu_two(*args):
+ self.zulu_calls.append(args)
+
+ Before(self.a.foo, zulu_one, sender_as_parameter=True)
+
+ assert self.zulu_calls == []
+ self.a.foo(0)
+ assert self.zulu_calls == [(self.a, 0)]
+
+ # The second method registered with the sender_as_parameter on did not receive it.
+ Before(self.a.foo, zulu_two, sender_as_parameter=True)
+
+ self.zulu_calls = []
+ self.a.foo(1)
+ assert self.zulu_calls == [(self.a, 1), (self.a, 1)]
+
+ def test_sender_as_parameter_after_and_before(self):
+ self.zulu_calls = []
+
+ def zulu_one(*args):
+ self.zulu_calls.append((1, args))
+
+ def zulu_too(*args):
+ self.zulu_calls.append((2, args))
+
+ Before(self.a.foo, zulu_one, sender_as_parameter=True)
+ After(self.a.foo, zulu_too)
+
+ assert self.zulu_calls == []
+ self.a.foo(0)
+ assert self.zulu_calls == [(1, (self.a, 0)), (2, (0,))]
+
+ def testContains(self):
+
+ def foo(x):
+ pass
+
+ c = Callback()
+ assert not c.Contains(foo)
+ c.Register(foo)
+
+ assert c.Contains(foo)
+ c.Unregister(foo)
+ assert not c.Contains(foo)
+
+ def testCallbackReceiverDies(self):
+
+ class A:
+
+ def on_foo(dummy, *args): # @NoSelf
+ self.args = args
+
+ self.args = None
+ a = A()
+ weak_a = weakref.ref(a)
+
+ foo = Callback()
+ foo.Register(a.on_foo)
+
+ foo(1, 2)
+ assert self.args == (1, 2)
+ assert weak_a() is a
+
+ foo(3, 4)
+ assert self.args == (3, 4)
+ assert weak_a() is a
+
+ del a
+ assert weak_a() is None
+ foo(5, 6)
+ assert self.args == (3, 4)
+
+ def testActionMethodDies(self):
+
+ class A:
+
+ def foo(self):
+ pass
+
+ def FooAfter():
+ self.after_exec += 1
+
+ self.after_exec = 0
+
+ a = A()
+ weak_a = weakref.ref(a)
+ After(a.foo, FooAfter)
+ a.foo()
+
+ assert self.after_exec == 1
+
+ del a
+
+ # IMPORTANT: behaviour change. The description below is for the previous
+ # behaviour. That is not true anymore (the circular reference is not kept anymore)
+
+ # callback creates a circular reference; that's ok, because we want
+ # to still be able to do "x = a.foo" and keep a strong reference to it
+
+ assert weak_a() is None
+
+ def testAfterRegisterMultipleAndUnregisterOnce(self):
+
+ class A:
+
+ def foo(self):
+ pass
+
+ a = A()
+
+ def FooAfter1():
+ Remove(a.foo, FooAfter1)
+ self.after_exec += 1
+
+ def FooAfter2():
+ self.after_exec += 1
+
+ self.after_exec = 0
+ After(a.foo, FooAfter1)
+ After(a.foo, FooAfter2)
+ a.foo()
+
+ # it was iterating in the original after, so, this case
+ # was only giving 1 result and not 2 as it should
+ assert 2 == self.after_exec
+
+ a.foo()
+ assert 3 == self.after_exec
+ a.foo()
+ assert 4 == self.after_exec
+
+ After(a.foo, FooAfter2)
+ After(a.foo, FooAfter2)
+ After(a.foo, FooAfter2)
+
+ a.foo()
+ assert 5 == self.after_exec
+
+ Remove(a.foo, FooAfter2)
+ a.foo()
+ assert 5 == self.after_exec
+
+ def testOnClassMethod(self):
+
+ class A:
+
+ @classmethod
+ def foo(cls):
+ pass
+
+ self.after_exec_class_method = 0
+
+ def FooAfterClassMethod():
+ self.after_exec_class_method += 1
+
+ self.after_exec_self_method = 0
+
+ def FooAfterSelfMethod():
+ self.after_exec_self_method += 1
+
+ After(A.foo, FooAfterClassMethod)
+
+ a = A()
+ After(a.foo, FooAfterSelfMethod)
+
+ a.foo()
+ assert 1 == self.after_exec_class_method
+ assert 1 == self.after_exec_self_method
+
+ Remove(A.foo, FooAfterClassMethod)
+ a.foo()
+ assert 1 == self.after_exec_class_method
+ assert 2 == self.after_exec_self_method
+
+ def testSenderDies2(self):
+ After(self.a.foo, self.after, True)
+ self.a.foo(1)
+ assert (self.a, 1) == self.after_called
+
+ a = weakref.ref(self.a)
+ self.after_called = None
+ self.foo_called = None
+ del self.a
+ assert a() is None
+
+ def testCallbacks(self):
+ self.called = 0
+
+ def bar(*args):
+ self.called += 1
+
+ callbacks = Callbacks()
+ callbacks.Before(self.a.foo, bar)
+ callbacks.After(self.a.foo, bar)
+
+ self.a.foo(1)
+ assert 2 == self.called
+ callbacks.RemoveAll()
+ self.a.foo(1)
+ assert 2 == self.called
+
+ def testAfterRemove(self):
+
+ my_object = _MyClass()
+ my_object.SetAlpha(0)
+ my_object.SetBravo(0)
+
+ After(my_object.SetAlpha, my_object.SetBravo)
+
+ my_object.SetAlpha(1)
+ assert my_object.bravo == 1
+
+ Remove(my_object.SetAlpha, my_object.SetBravo)
+
+ my_object.SetAlpha(2)
+ assert my_object.bravo == 1
+
+ def testAfterRemoveCallback(self):
+ my_object = _MyClass()
+ my_object.SetAlpha(0)
+ my_object.SetBravo(0)
+
+ # Test After/Remove with a callback
+ event = Callback()
+ After(my_object.SetAlpha, event)
+ event.Register(my_object.SetBravo)
+
+ my_object.SetAlpha(3)
+ assert my_object.bravo == 3
+
+ Remove(my_object.SetAlpha, event)
+
+ my_object.SetAlpha(4)
+ assert my_object.bravo == 3
+
+ def testAfterRemoveCallbackAndSenderAsParameter(self):
+ my_object = _MyClass()
+ my_object.SetAlpha(0)
+ my_object.SetBravo(0)
+
+ def event(obj_or_value, value):
+ self._value = value
+
+ # Test After/Remove with a callback and sender_as_parameter
+ After(my_object.SetAlpha, event, sender_as_parameter=True)
+
+ my_object.SetAlpha(3)
+
+ assert 3 == self._value
+
+ Remove(my_object.SetAlpha, event)
+
+ my_object.SetAlpha(4)
+ assert 3 == self._value
+
+ def testDeadCallbackCleared(self):
+ my_object = _MyClass()
+ my_object.SetAlpha(0)
+ my_object.SetBravo(0)
+ self._value = []
+
+ class B:
+
+ def event(s, value): # @NoSelf
+ self._b_value = value
+
+ class A:
+
+ def event(s, obj, value): # @NoSelf
+ self._a_value = value
+
+ a = A()
+ b = B()
+
+ # Test After/Remove with a callback and sender_as_parameter
+ After(my_object.SetAlpha, a.event, sender_as_parameter=True)
+ After(my_object.SetAlpha, b.event, sender_as_parameter=False)
+
+ w = weakref.ref(a)
+ my_object.SetAlpha(3)
+ assert 3 == self._a_value
+ assert 3 == self._b_value
+ del a
+ my_object.SetAlpha(4)
+ assert 3 == self._a_value
+ assert 4 == self._b_value
+ assert w() is None
+
+ def testRemoveCallback(self):
+
+ class C:
+
+ def __init__(self, name):
+ self.name = name
+
+ def OnCallback(self):
+ pass
+
+ def __eq__(self, other):
+ return self.name == other.name
+
+ def __ne__(self, other):
+ return not self == other
+
+ instance1 = C('instance')
+ instance2 = C('instance')
+ assert instance1 == instance2
+
+ c = Callback()
+ c.Register(instance1.OnCallback)
+ c.Register(instance2.OnCallback)
+
+ # removing first callback, and checking that it was actually removed as expected
+ c.Unregister(instance1.OnCallback)
+ assert not c.Contains(instance1.OnCallback) == True
+
+ # self.assertNotRaises(RuntimeError, c.Unregister, instance1.OnCallback)
+ c.Unregister(instance1.OnCallback)
+
+ # removing second callback, and checking that it was actually removed as expected
+ c.Unregister(instance2.OnCallback)
+ assert not c.Contains(instance2.OnCallback) == True
+
+ # self.assertNotRaises(RuntimeError, c.Unregister, instance2.OnCallback)
+ c.Unregister(instance2.OnCallback)
+
+ def testRegisterTwice(self):
+ self.called = 0
+
+ def After(*args):
+ self.called += 1
+
+ c = Callback()
+ c.Register(After)
+ c.Register(After)
+ c.Register(After)
+ c()
+ assert self.called == 1
+
+ def testHandleErrorOnCallback(self, mocker):
+ old_default_handle_errors = Callback.DEFAULT_HANDLE_ERRORS
+ Callback.DEFAULT_HANDLE_ERRORS = False
+ try:
+
+ self.called = 0
+
+ def After(*args, **kwargs):
+ self.called += 1
+ raise RuntimeError('test')
+
+ def After2(*args, **kwargs):
+ self.called += 1
+ raise RuntimeError('test2')
+
+ c = Callback(handle_errors=True)
+ c.Register(After)
+ c.Register(After2)
+
+ mocked = mocker.patch('oop_ext.foundation.handle_exception.HandleException', autospec=True)
+ c()
+ assert self.called == 2
+ assert mocked.call_count == 2
+
+ mocked.reset_mock()
+ c(1, a=2)
+ assert self.called == 4
+ assert mocked.call_count == 2
+ from _pytest.pytester import LineMatcher
+ matcher = LineMatcher(mocked.call_args[0][0].splitlines())
+ expected = [
+ 'Error while trying to call .After2 at 0x*>',
+ 'Args: (1,)',
+ "Kwargs: {'a': 2}",
+ ]
+ matcher.fnmatch_lines(expected)
+
+ # test the default behaviour: errors are not handled and stop execution as usual
+ self.called = 0
+ c = Callback()
+ c.Register(After)
+ c.Register(After2)
+ with pytest.raises(RuntimeError):
+ c()
+ assert self.called == 1
+ finally:
+ Callback.DEFAULT_HANDLE_ERRORS = old_default_handle_errors
+
+ def testAfterBeforeHandleError(self, mocker):
+
+ class C:
+
+ def Method(self, x):
+ return x * 2
+
+ def AfterMethod(*args):
+ self.before_called += 1
+ raise RuntimeError
+
+ def BeforeMethod(*args):
+ self.after_called += 1
+ raise RuntimeError
+
+ self.before_called = 0
+ self.after_called = 0
+
+ c = C()
+ Before(c.Method, BeforeMethod)
+ After(c.Method, AfterMethod)
+
+ mocked = mocker.patch('oop_ext.foundation.callback.HandleErrorOnCallback', autospec=True)
+ assert c.Method(10) == 20
+ assert self.before_called == 1
+ assert self.after_called == 1
+ assert mocked.call_count == 2
+
+ mocked.reset_mock()
+ assert c.Method(20) == 40
+ assert self.before_called == 2
+ assert self.after_called == 2
+
+ def testKeyReusedAfterDead(self, monkeypatch):
+ self._gotten_key = False
+
+ def GetKey(*args, **kwargs):
+ self._gotten_key = True
+ return 1
+
+ monkeypatch.setattr(Callback, '_GetKey', GetKey)
+
+ def AfterMethod(*args):
+ pass
+
+ def AfterMethodB(*args):
+ pass
+
+ c = Callback()
+
+ c.Register(AfterMethod)
+ self._gotten_key = False
+ assert not c.Contains(AfterMethodB)
+ assert c.Contains(AfterMethod)
+ assert self._gotten_key
+
+ # As we made _GetKey return always the same, this will make it remove one and add the
+ # other one, so, the contains will have to check if they're actually the same or not.
+ c.Register(AfterMethodB)
+ self._gotten_key = False
+ assert c.Contains(AfterMethodB)
+ assert not c.Contains(AfterMethod)
+ assert self._gotten_key
+
+ class A:
+
+ def __init__(self):
+ self._a = 0
+
+ def GetA(self):
+ return self._a
+
+ def SetA(self, value):
+ self._a = value
+
+ a = property(GetA, SetA)
+
+ a = A()
+ # If registering a bound, it doesn't contain the unbound
+ c.Register(a.SetA)
+ assert not c.Contains(AfterMethodB)
+ assert not c.Contains(A.SetA)
+ assert c.Contains(a.SetA)
+
+ # But if registering an unbound, it contains the bound
+ c.Register(A.SetA)
+ assert not c.Contains(AfterMethodB)
+ assert c.Contains(A.SetA)
+ assert c.Contains(a.SetA)
+
+ c.Register(a.SetA)
+ assert len(c) == 1
+ del a
+ assert not c.Contains(AfterMethodB)
+ assert len(c) == 0
+
+ a = A()
+ from oop_ext.foundation.callback._callback_wrapper import _CallbackWrapper
+ c.Register(_CallbackWrapper(WeakMethodRef(a.SetA)))
+ assert len(c) == 1
+ del a
+ assert not c.Contains(AfterMethodB)
+ assert len(c) == 0
+
+ def testNeedsUnregister(self):
+ c = Callback()
+
+ # Even when the function isn't registered, we not raise an error.
+ def Func():
+ pass
+
+ # self.assertNotRaises(RuntimeError, c.Unregister, Func)
+ c.Unregister(Func)
+
+ def testUnregisterAll(self):
+ c = Callback()
+
+ # self.assertNotRaises(AttributeError, c.UnregisterAll)
+ c.UnregisterAll()
+
+ self.called = False
+
+ def Func():
+ self.called = True
+
+ c.Register(Func)
+ c.UnregisterAll()
+
+ c()
+ assert self.called == False
+
+ def testOnClassAndOnInstance1(self):
+ vals = []
+
+ def OnCall1(instance, val):
+ vals.append(('call_instance', val))
+
+ def OnCall2(val):
+ vals.append(('call_class', val))
+
+ After(Stub.call, OnCall1)
+ s = Stub()
+ After(s.call, OnCall2)
+
+ s.call(True)
+ assert [('call_instance', True), ('call_class', True)] == vals
+
+ def testOnClassAndOnInstance2(self):
+ vals = []
+
+ def OnCall1(instance, val):
+ vals.append(('call_class', val))
+
+ def OnCall2(val):
+ vals.append(('call_instance', val))
+
+ s = Stub()
+ After(s.call, OnCall2)
+ After(Stub.call, OnCall1)
+
+ # Tricky thing here: because we added the callback in the class after we added it to the
+ # instance, the callback on the instance cannot be rebound, thus, calling it on the instance
+ # won't really trigger the callback on the class (not really what would be expected of the
+ # after method, but I couldn't find a reasonable way to overcome that).
+ # A solution could be keeping track of all callbacks and rebinding all existent ones in the
+ # instances to the one in the class, but it seems overkill for such an odd situation.
+ s.call(True)
+ assert [('call_instance', True), ] == vals
+
+ def testOnNullClass(self):
+
+ class _MyNullSubClass(Null):
+
+ def GetIstodraw(self):
+ return True
+
+ s = _MyNullSubClass()
+
+ def AfterSetIstodraw():
+ pass
+
+ After(s.SetIstodraw, AfterSetIstodraw)
+
+ def testListMethodAsCallback(self, mocker):
+ '''
+ This was based on a failure on
+ souring20.core.model.multiple_simulation_runs._tests.test_multiple_simulation_runs.testNormalExecution
+ which failed with "TypeError: cannot create weak reference to 'list' object"
+ '''
+ vals = []
+
+ class Stub:
+
+ def call(self, *args, **kwargs):
+ pass
+
+ s = Stub()
+ After(s.call, vals.append)
+
+ s.call('call_append')
+ assert ['call_append'] == vals
+
+ def testCallbackWithMagicMock(self, mocker):
+ """
+ Check that we can register mock.MagicMock objects in callbacks.
+
+ This makes it easier to test that public callbacks are being called with correct arguments.
+
+ Usage (in testing, of course):
+
+ save_listener = mock.MagicMock(spec=lambda: None)
+ project_manager.on_save.Register(save_listener)
+ project_manager.SlotSave()
+ assert save_listener.call_args == mock.call('foo.file', '.txt')
+
+ Instead of the more traditional:
+
+ def OnSave(filename, ext):
+ self.filename = filename
+ self.ext = ext
+
+ self.filename = None
+ self.ext = ext
+
+ project_manager.on_save.Register(OnSave)
+ project_manager.SlotSave()
+ assert (self.filename, self.ext) == ('foo.file', '.txt')
+ """
+ c = Callback()
+
+ with pytest.raises(RuntimeError):
+ c.Register(mocker.MagicMock())
+
+ magic_mock = mocker.stub()
+ c = Callback()
+ c.Register(magic_mock)
+
+ c(10, name='X')
+ assert magic_mock.call_args_list == [mocker.call(10, name='X')]
+
+ c(20, name='Y')
+ assert magic_mock.call_args_list == [mocker.call(10, name='X'), mocker.call(20, name='Y')]
+
+ c.Unregister(magic_mock)
+ c(30, name='Z')
+ assert len(magic_mock.call_args_list) == 2
+
+ def testCallbackInstanceWeakRef(self):
+
+ class Obj:
+
+ def __init__(self):
+ self.called = False
+
+ def __call__(self):
+ self.called = True
+
+ c = Callback()
+ obj = Obj()
+ c.Register(obj)
+ c()
+ assert c.Contains(obj)
+ assert obj.called
+ obj = weakref.ref(obj)
+ assert obj() is None
+
+ def testBeforeAfterWeakProxy(self):
+
+ class Foo:
+
+ def __init__(self):
+ Before(self.SetFilename, GetWeakProxy(self._BeforeSetFilename))
+ After(self.SetFilename, GetWeakProxy(self._AfterSetFilename))
+ self.before = False
+ self.after = False
+
+ def _BeforeSetFilename(self, *args, **kwargs):
+ self.before = True
+
+ def _AfterSetFilename(self, *args, **kwargs):
+ self.after = True
+
+ def SetFilename(self, f):
+ pass
+
+ foo = Foo()
+ foo.SetFilename('bar')
+ assert foo.before
+ assert foo.after
+
+ def testKeepStrongReference(self):
+
+ class Obj:
+ __CALLBACK_KEEP_STRONG_REFERENCE__ = True
+
+ def __init__(self):
+ self.called = False
+
+ def __call__(self):
+ self.called = True
+
+ c = Callback()
+ obj = Obj()
+ c.Register(obj)
+ c()
+ assert c.Contains(obj)
+ assert obj.called
+ obj = weakref.ref(obj)
+ # Not collected because of __CALLBACK_KEEP_STRONG_REFERENCE__ in the class.
+ assert obj() is not None
+
+ def testWeakMethodProxy(self):
+
+ class Obj:
+
+ def Foo(self):
+ self.called = True
+
+ obj = Obj()
+ proxy = WeakMethodProxy(obj.Foo)
+
+ c = Callback()
+ c.Register(proxy)
+ c()
+ assert obj.called
+ assert c.Contains(proxy)
+ obj = weakref.ref(obj)
+ assert obj() is None
+ c()
+ assert len(c) == 0
+
+ def testWeakMethodProxy2(self):
+
+ def Foo():
+ self.called = True
+
+ proxy = WeakMethodProxy(Foo)
+
+ c = Callback()
+ c.Register(proxy)
+ c()
+ assert self.called
+ assert c.Contains(proxy)
+
+ def testWeakRefToCallback(self):
+ c = Callback()
+ c_ref = weakref.ref(c)
+ assert c_ref() is c
+
+ def testCallbackAndPartial(self):
+ from functools import partial
+ called = []
+
+ def Method(a):
+ called.append(a)
+
+ c = Callback()
+ c.Register(lambda: Method('lambda'))
+ c.Register(partial(Method, 'partial'))
+ c()
+ assert called == ['lambda', 'partial']
diff --git a/src/oop_ext/foundation/callback/_tests/test_priority_callback.py b/src/oop_ext/foundation/callback/_tests/test_priority_callback.py
new file mode 100644
index 0000000..043f3d3
--- /dev/null
+++ b/src/oop_ext/foundation/callback/_tests/test_priority_callback.py
@@ -0,0 +1,32 @@
+
+from oop_ext.foundation.callback import PriorityCallback
+
+
+def testPriorityCallback():
+ priority_callback = PriorityCallback()
+
+ called = []
+
+ def OnCall1():
+ called.append(1)
+
+ def OnCall2():
+ called.append(2)
+
+ def OnCall3():
+ called.append(3)
+
+ def OnCall4():
+ called.append(4)
+
+ def OnCall5():
+ called.append(5)
+
+ priority_callback.Register(OnCall1, priority=2)
+ priority_callback.Register(OnCall2, priority=2)
+ priority_callback.Register(OnCall3, priority=1)
+ priority_callback.Register(OnCall4, priority=3)
+ priority_callback.Register(OnCall5, priority=2)
+
+ priority_callback()
+ assert called == [3, 1, 2, 5, 4]
diff --git a/src/oop_ext/foundation/callback/_tests/test_single_call_callback.py b/src/oop_ext/foundation/callback/_tests/test_single_call_callback.py
new file mode 100644
index 0000000..b03ca08
--- /dev/null
+++ b/src/oop_ext/foundation/callback/_tests/test_single_call_callback.py
@@ -0,0 +1,90 @@
+
+import pytest
+
+from oop_ext.foundation.callback.single_call_callback import SingleCallCallback
+
+
+def testSingleCallCallback():
+
+ class Stub:
+ pass
+
+ stub = Stub()
+ callback = SingleCallCallback(stub)
+
+ called = []
+
+ def Method1(arg):
+ called.append(arg)
+
+ def Method2(arg):
+ called.append(arg)
+
+ def Method3(arg):
+ called.append(arg)
+
+ callback.Register(Method1)
+
+ callback()
+
+ assert called == [stub]
+
+ with pytest.raises(AssertionError):
+ callback()
+
+ assert called == [stub]
+
+ callback.Register(Method1) # It was already there, so, won't be called again.
+
+ assert called == [stub]
+
+ callback.Register(Method2)
+
+ assert called == [stub, stub]
+
+ del stub
+ del called[:]
+
+ with pytest.raises(ReferenceError):
+ callback.Register(Method1)
+
+
+def testSingleCallCallbackNoParameter():
+
+ class Stub:
+ pass
+
+ callback = SingleCallCallback(None)
+
+ called = []
+
+ def Method1():
+ called.append('Method1')
+
+ def Method2():
+ called.append('Method2')
+
+ callback.Register(Method1)
+
+ callback()
+
+ assert called == ['Method1']
+
+ callback.Register(Method2)
+
+ assert called == ['Method1', 'Method2']
+
+ with pytest.raises(AssertionError):
+ callback()
+
+ callback.AllowCallingAgain()
+ callback()
+
+ assert called == ['Method1', 'Method2', 'Method1', 'Method2']
+
+ callback.Register(Method1)
+ assert called == ['Method1', 'Method2', 'Method1', 'Method2']
+
+ callback.Unregister(Method1)
+ callback.Register(Method1)
+ assert called == ['Method1', 'Method2', 'Method1', 'Method2', 'Method1']
diff --git a/src/oop_ext/foundation/callback/single_call_callback.py b/src/oop_ext/foundation/callback/single_call_callback.py
new file mode 100644
index 0000000..1233db4
--- /dev/null
+++ b/src/oop_ext/foundation/callback/single_call_callback.py
@@ -0,0 +1,86 @@
+
+from ._fast_callback import Callback
+
+
+#===================================================================================================
+# SingleCallCallback
+#===================================================================================================
+class SingleCallCallback:
+ '''
+ Callback-like implementation used for a callback to which __call__ is called only once (and
+ subsequent calls will always trigger the same callback).
+
+ The callback parameter is pre-registered and kept as a weak-reference.
+ '''
+
+ def __init__(self, callback_parameter):
+ '''
+ :param object callback_parameter:
+ A weak-reference is kept to this object (because the usual use-case is making a call
+ passing the object that contains this callback).
+ '''
+ from oop_ext.foundation.weak_ref import GetWeakRef
+ if callback_parameter is None:
+ self._callback_parameter = None
+ else:
+ self._callback_parameter = GetWeakRef(callback_parameter)
+ self._done_callbacks = Callback()
+ self._done = False
+
+ self._args = ()
+ self._kwargs = {}
+
+ def __call__(self, *args, **kwargs):
+ if self._done:
+ raise AssertionError('This callback can only be called once.')
+
+ # Keep the args passed to call it later on...
+ self._args = args
+ self._kwargs = kwargs
+
+ if self._callback_parameter is not None:
+ callback_parameter = self._callback_parameter()
+ if callback_parameter is None:
+ raise ReferenceError('Callback parameter is already garbage collected.')
+ else:
+ callback_parameter = None
+
+ # We can dispose of it (as of now, callbacks should be called directly).
+ self._done = True
+ if callback_parameter is not None:
+ self._done_callbacks(callback_parameter, *args, **kwargs)
+ else:
+ self._done_callbacks(*args, **kwargs)
+
+ def Unregister(self, fn):
+ self._done_callbacks.Unregister(fn)
+
+ def UnregisterAll(self):
+ self._done_callbacks.UnregisterAll()
+
+ def Register(self, fn):
+ if self._callback_parameter is not None:
+ callback_parameter = self._callback_parameter()
+ if callback_parameter is None:
+ raise ReferenceError('Callback parameter is already garbage collected.')
+ else:
+ callback_parameter = None
+
+ contains = self._done_callbacks.Contains(fn)
+
+ self._done_callbacks.Register(fn)
+ if self._done and not contains:
+ if callback_parameter is not None:
+ fn(callback_parameter, *self._args, **self._kwargs)
+ else:
+ fn(*self._args, **self._kwargs)
+
+ def AllowCallingAgain(self):
+ '''
+ This callback is usually called only once, afterwards, any registry will call it directly
+ (and the callback cannot be called anymore).
+
+ By calling this method, we allow calling this callback again (and stop directly notifying
+ clients just registered until it's called again).
+ '''
+ self._done = False
diff --git a/src/oop_ext/foundation/compat.py b/src/oop_ext/foundation/compat.py
new file mode 100644
index 0000000..23bac6e
--- /dev/null
+++ b/src/oop_ext/foundation/compat.py
@@ -0,0 +1,33 @@
+'''
+A compatibility module for quirks when porting from py2->py3.
+'''
+
+
+def __str__(self):
+ return self.__unicode__().encode('utf-8')
+
+
+def GetClassForUnboundMethod(method):
+ """
+ On Python 3 there are no unbound methods anymore. They are only regular functions.
+
+ This function abstracts that difference and implements a workaround for Python 3.
+
+ However this has a drawback: callback to method of local classes AREN'T SUPPORTED anymore,
+ as it is impossible to retrieve their class object just by method object alone.
+ """
+ locals_name = ''
+
+ # Find the class which this method belongs too. We need this because on Python 3, unbound
+ # methods are just regular functions with no reference to its class
+ names = method.__qualname__.split('.')
+ names.pop()
+ method_class = method.__globals__[names.pop(0)]
+ while names:
+ name = names.pop(0)
+ if name == locals_name:
+ raise NotImplementedError("Impossible to retrieve class object for "
+ "unbound methods in local classes.")
+
+ method_class = getattr(method_class, name)
+ return method_class
diff --git a/src/oop_ext/foundation/decorators.py b/src/oop_ext/foundation/decorators.py
new file mode 100644
index 0000000..f73d494
--- /dev/null
+++ b/src/oop_ext/foundation/decorators.py
@@ -0,0 +1,188 @@
+
+import warnings
+
+from oop_ext.foundation.is_frozen import IsDevelopment
+
+'''
+Collection of decorator with ONLY standard library dependencies.
+'''
+
+
+#===================================================================================================
+# Override
+#===================================================================================================
+def Override(method):
+ '''
+ Decorator that marks that a method overrides a method in the superclass.
+
+ :param type method:
+ The overridden method
+
+ :returns function:
+ The decorated function
+
+ .. note:: This decorator actually works by only making the user to access the class and the overridden method at
+ class level scope, so if in the future that method gets deleted or renamed, the import of the decorated method will
+ fail.
+
+ Example::
+
+ class MyInterface:
+ def foo():
+ pass
+
+ class MyClass(MyInterface):
+
+ @Overrides(MyInterace.foo)
+ def foo():
+ pass
+ '''
+
+ def Wrapper(func):
+ if func.__name__ != method.__name__:
+ msg = "Wrong @Override: %r expected, but overwriting %r."
+ msg = msg % (func.__name__, method.__name__)
+ raise AssertionError(msg)
+
+ if func.__doc__ is None:
+ func.__doc__ = method.__doc__
+
+ return func
+
+ return Wrapper
+
+
+#===================================================================================================
+# Implements
+#===================================================================================================
+def Implements(method):
+ '''
+ Decorator that marks that a method implements a method in some interface.
+
+ :param function method:
+ The implemented method
+
+ :returns function:
+ The decorated function
+
+ :raises AssertionError:
+ if the implementation method's name is different from the one
+ that is being defined. This is a common error when copying/pasting the @Implements code.
+
+ .. note:: This decorator actually works by only making the user to access the class and the implemented method at
+ class level scope, so if in the future that method gets deleted or renamed, the import of the decorated method will
+ fail.
+
+ Example::
+
+ class MyInterface:
+ def foo():
+ pass
+
+ class MyClass(MyInterface):
+
+ @Implements(MyInterace.foo)
+ def foo():
+ pass
+ '''
+
+ def Wrapper(func):
+ if func.__name__ != method.__name__:
+ msg = "Wrong @Implements: %r expected, but overwriting %r."
+ msg = msg % (func.__name__, method.__name__)
+ raise AssertionError(msg)
+
+ if func.__doc__ is None:
+ func.__doc__ = method.__doc__
+
+ return func
+
+ return Wrapper
+
+
+#===================================================================================================
+# Deprecated
+#===================================================================================================
+def Deprecated(name=None):
+ '''
+ Decorator that marks a method as deprecated.
+
+ :param str name:
+ The name of the method that substitutes this one, if any.
+ '''
+ if not IsDevelopment():
+ # Optimization: we don't want deprecated to add overhead in release mode.
+
+ def DeprecatedDecorator(func):
+ return func
+
+ else:
+
+ def DeprecatedDecorator(func):
+ '''
+ The actual deprecated decorator, configured with the name parameter.
+ '''
+
+ def DeprecatedWrapper(*args, **kwargs):
+ '''
+ This method wrapper gives a deprecated message before calling the original
+ implementation.
+ '''
+ if name is not None:
+ msg = 'DEPRECATED: \'%s\' is deprecated, use \'%s\' instead' % (func.__name__, name)
+ else:
+ msg = 'DEPRECATED: \'%s\' is deprecated' % func.__name__
+ warnings.warn(msg, stacklevel=2)
+ return func(*args, **kwargs)
+
+ DeprecatedWrapper.__name__ = func.__name__
+ DeprecatedWrapper.__doc__ = func.__doc__
+ return DeprecatedWrapper
+
+ return DeprecatedDecorator
+
+
+#===================================================================================================
+# Abstract
+#===================================================================================================
+def Abstract(func):
+ '''
+ Decorator to make methods 'abstract', which are meant to be overwritten in subclasses. If some
+ subclass doesn't override the method, it will raise NotImplementedError when called. Note that
+ this decorator should be used together with :dec:Override.
+
+ Example::
+
+ class Base(object):
+
+ @Abstract
+ def Foo(self):
+ """
+ This method ...
+ """
+ # no body required here; an exception will be raised automatically
+
+
+ class Derived(Base):
+
+ @Override(Base.Foo)
+ def Foo(self):
+ ...
+
+ '''
+
+ def AbstractWrapper(self, *args, **kwargs):
+ '''
+ This wrapper method replaces the implementation of the (abstract) method, providing a
+ friendly message to the user.
+ '''
+ # # Unused argument args, kwargs
+ # # pylint: disable-msg=W0613
+ msg = 'method {} not implemented in class {}.'.format(repr(func.__name__), repr(self.__class__))
+ raise NotImplementedError(msg)
+
+ # # Redefining build-in
+ # # pylint: disable-msg=W0622
+ AbstractWrapper.__name__ = func.__name__
+ AbstractWrapper.__doc__ = func.__doc__
+ return AbstractWrapper
diff --git a/src/oop_ext/foundation/exceptions.py b/src/oop_ext/foundation/exceptions.py
new file mode 100644
index 0000000..b61d576
--- /dev/null
+++ b/src/oop_ext/foundation/exceptions.py
@@ -0,0 +1,17 @@
+
+
+#===================================================================================================
+# ExceptionToUnicode
+#===================================================================================================
+def ExceptionToUnicode(exception):
+ """
+ Python 3 exception handling already deals with string error messages. Here we
+ will only append the original exception message to the returned message (this is automatically done in Python 2
+ since the original exception message is added into the new exception while Python 3 keeps the original exception
+ as a separated attribute
+ """
+ messages = []
+ while exception:
+ messages.append(str(exception))
+ exception = exception.__cause__ or exception.__context__
+ return '\n'.join(messages)
diff --git a/src/oop_ext/foundation/handle_exception.py b/src/oop_ext/foundation/handle_exception.py
new file mode 100644
index 0000000..35f91a0
--- /dev/null
+++ b/src/oop_ext/foundation/handle_exception.py
@@ -0,0 +1,81 @@
+
+import logging
+from contextlib import contextmanager
+
+from oop_ext.foundation.callback import Callback
+
+'''
+This module contains utility functions for dealing with exceptions that should be handled in the
+application.
+'''
+
+# Callback for clients that want to hear about handled exceptions.
+on_exception_handled = Callback()
+
+_ignore_expected_exception = 0
+
+# hack because some tests need this
+LOG_ERRORS = True
+
+
+#===================================================================================================
+# StartIgnoreHandleException
+#===================================================================================================
+def StartIgnoreHandleException():
+ '''
+ Starts ignoring any handled exception (will still call the on_exception_handled(), but
+ won't log nor call the excepthook).
+ '''
+ global _ignore_expected_exception
+ _ignore_expected_exception += 1
+
+
+#===================================================================================================
+# EndIgnoreHandleException
+#===================================================================================================
+def EndIgnoreHandleException():
+ '''
+ Stops ignoring handled exceptions.
+ '''
+ global _ignore_expected_exception
+ _ignore_expected_exception -= 1
+
+
+#===================================================================================================
+# IgnoringHandleException
+#===================================================================================================
+@contextmanager
+def IgnoringHandleException():
+ '''
+ Ignores exception handling during context.
+ '''
+ StartIgnoreHandleException()
+ yield
+ EndIgnoreHandleException()
+
+
+#===================================================================================================
+# HandleException
+#===================================================================================================
+def HandleException(msg=''):
+ '''
+ Handles the current exception (in sys.exc_info()) without actually continuing its raise.
+
+ It should be used when for some reason the current exception should not stop the current
+ execution flow but should still be shown to the user and reported accordingly.
+ '''
+ # Let listeners know about the exception (usually the test case)
+ # TODO: if handle error has any bug in this callback, it calls handle error again in a loop
+ on_exception_handled()
+
+ if _ignore_expected_exception > 0:
+ return # Don't log nor call excepthook.
+
+ if LOG_ERRORS:
+ logger = logging.getLogger(__name__)
+ logger.exception(msg)
+
+ # And just call the except hook (and keep on going with the calls) -- in the application
+ # this should trigger the except dialog.
+ import sys
+ sys.excepthook(*sys.exc_info())
diff --git a/src/oop_ext/foundation/immutable.py b/src/oop_ext/foundation/immutable.py
new file mode 100644
index 0000000..81165e6
--- /dev/null
+++ b/src/oop_ext/foundation/immutable.py
@@ -0,0 +1,208 @@
+
+'''
+ Defines types and functions to generate immutable structures.
+
+ USER: The cache-manager uses this module to generate a valid KEY for its cache dictionary.
+'''
+
+_IMMUTABLE_TYPES = {float, str, bytes, bool, type(None)}
+_IMMUTABLE_TYPES.update({int})
+
+
+#===================================================================================================
+# RegisterAsImmutable
+#===================================================================================================
+def RegisterAsImmutable(immutable_type):
+ '''
+ Registers the given class as being immutable. This makes it be immutable for this module and
+ also registers a faster copy in the copy module (to return the same instance being copied).
+
+ :param type immutable_type:
+ The type to be considered immutable.
+ '''
+ _IMMUTABLE_TYPES.add(immutable_type)
+
+ # Fix it for the copy too!
+ import copy
+ copy._copy_dispatch[immutable_type] = copy._copy_immutable
+
+
+#===================================================================================================
+# AsImmutable
+#===================================================================================================
+def AsImmutable(value, return_str_if_not_expected=True):
+ '''
+ Returns the given instance as a immutable object:
+ - Converts lists to tuples
+ - Converts dicts to ImmutableDicts
+ - Converts other objects to str
+ - Does not convert basic types (int/float/str/bool)
+
+ :param object value:
+ The value to be returned as an immutable value
+
+ :param bool return_str_if_not_expected:
+ If True, a string representation of the object will be returned if we're unable to match the
+ type as a known type (otherwise, an error is thrown if we cannot handle the passed type).
+
+ :rtype: object
+ :returns:
+ Returns an immutable representation of the passed object
+ '''
+
+ # Micro-optimization (a 40% improvement on the AsImmutable function overall in a real case
+ # using sci20 processes).
+ # Checking the type of the class before going to the isinstance series and added
+ # SemanticAssociation as an immutable object.
+ value_class = value.__class__
+
+ if value_class in _IMMUTABLE_TYPES:
+ return value
+
+ if value_class == dict:
+ return ImmutableDict((i, AsImmutable(j)) for i, j in value.items())
+
+ if value_class in (tuple, list):
+ return tuple(AsImmutable(i) for i in value)
+
+ if value_class in (set, frozenset):
+ return frozenset(value)
+
+ # Now, on to the isinstance series...
+ if isinstance(value, int):
+ return value
+
+ if isinstance(value, (float, str, bytes, bool)):
+ return value
+
+ if isinstance(value, dict):
+ return ImmutableDict((i, AsImmutable(j)) for i, j in value.items())
+
+ if isinstance(value, (tuple, list)):
+ return tuple(AsImmutable(i) for i in value)
+
+ if isinstance(value, (set, frozenset)):
+ return frozenset(value)
+
+ if return_str_if_not_expected:
+ return str(value)
+
+ else:
+ raise RuntimeError('Cannot make %s immutable (not supported).' % value)
+
+
+#===================================================================================================
+# ImmutableDict
+#===================================================================================================
+class ImmutableDict(dict):
+ '''A hashable dict.'''
+
+ def __init__(self, *args, **kwds):
+ dict.__init__(self, *args, **kwds)
+
+ def __deepcopy__(self, memo):
+ return self # it's immutable, so, there's no real need to make any copy
+
+ def __setitem__(self, key, value):
+ raise NotImplementedError("dict is immutable")
+
+ def __delitem__(self, key):
+ raise NotImplementedError("dict is immutable")
+
+ def clear(self):
+ raise NotImplementedError("dict is immutable")
+
+ def setdefault(self, k, default=None):
+ raise NotImplementedError("dict is immutable")
+
+ def popitem(self):
+ raise NotImplementedError("dict is immutable")
+
+ def update(self, other):
+ raise NotImplementedError("dict is immutable")
+
+ def __hash__(self):
+ if not hasattr(self, '_hash'):
+ # must be sorted (could give different results for dicts that should be the same
+ # if it's not).
+ self._hash = hash(tuple(sorted(self.items())))
+
+ return self._hash
+
+ def AsMutable(self):
+ '''
+ :rtype: this dict as a new dict that can be changed (without altering the state
+ of this immutable dict).
+ '''
+ return dict(self.items())
+
+ def __reduce__(self):
+ """
+ Making ImmutableDict work with newer versions of pickle protocol.
+
+ Without this, it uses the default behavior on loading which tries to create an empty dict
+ and then set its items, which is not an allowed operation on ImmutableDict.
+
+ In general, there are higher level functions to be redefined for pickle customization, but
+ for dict subclasses we need to define __reduce__ method. For more details of this special
+ case, see __reduce__ in the referenced docs (links below).
+
+ See also:
+ - https://docs.python.org/2/library/pickle.html#pickling-and-unpickling-extension-types
+ - https://docs.python.org/3/library/pickle.html#pickling-class-instances
+
+ :return tuple:
+ (Callable, tuple of arguments). See __reduce__ docs for more details.
+ """
+ return (ImmutableDict, (list(self.items()),))
+
+
+#===================================================================================================
+# IdentityHashableRef
+#===================================================================================================
+class IdentityHashableRef:
+ '''
+ Represents a immutable reference to an object.
+
+ Useful when is desired to use some mutable object as key in a dict or element in a set.
+ Any form of overwriting the `__hash__`, `__eq__`, or `__ne__` in the original object is ignored
+ when taking the hash or comparing the reference (for they to be equal they must point to the
+ same object and if equal they will have the same hash).
+
+ Usage:
+
+ ```
+ foo = NonHashableWithFancyEquality()
+ ref_to_foo = IdentityHashableRef(foo)
+
+ ref_to_foo() is foo # True
+
+ aset = set()
+ aset.add(IdentityHashableRef(foo))
+ IdentityHashableRef(foo) in aset # True
+
+ adict = dict()
+ adict[IdentityHashableRef(foo)] = 7
+ IdentityHashableRef(foo) in adict # True
+ ```
+ '''
+
+ _SENTINEL = object()
+
+ def __init__(self, original):
+ self._original = original
+
+ def __eq__(self, other):
+ return self._original is getattr(other, '_original', self._SENTINEL)
+
+ def __ne__(self, other):
+ return self._original is not getattr(other, '_original', self._SENTINEL)
+
+ def __hash__(self):
+ return id(self._original)
+
+ def __call__(self):
+ return self._original
+
+
+RegisterAsImmutable(IdentityHashableRef)
diff --git a/src/oop_ext/foundation/is_frozen.py b/src/oop_ext/foundation/is_frozen.py
new file mode 100644
index 0000000..35583ee
--- /dev/null
+++ b/src/oop_ext/foundation/is_frozen.py
@@ -0,0 +1,87 @@
+
+import sys
+
+'''
+frozen
+Setup the sys.frozen attribute when the application is not in release mode.
+This attribute is automatically set when the source code is in an executable.
+
+Use "IsFrozen" instead of "sys.frozen == False" because some libraries (pywin32) checks for the
+attribute existence, not the value.
+'''
+
+_is_frozen = hasattr(sys, 'frozen') and getattr(sys, 'frozen')
+
+
+def IsFrozen():
+ '''
+ Returns true if the code is frozen, that is, the code is inside a generated executable.
+
+ Frozen == False means the we are running the code using Python interpreter, usually associated with the code being
+ in development.
+ '''
+ return _is_frozen
+
+
+def SetIsFrozen(is_frozen):
+ '''
+ Sets the is_frozen value manually, overriding the "calculated" value.
+
+ :param bool is_frozen:
+ The new value for is_frozen.
+
+ :returns bool:
+ Returns the original value, before the given value is set.
+ '''
+ global _is_frozen
+ try:
+ return _is_frozen
+ finally:
+ _is_frozen = is_frozen
+
+
+_is_development = not _is_frozen
+
+
+def IsDevelopment():
+ '''
+ This function is used to indentify if we're in a development environment or production
+ environment.
+
+ :return bool:
+ Returns True if we're in a development environment or False if we're in a production
+ environment.
+
+ By default, the "development environment" is understood as not in frozen mode. However, be
+ careful not think that this will always be equivalent to 'not IsFrozen()'. This could also
+ return True in frozen environment, particularly when running tests on the executable.
+
+ ..seealso:: SetIsDevelopment to understand why.
+ '''
+ return _is_development
+
+
+def SetIsDevelopment(is_development):
+ '''
+ :param bool is_development:
+ The new is-development value, which is returned by ..seealso:: IsDevelopment.
+
+ :return bool:
+ The previous value of is-development property.
+
+ We wanted this method for the following reason:
+ Some methods we use in our codebase can make some checks/assertions that might be overly time-consuming to
+ have them running in production code. Therefore, the helper IsDevelopment is used to know if those methods
+ should run or not. However, due to the fact that we run tests on the executable and we want those methods
+ to be executed during testing, we need this method to make sure IsDevelopment returns true even in "frozen
+ environment".
+
+ DevelopmentCheckType is an example of a method using IsDevelopment to be enabled.
+
+ So always mind this difference and think.
+ '''
+ global _is_development
+ try:
+ return _is_development
+ finally:
+ _is_development = is_development
diff --git a/src/oop_ext/foundation/odict.py b/src/oop_ext/foundation/odict.py
new file mode 100644
index 0000000..92fa543
--- /dev/null
+++ b/src/oop_ext/foundation/odict.py
@@ -0,0 +1,39 @@
+
+import collections
+
+
+class _OrderedDict(collections.OrderedDict):
+
+ def insert(self, index, key, value, dict_setitem=dict.__setitem__):
+ """
+ Convenience method to have same interface as `ruamel.ordereddict`, which as traditionally
+ used on Python 2.
+ """
+ self[key] = value
+ # Determine which direction is cheaper to move items first. If new item is more to the left
+ # of center, move items to its left to first, otherwise it is cheaper to move items to
+ # right to last.
+ #
+ # Note that `move_to_end` is a O(1) operation that just swaps endpoints of underlying
+ # double linked list maintained by C-extension ordered dict.
+ if (len(self) - index) <= (len(self) // 2):
+ moved = [k for i, k in enumerate(self.keys()) if i >= index and k != key]
+ last = True
+ else:
+ moved = reversed([k for i, k in enumerate(self.keys()) if i < index or k == key])
+ last = False
+ for k in moved:
+ self.move_to_end(k, last=last)
+
+ def __delitem__(self, key):
+ if isinstance(key, slice):
+ # Properly deal with slices (based on order).
+ keys = list(self.keys())
+ for k in keys[key]:
+ collections.OrderedDict.__delitem__(self, k)
+
+ else:
+ collections.OrderedDict.__delitem__(self, key)
+
+
+odict = _OrderedDict
diff --git a/src/oop_ext/foundation/reraise.py b/src/oop_ext/foundation/reraise.py
new file mode 100644
index 0000000..a41f2bf
--- /dev/null
+++ b/src/oop_ext/foundation/reraise.py
@@ -0,0 +1,11 @@
+
+
+def Reraise(exception, message, separator='\n'):
+ """
+ Forwards to a `raise exc from cause` statement. Kept alive for backwards compatibility
+ (`separator` argument only kept for this reason).
+ """
+ # Important: Don't create a local variable for the new exception otherwise we'll get a
+ # cyclic reference between the exception and its traceback, meaning the traceback will
+ # keep all frames (and their contents) alive.
+ raise type(exception)(message) from exception
diff --git a/src/oop_ext/foundation/singleton.py b/src/oop_ext/foundation/singleton.py
new file mode 100644
index 0000000..88778e5
--- /dev/null
+++ b/src/oop_ext/foundation/singleton.py
@@ -0,0 +1,287 @@
+
+import threading
+
+
+#===================================================================================================
+# SingletonError
+#===================================================================================================
+class SingletonError(RuntimeError):
+ '''
+ Base class for all Singleton-related exceptions.
+ '''
+
+
+#===================================================================================================
+# SingletonAlreadySetError
+#===================================================================================================
+class SingletonAlreadySetError(SingletonError):
+ '''
+ Trying to set a singleton when the class already have one defined.
+ '''
+
+
+#===================================================================================================
+# SingletonNotSetError
+#===================================================================================================
+class SingletonNotSetError(SingletonError):
+ '''
+ Trying to clear a singleton when there's none defined.
+ '''
+
+
+#===================================================================================================
+# PushPopSingletonError
+#===================================================================================================
+class PushPopSingletonError(SingletonError):
+ '''
+ Trying to set a singleton between a PushSingleton/PopSingleton calls.
+ '''
+
+
+#===================================================================================================
+# Singleton
+#===================================================================================================
+class Singleton:
+ '''
+ Base class for singletons.
+
+ A Singleton class should have a unique instance during the lifetime of the application. Besides
+ the functionality of obtaining the singleton instance, this class also provides methods to push
+ and pop singletons, useful for testing, where you push a singleton into a known state during
+ setUp and pops it back during tearDown
+ '''
+
+ # name of the attribute that holds the stack of singletons
+ __singleton_stack_start_index = 0
+ __lock = threading.RLock()
+
+ _singleton_classes = set()
+
+ @staticmethod
+ def ResetDefaultSingletonInstances():
+ '''
+ This singleton class is intended to be used in tests with the push / pop protocol. However some singleton
+ dependencies might be hidden away from the test creator (or even be introduced after the test creation) making
+ easy for a code to access and change the default class singleton (for example registering on its callbacks).
+
+ This code is intended to clear any change made in such default singletons. Pushed singletons will not be cleared
+ because if a test has correctly pushed it singleton, it is reasonable to assume that the test will correctly
+ clean (pop) it.
+
+ TODO: ETK-1235 As soon as the classes with ResetInstance are moved to do not be a singleton, then this method
+ can be removed.
+ '''
+ for cls in Singleton._singleton_classes:
+ if cls._UsingDefaultSingleton():
+ instance = cls.GetSingleton()
+ instance.ResetInstance()
+
+ @classmethod
+ def GetSingleton(cls):
+ '''
+ :rtype: Singleton
+ :returns:
+ Returns the current singleton instance.
+
+ .. note:: This function is thread-safe, but all the other methods (such as SetSingleton,
+ PushSingleton, PopSingleton, etc) are not (which should be Ok as those are mostly
+ test-related, as singletons shouldn't really be changed after the application is up
+ especially on multi-threaded environments).
+ '''
+ Singleton._singleton_classes.add(cls)
+
+ try:
+ # Make common case faster.
+ return cls.__singleton_singleton_stack__[-1]
+ except (AttributeError, IndexError):
+ with cls.__lock:
+ # Only lock if the 'fast path' did not work.
+ stack = cls._ObtainStack()
+
+ if not stack: # Faster than doing len(stack) == 0
+ return cls.SetSingleton(None)
+
+ return stack[-1]
+
+ @classmethod
+ def SetSingleton(cls, instance):
+ '''
+ Sets the current singleton.
+
+ :param Singleton instance:
+ The Singleton to pass as parameter
+
+ :rtype: Singleton
+ :returns:
+ The singleton passed as parameter.
+
+ @raise PushPopSingletonError
+ @raise SingletonAlreadySetError
+ '''
+ stack = cls._ObtainStack()
+
+ # Error if we trying to use SetSingleton between a Push/Pop
+ if len(stack) != cls.__singleton_stack_start_index:
+ raise PushPopSingletonError('SetSingleton can not be called between a Push/Pop pair.')
+
+ if len(stack) > 0:
+ raise SingletonAlreadySetError('SetSingleton can only be called when there is no singleton set.')
+
+ # Obtain default instance (if needed)
+ if instance is None:
+ instance = cls.CreateDefaultSingleton()
+
+ # Set the stack[0] as the singleton
+ if len(stack) == 0:
+ stack.append(instance)
+ cls.__singleton_stack_start_index = 1
+ else:
+ stack[0] = instance
+
+ assert cls.__singleton_stack_start_index == 1
+
+ return instance
+
+ @classmethod
+ def _UsingDefaultSingleton(cls):
+ '''
+ Checks if the current singleton instance is the default instance.
+
+ :rtype: bool
+ :returns:
+ True if the current singleton instance is the default created instance. Returns False if the current instance
+ is a pushed singleton or if no instance is currently set
+ '''
+ stack = cls._ObtainStack()
+ has_pushed = len(stack) != cls.__singleton_stack_start_index
+ has_singleton = cls.HasSingleton()
+
+ return has_singleton and not has_pushed
+
+ def ResetInstance(self):
+ '''
+ Restore the instance original configuration. Singleton classes should not have a internal state to reset
+ (as described in issue ETK-1235), so subclasses that implement this method are strong candidates to be refactored
+ to do not be a singleton.
+
+ This method is used to avoid interference between tests while ETK-1235 is not implemented.
+ '''
+ pass
+
+ @classmethod
+ def ClearSingleton(cls):
+ '''
+ Clears the current singleton
+ '''
+ stack = cls._ObtainStack()
+
+ # Error if we trying to use ClearSingleton between a Push/Pop
+ if len(stack) != cls.__singleton_stack_start_index:
+ raise PushPopSingletonError('ClearSingleton can not be called between a Push/Pop pair.')
+
+ if not stack:
+ raise SingletonNotSetError('ClearSingleton can only be called when THERE IS singleton set.')
+
+ del stack[0]
+ cls.__singleton_stack_start_index = 0
+
+ @classmethod
+ def HasSingleton(cls):
+ '''
+ Do we have any singleton set?
+
+ :rtype: bool
+ :returns:
+ True if there's a singleton set.
+ '''
+ stack = cls._ObtainStack()
+ return len(stack) > 0
+
+ @classmethod
+ def CreateDefaultSingleton(cls):
+ '''
+ Creates the default singleton instance, that will be used when no singleton has been installed.
+ By default, tries to create the class without constructor.
+
+ :rtype: Singleton
+ :returns:
+ an instance of the singleton subclass
+ '''
+ return cls()
+
+ # Push/Pop -------------------------------------------------------------------------------------
+
+ @classmethod
+ def PushSingleton(cls, instance=None):
+ '''
+ Pushes the given singleton to the top of the stack. The previous singleton will be restored
+ when PopSingleton is called.
+
+ :param Singleton instance:
+ The singleton to install as the current one. If not given, a new singleton default
+ is created.
+
+ :rtype: Singleton
+ :returns:
+ The current singleton.
+ '''
+ if instance is None:
+ instance = cls.CreateDefaultSingleton()
+ stack = cls._ObtainStack()
+
+# DEBUG CODE
+# print '%s.PushSingleton' % cls.__name__, map(id, stack)
+# if len(stack) > 1:
+# from coilib50.debug import PrintTrace
+# PrintTrace(count=5)
+
+ stack.append(instance)
+ return instance
+
+ @classmethod
+ def PopSingleton(cls):
+ '''
+ Restores the singleton that was the current before the last PushSingleton.
+
+ :rtype: Singleton
+ :returns:
+ Return the removed singleton.
+ '''
+ stack = cls._ObtainStack()
+
+# DEBUG CODE
+# print '%s.PopSingleton' % cls.__name__, map(id, stack)
+# if len(stack) > 1:
+# from coilib50.debug import PrintTrace
+# PrintTrace(count=5)
+
+ if len(stack) == cls.__singleton_stack_start_index:
+ raise PushPopSingletonError('PopSingleton called without a pair PushSingleton call')
+
+ return cls._ObtainStack().pop(-1)
+
+ @classmethod
+ def _ObtainStack(cls):
+ '''
+ Obtains the stack of singletons.
+
+ :rtype: list
+ :returns:
+ The singleton stack.
+ '''
+ try:
+ return cls.__singleton_singleton_stack__
+ except AttributeError:
+ assert cls is not Singleton, 'This method can only be called from a Singleton subclass.'
+ stack = []
+ cls.__singleton_singleton_stack__ = stack
+ return stack
+
+ @classmethod
+ def GetStackCount(cls):
+ '''
+ @return int:
+ The number of elements added int the stack using PushSingleton.
+ '''
+ stack = cls._ObtainStack()
+ return len(stack) - cls.__singleton_stack_start_index
diff --git a/src/oop_ext/foundation/types_.py b/src/oop_ext/foundation/types_.py
new file mode 100644
index 0000000..c835ddf
--- /dev/null
+++ b/src/oop_ext/foundation/types_.py
@@ -0,0 +1,142 @@
+'''
+Extensions to python native types.
+'''
+
+#===================================================================================================
+# Method
+#===================================================================================================
+class Method:
+ '''
+ This class is an 'organization' class, so that subclasses are considered as methods
+ (and its __call__ method is checked for the parameters)
+ '''
+
+
+#===================================================================================================
+# Null
+#===================================================================================================
+class Null:
+ '''
+ This is a sample implementation of the 'Null Object' design pattern.
+
+ Roughly, the goal with Null objects is to provide an 'intelligent'
+ replacement for the often used primitive data type None in Python or
+ Null (or Null pointers) in other languages. These are used for many
+ purposes including the important case where one member of some group
+ of otherwise similar elements is special for whatever reason. Most
+ often this results in conditional statements to distinguish between
+ ordinary elements and the primitive Null value.
+
+ Among the advantages of using Null objects are the following:
+
+ - Superfluous conditional statements can be avoided
+ by providing a first class object alternative for
+ the primitive value None.
+
+ - Code readability is improved.
+
+ - Null objects can act as a placeholder for objects
+ with behaviour that is not yet implemented.
+
+ - Null objects can be replaced for any other class.
+
+ - Null objects are very predictable at what they do.
+
+ To cope with the disadvantage of creating large numbers of passive
+ objects that do nothing but occupy memory space Null objects are
+ often combined with the Singleton pattern.
+
+ For more information use any internet search engine and look for
+ combinations of these words: Null, object, design and pattern.
+
+ Dinu C. Gherman,
+ August 2001
+
+ ---
+
+ A class for implementing Null objects.
+
+ This class ignores all parameters passed when constructing or
+ calling instances and traps all attribute and method requests.
+ Instances of it always (and reliably) do 'nothing'.
+
+ The code might benefit from implementing some further special
+ Python methods depending on the context in which its instances
+ are used. Especially when comparing and coercing Null objects
+ the respective methods' implementation will depend very much
+ on the environment and, hence, these special methods are not
+ provided here.
+ '''
+
+ # object constructing
+
+ def __init__(self, *_args, **_kwargs):
+ "Ignore parameters."
+ # Setting the name of what's gotten (so that __name__ is properly preserved).
+ self.__dict__['_Null__name__'] = 'Null'
+ return None
+
+ def __call__(self, *_args, **_kwargs):
+ "Ignore method calls."
+ return self
+
+ def __getattr__(self, mname):
+ "Ignore attribute requests."
+ if mname == '__getnewargs__':
+ raise AttributeError('No support for that (pickle causes error if it returns self in this case.)')
+
+ if mname == '__name__':
+ return self.__dict__['_Null__name__']
+
+ return self
+
+ def __setattr__(self, _name, _value):
+ "Ignore attribute setting."
+ return self
+
+ def __delattr__(self, _name):
+ "Ignore deleting attributes."
+ return self
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, *args, **kwargs):
+ return self
+
+ def __repr__(self):
+ "Return a string representation."
+ return ""
+
+ def __str__(self):
+ "Convert to a string and return it."
+ return "Null"
+
+ def __bool__(self):
+ "Null objects are always false"
+ return False
+
+ def __nonzero__(self):
+ # Py 2 compatibility
+ return self.__bool__()
+
+ # iter
+
+ def __iter__(self):
+ "I will stop it in the first iteration"
+ return self
+
+ def __next__(self):
+ "Stop the iteration right now"
+ raise StopIteration()
+
+ def next(self):
+ # Py 2 compatibility
+ return self.__next__()
+
+ def __eq__(self, o):
+ "It is just equal to another Null object."
+ return self.__class__ == o.__class__
+
+
+NULL = Null() # Create a default instance to be used.
diff --git a/src/oop_ext/foundation/weak_ref.py b/src/oop_ext/foundation/weak_ref.py
new file mode 100644
index 0000000..a2b0e65
--- /dev/null
+++ b/src/oop_ext/foundation/weak_ref.py
@@ -0,0 +1,452 @@
+
+import inspect
+import weakref
+from types import LambdaType, MethodType
+
+from oop_ext.foundation.decorators import Implements
+
+
+#===================================================================================================
+# WeakList
+#===================================================================================================
+class WeakList:
+ '''
+ The weak list is a list that will only keep weak-references to objects passed to it.
+
+ When iterating the actual objects are used, but internally, only weakrefs are kept.
+
+ It does not contain the whole list interface (but can be extended as needed).
+
+ IMPORTANT: if you got here and need to implement a new feature or fix a bug,
+ consider replacing this implementation by this one instead:
+ https://github.com/apieum/weakreflist
+ '''
+
+ def __init__(self, initlist=None):
+ self.data = []
+
+ if initlist is not None:
+ for x in initlist:
+ self.append(x)
+
+ @Implements(list.append)
+ def append(self, item):
+ self.data.append(GetWeakRef(item))
+
+ @Implements(list.extend)
+ def extend(self, lst):
+ for o in lst:
+ self.append(o)
+
+ def __iter__(self):
+ # iterate in a copy
+ for ref in self.data[:]:
+ d = ref()
+ if d is None:
+ self.data.remove(ref)
+ else:
+ yield d
+
+ def remove(self, item):
+ '''
+ Remove first occurrence of a value.
+
+ It differs from the normal version because it will not raise an exception if the
+ item is not found (because it may be garbage-collected already).
+
+ :param object item:
+ The object to be removed.
+ '''
+ # iterate in a copy
+ for ref in self.data[:]:
+ d = ref()
+
+ if d is None:
+ self.data.remove(ref)
+
+ elif d == item:
+ self.data.remove(ref)
+ break
+
+ def __len__(self):
+ i = 0
+ for _k in self: # we make an iteration to remove dead references...
+ i += 1
+ return i
+
+ def __delitem__(self, i):
+ self.data.__delitem__(i)
+
+ def __getitem__(self, i):
+ if isinstance(i, slice):
+ slice_ = []
+ for ref in self.data[i.start:i.stop:i.step]:
+ d = ref()
+ if d is not None:
+ slice_.append(d)
+
+ return WeakList(slice_)
+ else:
+ return self.data[i]()
+
+ def __setitem__(self, i, item):
+ '''
+ Set a weakref of item on the ith position
+ '''
+ self.data[i] = GetWeakRef(item)
+
+ def __str__(self):
+ return '\n'.join(str(x) for x in self)
+
+
+#===================================================================================================
+# WeakMethodRef
+#===================================================================================================
+class WeakMethodRef:
+ '''
+ Weak reference to bound-methods. This allows the client to hold a bound method
+ while allowing GC to work.
+
+ Based on recipe from Python Cookbook, page 191. Differs by only working on
+ boundmethods and returning a true boundmethod in the __call__() function.
+
+ Keeps a reference to an object but doesn't prevent that object from being garbage collected.
+ '''
+
+ __slots__ = [
+ '_obj',
+ '_func',
+ '_class',
+ '_hash',
+ '__weakref__',
+ ]
+
+ def __init__(self, method):
+ try:
+ if method.__self__ is not None:
+ # bound method
+ self._obj = weakref.ref(method.__self__)
+ else:
+ # unbound method
+ self._obj = None
+ self._func = method.__func__
+ self._class = method.__self__.__class__
+ except AttributeError:
+ # not a method -- a callable: create a strong reference (the CallbackWrapper
+ # is depending on this behaviour... is it correct?)
+ self._obj = None
+ self._func = method
+ self._class = None
+
+ def __call__(self):
+ '''
+ Return a new bound-method like the original, or the original function if refers just to
+ a function or unbound method.
+
+ @return:
+ None if the original object doesn't exist anymore.
+ '''
+ if self.is_dead():
+ return None
+ if self._obj is not None:
+ # we have an instance: return a bound method
+ return MethodType(self._func, self._obj())
+ else:
+ # we don't have an instance: return just the function
+ return self._func
+
+ def is_dead(self):
+ '''Returns True if the referenced callable was a bound method and
+ the instance no longer exists. Otherwise, return False.
+ '''
+ return self._obj is not None and self._obj() is None
+
+ def __eq__(self, other):
+ try:
+ return type(self) is type(other) and self() == other()
+ except:
+ return False
+
+ def __ne__(self, other):
+ return not self == other
+
+ def __hash__(self):
+ if not hasattr(self, '_hash'):
+ # The hash should be immutable (must be calculated once and never changed -- otherwise
+ # we won't be able to get it when the object dies)
+ self._hash = hash(WeakMethodRef.__call__(self))
+
+ return self._hash
+
+ def __repr__(self):
+ func_name = getattr(self._func, '__name__', str(self._func))
+ if self._obj is not None:
+ obj = self._obj()
+ if obj is None:
+ obj_str = ''
+ else:
+ obj_str = '%X' % id(obj)
+ msg = ''
+ return msg % (self._class.__name__, func_name, obj_str)
+ else:
+ return '' % func_name
+
+
+#===================================================================================================
+# WeakMethodProxy
+#===================================================================================================
+class WeakMethodProxy(WeakMethodRef):
+ '''
+ Like ref, but calling it will cause the referent method to be called with the same
+ arguments. If the referent's object no longer lives, ReferenceError is raised.
+ '''
+
+ def GetWrappedFunction(self):
+ return WeakMethodRef.__call__(self)
+
+ def __call__(self, *args, **kwargs):
+ func = WeakMethodRef.__call__(self)
+ if func is None:
+ raise ReferenceError('Object is dead. Was of class: {}'.format(self._class))
+ else:
+ return func(*args, **kwargs)
+
+ def __eq__(self, other):
+ try:
+ func1 = WeakMethodRef.__call__(self)
+ func2 = WeakMethodRef.__call__(other)
+ return type(self) == type(other) and func1 == func2
+ except:
+ return False
+
+
+#===================================================================================================
+# WeakSet
+#===================================================================================================
+class WeakSet:
+ '''
+ Just like `weakref.WeakSet`, but supports adding methods (the standard `weakref.WeakSet` can't
+ add methods, this feature comes from `oop_ext.foundation.weak_ref.GetWeakRef`, see `testWeakSet2`).
+
+ It does not contain the whole set interface (but can be extended as needed).
+
+ ..see:: oop_ext.foundation.weak_ref.GetWeakRef
+ ..see:: weakref.WeakSet
+ '''
+
+ def __init__(self):
+ self.data = set()
+
+ def add(self, item):
+ self.data.add(GetWeakRef(item))
+
+ def clear(self):
+ self.data.clear()
+
+ def __iter__(self):
+ # iterate in a copy
+ for ref in self.data.copy():
+ d = ref()
+ if d is None:
+ self.data.remove(ref)
+ else:
+ yield d
+
+ def remove(self, item):
+ '''
+ Remove an item from the available data.
+
+ :param object item:
+ The object to be removed.
+ '''
+ self.data.remove(GetWeakRef(item))
+
+ def union(self, another_set):
+ result = WeakSet()
+ result.data = self.data.copy()
+ for i in another_set:
+ result.add(i)
+ return result
+
+ def __sub__(self, another_set):
+ result = WeakSet()
+ result.data = self.data.copy()
+ for i in another_set:
+ result.discard(i)
+ return result
+
+ def __rsub__(self, another_set):
+ result = another_set.copy()
+ for i in self:
+ result.discard(i)
+ return result
+
+ def discard(self, item):
+ try:
+ self.remove(item)
+ except KeyError:
+ pass
+
+ def __len__(self):
+ i = 0
+ for _k in self: # we make an iteration to remove dead references...
+ i += 1
+ return i
+
+ def __str__(self):
+ return '\n'.join(str(x) for x in self)
+
+
+#===================================================================================================
+# IsWeakProxy
+#===================================================================================================
+def IsWeakProxy(obj):
+ '''
+ Returns whether the given object is a weak-proxy
+ '''
+ return isinstance(obj, (weakref.ProxyType, WeakMethodProxy))
+
+
+#===================================================================================================
+# IsWeakRef
+#===================================================================================================
+def IsWeakRef(obj):
+ '''
+ Returns wheter ths given object is a weak-reference.
+ '''
+ return isinstance(obj, (weakref.ReferenceType, WeakMethodRef)) and not isinstance(obj, WeakMethodProxy)
+
+
+#===================================================================================================
+# IsWeakObj
+#===================================================================================================
+def IsWeakObj(obj):
+ '''
+ Returns whether the given object is a weak object. Either a weak-proxy or a weak-reference.
+
+ :param obj: The object that may be a weak reference or proxy
+ :return bool: True if it is a proxy or a weak reference.
+ '''
+ return IsWeakProxy(obj) or IsWeakRef(obj)
+
+
+#===================================================================================================
+# GetRealObj
+#===================================================================================================
+def GetRealObj(obj):
+ '''
+ Returns the real-object from a weakref, or the object itself otherwise.
+ '''
+ if IsWeakRef(obj):
+ return obj()
+ if isinstance(obj, LambdaType):
+ return obj()
+ return obj
+
+
+#===================================================================================================
+# GetWeakProxy
+#===================================================================================================
+def GetWeakProxy(obj):
+ '''
+ :param obj: This is the object we want to get as a proxy
+ :return:
+ Returns the object as a proxy (if it is still not already a proxy or a weak ref, in which case the passed object
+ is returned itself)
+ '''
+ if obj is None:
+ return None
+
+ if not IsWeakProxy(obj):
+
+ if IsWeakRef(obj):
+ obj = obj()
+
+ # for methods we cannot create regular weak-refs
+ if inspect.ismethod(obj):
+ return WeakMethodProxy(obj)
+
+ return weakref.proxy(obj)
+
+ return obj
+
+
+# Keep the same lambda for weak-refs (to be reused among all places that use GetWeakRef(None)
+_EMPTY_LAMBDA = lambda:None
+
+
+#===================================================================================================
+# GetWeakRef
+#===================================================================================================
+def GetWeakRef(obj):
+ '''
+ :type obj: this is the object we want to get as a weak ref
+ :param obj:
+ @return the object as a proxy (if it is still not already a proxy or a weak ref, in which case the passed
+ object is returned itself)
+ '''
+ if obj is None:
+ return _EMPTY_LAMBDA
+
+ if IsWeakProxy(obj):
+ raise RuntimeError('Unable to get weak ref for proxy.')
+
+ if not IsWeakRef(obj):
+
+ # for methods we cannot create regular weak-refs
+ if inspect.ismethod(obj):
+ return WeakMethodRef(obj)
+
+ return weakref.ref(obj)
+ return obj
+
+
+#===================================================================================================
+# IsSame
+#===================================================================================================
+def IsSame(o1, o2):
+ '''
+ This checks for the identity even if one of the parameters is a weak reference
+
+ :param o1:
+ first object to compare
+
+ :param o2:
+ second object to compare
+
+ @raise
+ RuntimeError if both of the passed parameters are weak references
+ '''
+ # get rid of weak refs (we only need special treatment for proxys)
+ if IsWeakRef(o1):
+ o1 = o1()
+ if IsWeakRef(o2):
+ o2 = o2()
+
+ # simple case (no weak objects)
+ if not IsWeakObj(o1) and not IsWeakObj(o2):
+ return o1 is o2
+
+ # all weak proxys
+ if IsWeakProxy(o1) and IsWeakProxy(o2):
+ if not o1 == o2:
+ # if they are not equal, we know they're not the same
+ return False
+
+ # but we cannot say anything if they are the same if they are equal
+ raise ReferenceError('Cannot check if object is same if both arguments passed are weak objects')
+
+ # one is weak and the other is not
+ if IsWeakObj(o1):
+ weak = o1
+ original = o2
+ else:
+ weak = o2
+ original = o1
+
+ weaks = weakref.getweakrefs(original)
+ for w in weaks:
+ if w is weak: # check the weak object identity
+ return True
+
+ return False
diff --git a/src/oop_ext/interface/__init__.py b/src/oop_ext/interface/__init__.py
new file mode 100644
index 0000000..f24cfcd
--- /dev/null
+++ b/src/oop_ext/interface/__init__.py
@@ -0,0 +1,45 @@
+
+from ._adaptable_interface import IAdaptable
+from ._interface import (
+ AssertDeclaresInterface, AssertImplements, AssertImplementsFullChecking, Attribute,
+ BadImplementationError, CacheInterfaceAttrs, DeclareClassImplements, GetImplementedInterfaces,
+ ImplementsInterface, Interface, InterfaceError, InterfaceImplementationMetaClass,
+ InterfaceImplementorStub, IsImplementation, IsImplementationOfAny, ReadOnlyAttribute)
+
+'''
+ Interfaces module.
+
+ A Interface describes a behaviour that some objects must implement.
+
+ To declare a interface, just subclass from Interface::
+
+ class IFoo(interface.Interface):
+ ...
+
+ To create a class that implements that interface, use interface.Implements:
+
+ class Foo(object):
+ interface.Implements(IFoo)
+
+ If Foo doesn't implement some method from IFoo, an exception is raised at class creation time.
+'''
+
+__all__ = [
+ 'AssertDeclaresInterface',
+ 'AssertImplements',
+ 'AssertImplementsFullChecking',
+ 'Attribute',
+ 'BadImplementationError',
+ 'CacheInterfaceAttrs',
+ 'GetImplementedInterfaces',
+ 'IAdaptable',
+ 'Interface',
+ 'InterfaceError',
+ 'InterfaceImplementationMetaClass',
+ 'InterfaceImplementorStub',
+ 'IsImplementation',
+ 'ReadOnlyAttribute',
+ 'ImplementsInterface',
+ 'DeclareClassImplements',
+ 'IsImplementationOfAny',
+]
diff --git a/src/oop_ext/interface/_adaptable_interface.py b/src/oop_ext/interface/_adaptable_interface.py
new file mode 100644
index 0000000..1f112b6
--- /dev/null
+++ b/src/oop_ext/interface/_adaptable_interface.py
@@ -0,0 +1,29 @@
+
+from ._interface import Interface
+
+
+#===================================================================================================
+# IAdaptable
+#===================================================================================================
+class IAdaptable(Interface):
+ '''
+ An interface for an object that is adaptable.
+
+ Adaptable objects can be queried about interfaces they adapt to (to which they
+ may respond or not).
+
+ For example:
+
+ a = [some IAdaptable];
+ x = a.GetAdapter(IFoo);
+ if x is not None:
+ [do IFoo things with x]
+ '''
+
+ def GetAdapter(self, interface_class):
+ '''
+ :type interface_class: this is the interface for which an adaptation is required
+ :param interface_class:
+ :rtype: an object implementing the required interface or None if this object cannot
+ adapt to that interface.
+ '''
diff --git a/src/oop_ext/interface/_interface.py b/src/oop_ext/interface/_interface.py
new file mode 100644
index 0000000..7ec5178
--- /dev/null
+++ b/src/oop_ext/interface/_interface.py
@@ -0,0 +1,967 @@
+'''
+This module provides a basic interface concept.
+
+To use, create an interface:
+
+
+class IMyCalculator(Interface):
+
+ def Sum(self, *values):
+ ...
+
+
+Have classes implement an interface:
+
+class MyCalculatorImpl(object):
+
+ ImplementsInterface(IMyCalculator)
+
+ ...
+
+
+Then, to check an interface:
+
+
+impl = MyCalculatorImpl()
+
+if IsImplementation(impl, IMyCalculator):
+ ...
+
+
+Or if it *needs* to check an interface:
+
+AssertImplements(impl, IMyCalculator)
+
+
+'''
+
+import inspect
+import sys
+
+from oop_ext.foundation.decorators import Deprecated
+from oop_ext.foundation.is_frozen import IsDevelopment
+from oop_ext.foundation.reraise import Reraise
+from oop_ext.foundation.types_ import Method
+
+
+#===================================================================================================
+# InterfaceError
+#===================================================================================================
+class InterfaceError(RuntimeError):
+ pass
+
+
+#===================================================================================================
+# BadImplementationError
+#===================================================================================================
+class BadImplementationError(InterfaceError):
+ pass
+
+
+#===================================================================================================
+# InterfaceImplementationMetaClass
+#===================================================================================================
+class InterfaceImplementationMetaClass(type):
+
+ def __new__(cls, name, bases, dct):
+ C = type.__new__(cls, name, bases, dct)
+ if IsDevelopment(): # Only doing check in dev mode.
+ for I in dct.get('__implements__', []):
+ # Will do full checking this first time, and also cache the results
+ AssertImplements(C, I)
+ return C
+
+
+#===================================================================================================
+# InterfaceImplementorStub
+#===================================================================================================
+class InterfaceImplementorStub:
+ '''
+ A helper for acting as a stub for some object (in this way, we're only able to access
+ attributes declared directly in the interface.
+
+ It forwards the calls to the actual implementor (the wrapped object)
+ '''
+
+ def __init__(self, wrapped, implemented_interface):
+ self.__wrapped = wrapped
+ self.__implemented_interface = implemented_interface
+
+ self.__interface_methods, self.__attrs = \
+ cache_interface_attrs.GetInterfaceMethodsAndAttrs(implemented_interface)
+
+ def GetWrappedFromImplementorStub(self):
+ '''
+ Really big and awkward name because we don't want name-clashes
+ '''
+ return self.__wrapped
+
+ def __getattr__(self, attr):
+ if attr not in self.__attrs and attr not in self.__interface_methods:
+ raise AttributeError("Error. The interface {} does not have the attribute '{}' declared.".format(self.__implemented_interface, attr))
+ return getattr(self.__wrapped, attr)
+
+ def __getitem__(self, *args, **kwargs):
+ if '__getitem__' not in self.__interface_methods:
+ raise AttributeError("Error. The interface {} does not have the attribute '{}' declared.".format(self.__implemented_interface, '__getitem__'))
+ return self.__wrapped.__getitem__(*args, **kwargs)
+
+ def __setitem__(self, *args, **kwargs):
+ if '__setitem__' not in self.__interface_methods:
+ raise AttributeError("Error. The interface {} does not have the attribute '{}' declared.".format(self.__implemented_interface, '__setitem__'))
+ return self.__wrapped.__setitem__(*args, **kwargs)
+
+ def __repr__(self):
+ return '' % self.__wrapped
+
+ def __call__(self, *args, **kwargs):
+ if '__call__' not in self.__interface_methods:
+ raise AttributeError("Error. The interface {} does not have the attribute '{}' declared.".format(self.__implemented_interface, '__call__'))
+ return self.__wrapped.__call__(*args, **kwargs)
+
+
+#===================================================================================================
+# Interface
+#===================================================================================================
+class Interface:
+ '''Base class for interfaces.
+
+ A interface describes a behavior that some objects must implement.
+ '''
+
+ # : instance to check if we are receiving an argument during __new__
+ _SENTINEL = []
+
+ def __new__(cls, class_=_SENTINEL):
+ # if no class is given, raise InterfaceError('trying to instantiate interface')
+ # check if class_or_object implements this interface
+ from ._adaptable_interface import IAdaptable
+
+ if class_ is cls._SENTINEL:
+ raise InterfaceError('Can\'t instantiate Interface.')
+ else:
+ if isinstance(class_, type):
+ # We're doing something as Interface(InterfaceImpl) -- not instancing
+ _AssertImplementsFullChecking(class_, cls, check_attr=False)
+ return class_
+ elif isinstance(class_, InterfaceImplementorStub):
+ return class_
+ else:
+ implemented_interfaces = GetImplementedInterfaces(class_)
+
+ if cls in implemented_interfaces:
+ return InterfaceImplementorStub(class_, cls)
+
+ elif IAdaptable in implemented_interfaces:
+ adapter = class_.GetAdapter(cls)
+ if adapter is not None:
+ return InterfaceImplementorStub(adapter, cls)
+
+ # We're doing something as Interface(InterfaceImpl()) -- instancing
+ _AssertImplementsFullChecking(class_, cls, check_attr=True)
+ return InterfaceImplementorStub(class_, cls)
+
+
+#===================================================================================================
+# _GetClassForInterfaceChecking
+#===================================================================================================
+def _GetClassForInterfaceChecking(class_or_instance):
+ if _IsClass(class_or_instance):
+ return class_or_instance # is class
+ elif isinstance(class_or_instance, InterfaceImplementorStub):
+ return _GetClassForInterfaceChecking(class_or_instance.GetWrappedFromImplementorStub())
+
+ return class_or_instance.__class__ # is instance
+
+
+def _IsClass(obj):
+ return isinstance(obj, type)
+
+
+#===================================================================================================
+# IsImplementation
+#===================================================================================================
+def IsImplementation(class_or_instance, interface):
+ '''
+ :type class_or_instance: type or classobj or object
+
+ :type interface: Type[Interface]
+
+ :rtype: bool
+
+ :see: :py:func:`.AssertImplements`
+ '''
+ try:
+ is_interface = issubclass(interface, Interface)
+ except TypeError as e:
+ Reraise(e, "interface={} (type {})".format(interface, type(interface)))
+
+ if not is_interface:
+ raise InterfaceError(
+ 'To check against an interface, an interface is required (received: %s -- mro:%s)' %
+ (interface, interface.__mro__)
+ )
+
+ class_ = _GetClassForInterfaceChecking(class_or_instance)
+
+ is_implementation, _reason = _CheckIfClassImplements(class_, interface)
+
+ # Check older revisions of this file for helper debug code in this place.
+
+ return is_implementation
+
+
+#===================================================================================================
+# IsImplementationOfAny
+#===================================================================================================
+def IsImplementationOfAny(class_or_instance, interfaces):
+ '''
+ Check if the class or instance implements any of the given interfaces
+
+ :type class_or_instance: type or classobj or object
+
+ :type interfaces: list(Interface)
+
+ :rtype: bool
+
+ :see: :py:func:`.IsImplementation`
+ '''
+ for interface in interfaces:
+ if IsImplementation(class_or_instance, interface):
+ return True
+
+ return False
+
+
+#===================================================================================================
+# AssertImplements
+#===================================================================================================
+def AssertImplements(class_or_instance, interface):
+ '''
+ If given a class, will try to match the class against a given interface. If given an object
+ (instance), will try to match the class of the given object.
+
+ NOTE: The Interface must have been explicitly declared through :py:func:`ImplementsInterface`.
+
+ :type class_or_instance: type or classobj or object
+
+ :type interface: Interface
+
+ :raises BadImplementationError:
+ If the object's class does not implement the given :arg interface:.
+
+ :raises InterfaceError:
+ In case the :arg interface: object is not really an interface.
+
+ .. attention:: Caching
+ Will do a full checking only once, and then cache the result for the given class.
+
+ .. attention:: Runtime modifications
+ Runtime modifications in the instances (appending methods or attributed) won't affect
+ implementation checking (after the first check), because what is really being tested is the
+ class.
+ '''
+ class_ = _GetClassForInterfaceChecking(class_or_instance)
+
+ is_implementation, reason = _CheckIfClassImplements(class_, interface)
+
+ assert is_implementation, reason
+
+
+#===================================================================================================
+# __ResultsCache
+#===================================================================================================
+class __ResultsCache:
+
+ def __init__(self):
+ self._cache = {}
+
+ def SetResult(self, args, result):
+ self._cache[args] = result
+
+ def GetResult(self, args):
+ return self._cache.get(args, None)
+
+ def ForgetResult(self, args):
+ self._cache.pop(args, None)
+
+
+__ImplementsCache = __ResultsCache()
+
+__ImplementedInterfacesCache = __ResultsCache()
+
+
+#===================================================================================================
+# _CheckIfClassImplements
+#===================================================================================================
+def _CheckIfClassImplements(class_, interface):
+ '''
+ :type class_: type or classobj
+ :param class_:
+ A class type (NOT an instance of the class).
+
+ :type interface: Interface
+
+ :rtype: (bool, str) or (bool, None)
+ :returns:
+ (is_implementation, reason)
+ If the class doesn't implement the given interface, will return False, and a message stating
+ the reason (missing methods, etc.). The message may be None.
+ '''
+ assert _IsClass(class_)
+
+ # Using explicit memoization, because we need to forget some values at some times
+ cache = __ImplementsCache
+
+ cached_result = cache.GetResult((class_, interface))
+ if cached_result is not None:
+ return cached_result
+
+ is_implementation = True
+ reason = None
+
+ # Exception: Null implements every Interface (useful for Null Object Pattern and for testing)
+ from oop_ext.foundation.types_ import Null
+
+ if not issubclass(class_, Null):
+ if _IsInterfaceDeclared(class_, interface):
+ # It is required to explicitly declare that the class implements the interface.
+
+ # Since this will only run *once*, a full check is also done here to ensure it is really
+ # implementing.
+ try:
+ _AssertImplementsFullChecking(class_, interface, check_attr=False)
+ except BadImplementationError as e:
+ is_implementation = False
+ from oop_ext.foundation.exceptions import ExceptionToUnicode
+ reason = ExceptionToUnicode(e)
+ else:
+ is_implementation = False
+ reason = 'The class {} does not declare that it implements the interface {}.'.format(
+ class_, interface)
+
+ result = (is_implementation, reason)
+ cache.SetResult((class_, interface), result)
+ return result
+
+
+#===================================================================================================
+# _IsImplementationFullChecking
+#===================================================================================================
+def _IsImplementationFullChecking(class_or_instance, interface):
+ '''
+ Used internally by Attribute.
+
+ :see: :py:func:`._AssertImplementsFullChecking`
+ :type class_or_instance: type or instance
+ :param class_or_instance:
+ Class or instance to check
+
+ :param Interface interface:
+ Interface to check
+
+ :rtype: bool
+ :returns:
+ If it implements the interface
+ '''
+ try:
+ _AssertImplementsFullChecking(class_or_instance, interface)
+ except BadImplementationError:
+ return False
+ else:
+ return True
+
+
+#===================================================================================================
+# Attribute
+#===================================================================================================
+class Attribute:
+ '''
+ '''
+ _do_not_check_instance = object()
+
+ def __init__(self, attribute_type, instance=_do_not_check_instance):
+ '''
+ :param type attribute_type:
+ Will check the attribute type in the implementation against this type.
+ Checks if the attribute is a direct instance of attribute_type, or of it implements it.
+
+ :param object instance:
+ If passed, will check for *equality* against this instance. The default is to not check
+ for equality.
+ '''
+ self.attribute_type = attribute_type
+ self.instance = instance
+
+ def Match(self, attribute):
+ '''
+ :param object attribute:
+ Object that will be compared to see if it matches the expected interface.
+
+ :rtype: (bool, str)
+ :returns:
+ If the given object implements or inherits from the interface expected by this
+ attribute, will return (True, None), otherwise will return (False, message), where
+ message is an error message of why there was a mismatch (may be None also).
+ '''
+ msg = None
+
+ if isinstance(attribute, self.attribute_type):
+ return (True, None)
+
+ if self.instance is not self._do_not_check_instance:
+ if self.instance == attribute:
+ return (True, None)
+ else:
+ return (
+ False,
+ 'The instance ({}) does not match the expected one ({}).'.format(
+ self.instance, attribute)
+ )
+
+ try:
+ if _IsImplementationFullChecking(attribute, self.attribute_type):
+ return (True, msg)
+ except InterfaceError as exception_msg:
+ # Necessary because whenever a value is compared to an interface it does not inherits
+ # from, IsImplementation raises an InterfaceError. In this context, an error like that
+ # will mean that our candidate attribute is in fact not matching the interface, so we
+ # capture this error and return False.
+ msg = exception_msg
+
+ return (False, None)
+
+
+#===================================================================================================
+# ReadOnlyAttribute
+#===================================================================================================
+class ReadOnlyAttribute(Attribute):
+ '''
+ This is an attribute that should be treated as read-only (note that usually this means that
+ the related property should be also declared as read-only).
+ '''
+
+
+#===================================================================================================
+# CacheInterfaceAttrs
+#===================================================================================================
+class CacheInterfaceAttrs:
+ '''
+ Cache for holding the attrs for a given interface (separated by attrs and methods).
+ '''
+
+ _ATTRIBUTE_CLASSES = (Attribute, ReadOnlyAttribute)
+ INTERFACE_OWN_METHODS = {i for i in dir(Interface) if inspect.isfunction(getattr(Interface, i))}
+ FUTURE_OBJECT_ATTRS = ('next', '__long__', '__nonzero__', '__unicode__', '__native__')
+
+ @classmethod
+ def RegisterAttributeClass(cls, attribute_class):
+ '''
+ Registers a class to be considered as an attribute class.
+
+ This provides a way of extending the Interface behavior by declaring new attributes classes
+ such as ScalarAttribute.
+
+ :param Attribute attribute_class:
+ An Attribute class to register as an attribute class.
+
+ :return set(Attribute):
+ Returns a set with all the current attribute classes.
+ '''
+ result = set(cls._ATTRIBUTE_CLASSES)
+ result.add(attribute_class)
+ cls._ATTRIBUTE_CLASSES = tuple(result)
+ return result
+
+ def __GetInterfaceMethodsAndAttrs(self, interface):
+ '''
+ :type interface: the interface from where the methods and attributes should be gotten.
+ :param interface:
+ :rtype: the interface methods and attributes available in a given interface.
+ '''
+ all_attrs = dir(interface)
+
+ interface_methods = dict()
+ interface_attrs = dict()
+ interface_vars = vars(interface)
+
+ # Python 3+ changed how functions are represented, so it isn't possible anymore to
+ # determine if a function is a method BEFORE it is bound to an object.
+ # For this reason, it is necessary to also search by functions on Python and to filter out
+ # functions like `__new__`, which are part of `Interface` class implementation and not part
+ # expected interface.
+ for attr in all_attrs:
+ # If name inherited from `Interface`, check if isn't in list of reserved names
+ if attr not in interface_vars:
+ if attr in self.INTERFACE_OWN_METHODS:
+ continue
+
+ val = getattr(interface, attr)
+
+ if type(val) in self._ATTRIBUTE_CLASSES:
+ interface_attrs[attr] = val
+
+ if _IsMethod(val, include_functions=True):
+ interface_methods[attr] = val
+
+ return interface_methods, interface_attrs
+
+ def GetInterfaceMethodsAndAttrs(self, interface):
+ '''
+ We have to make the creation of the ImmutableParamsCacheManager lazy because
+ otherwise we'd enter a cyclic import.
+
+ :type interface: the interface from where the methods and attributes should be gotten
+ :param interface:
+ (used as the cache-key)
+ :rtype: @see: CacheInterfaceAttrs.__GetInterfaceMethodsAndAttrs
+ '''
+ try:
+ cache = self.cache
+ except AttributeError:
+ # create it on the 1st access
+ from oop_ext.foundation.cached_method import ImmutableParamsCachedMethod
+ cache = self.cache = ImmutableParamsCachedMethod(self.__GetInterfaceMethodsAndAttrs)
+ return cache(interface)
+
+
+# cache for the interface attrs (for Methods and Attrs).
+cache_interface_attrs = CacheInterfaceAttrs()
+
+
+#===================================================================================================
+# _IsMethod
+#===================================================================================================
+def _IsMethod(member, include_functions):
+ '''
+ Consider method the following:
+ 1) Methods
+ 2) Functions (if include_functions is True)
+ 3) instances of Method (should it be implementors of "IMethod"?)
+
+ USER: cache mechanism for coilib50.basic.process
+ '''
+ if include_functions and inspect.isfunction(member):
+ return True
+ elif inspect.ismethod(member):
+ return True
+ elif isinstance(member, Method):
+ return True
+ return False
+
+
+#===================================================================================================
+# AssertImplementsFullChecking
+#===================================================================================================
+@Deprecated(AssertImplements)
+def AssertImplementsFullChecking(class_or_instance, interface, check_attr=True):
+ return AssertImplements(class_or_instance, interface)
+
+
+#===================================================================================================
+# _AssertImplementsFullChecking
+#===================================================================================================
+def _AssertImplementsFullChecking(class_or_instance, interface, check_attr=True):
+ '''
+ Used internally.
+
+ This method will check each member of the given instance (or class) comparing them against the
+ ones declared in the interface, making sure that it actually implements it even if it does not
+ declare it so using ImplementsInterface.
+
+ .. note:: Slow
+ This method is *slow*, so make sure to never use it in hot-spots.
+
+ :raises BadImplementationError:
+ If :arg class_or_instance: doesn't implement this interface.
+ '''
+ # Moved from the file to avoid cyclic import:
+ from oop_ext.foundation.types_ import Null
+
+ try:
+ is_interface = issubclass(interface, Interface)
+ except TypeError as e:
+ Reraise(e, "interface={} (type {})".format(interface, type(interface)))
+
+ if not is_interface:
+ raise InterfaceError(
+ 'To check against an interface, an interface is required (received: %s -- mro:%s)' %
+ (interface, interface.__mro__)
+ )
+
+ if isinstance(class_or_instance, Null):
+ return True
+
+ try:
+ classname = class_or_instance.__name__
+ except:
+ classname = class_or_instance.__class__.__name__
+
+ if classname == 'InterfaceImplementorStub':
+ return _AssertImplementsFullChecking(class_or_instance.GetWrappedFromImplementorStub(), interface, check_attr)
+
+ interface_methods, interface_attrs = cache_interface_attrs.GetInterfaceMethodsAndAttrs(interface)
+ if check_attr:
+ for attr_name, val in interface_attrs.items():
+ if hasattr(class_or_instance, attr_name):
+ attr = getattr(class_or_instance, attr_name)
+ match, match_msg = val.Match(attr)
+ if not match:
+ msg = 'Attribute %r for class %s does not match the interface %s'
+ msg = msg % (attr_name, class_or_instance, interface)
+ if match_msg:
+ msg += ': ' + match_msg
+ raise BadImplementationError(msg)
+ else:
+ msg = 'Attribute %r is missing in class %s and it is required by interface %s'
+ msg = msg % (attr_name, class_or_instance, interface)
+ raise BadImplementationError(msg)
+
+ def GetArgSpec(method):
+ '''
+ Get the arguments for the method, considering the possibility of instances of Method,
+ in which case, we must obtain the arguments of the instance "__call__" method.
+
+ USER: cache mechanism for coilib50.basic.process
+ '''
+ if isinstance(method, Method):
+ argspec = inspect.getfullargspec(method.__call__)
+ else:
+ argspec = inspect.getfullargspec(method)
+ assert (not argspec.kwonlyargs) and (not argspec.kwonlydefaults), 'Not supported'
+ return argspec[:4]
+
+ for name in interface_methods:
+ # only check the interface methods (because trying to get all the instance methods is
+ # too slow).
+ try:
+ cls_method = getattr(class_or_instance, name)
+ if not _IsMethod(cls_method, True):
+ raise AttributeError
+
+ except AttributeError:
+ msg = 'Method %r is missing in class %r (required by interface %r)'
+ raise BadImplementationError(msg % (name, classname, interface.__name__))
+ else:
+ interface_method = interface_methods[name]
+
+ c_args, c_varargs, c_varkw, c_defaults = GetArgSpec(cls_method)
+
+ if c_varargs is not None and c_varkw is not None:
+ if not c_args or c_args == ['self'] or c_args == ['cls']:
+ # Accept the implementor if it matches the signature: (*args, **kwargs)
+ # Accept the implementor if it matches the signature: (self, *args, **kwargs)
+ # Accept the implementor if it matches the signature: (cls, *args, **kwargs)
+ continue
+
+ i_args, i_varargs, i_varkw, i_defaults = GetArgSpec(interface_method)
+
+ # Rules:
+ #
+ # 1. Variable arguments or keyword arguments: if present
+ # in interface, then it MUST be present in class too
+ #
+ # 2. Arguments: names must be the same
+ #
+ # 3. Defaults: for now we assume that default values
+ # must be the same too
+ mismatch_varargs = i_varargs is not None and c_varargs is None
+ mismatch_varkw = i_varkw is not None and c_varkw is None
+ mismatch_args = i_args != c_args
+ mismatch_defaults = i_defaults != c_defaults
+ if mismatch_varargs or mismatch_varkw or mismatch_args or mismatch_defaults:
+ class_sign = inspect.formatargspec(c_args, c_varargs, c_varkw, c_defaults)
+ interface_sign = inspect.formatargspec(i_args, i_varargs, i_varkw, i_defaults)
+ msg = '\nMethod %s.%s signature:\n %s\ndiffers from defined in interface %s\n %s'
+ msg = msg % (classname, name, class_sign, interface.__name__, interface_sign)
+ raise BadImplementationError(msg)
+
+#===================================================================================================
+# PROFILING FOR ASSERT IMPLEMENTS
+
+# NOTE: There was code here for profiling AssertImplements in revisions prior to 2013-03-19.
+# That code can be useful for seeing where exactly it is being slow.
+#===================================================================================================
+
+
+class _IfGuard:
+ '''
+ Guard that raises an error if an attempt to convert it to a boolean value is made.
+ '''
+
+ def __bool__(self):
+ raise RuntimeError('Invalid attempt to test interface.ImplementsInterface(). Did you mean interface.IsImplementation()?')
+
+
+__IF_GUARD = _IfGuard()
+
+DEBUG = False
+
+
+#===================================================================================================
+# ImplementsInterface
+#===================================================================================================
+def ImplementsInterface(*interfaces, **kwargs):
+ '''
+ Make sure a class implements the given interfaces. Must be used in as class decorator:
+
+ ```python
+ @ImplementsInterface(IFoo)
+ class Foo(object):
+ pass
+ ```
+
+ To avoid checking if the class implements declared interfaces during class creation time, or for
+ old-style classes, make sure to pass the flag `no_check` as True:
+
+ ```python
+ @ImplementsInterface(IFoo, no_check=True)
+ class Foo(object):
+ pass
+ ```
+ '''
+ no_check = kwargs.pop('no_check', False)
+ assert len(kwargs) == 0, "Expected only 'no_init_check' as kwargs. Found: {}".format(kwargs)
+
+ called = [False]
+
+ class Check:
+
+ def __init__(self):
+
+ def _OnDie(ref):
+ # We may just use warnings.warn in the future, after our
+ # codebase is properly 'sanitized', instead of handle_exception.
+ #
+ # This is to prevent users of doing an ImplementsInterface()
+ # without using it as a decorator.
+ if not called[0]:
+ if not DEBUG:
+ created_at_line = '\nSet DEBUG == True in: {} to see location.'.format(__file__)
+ else:
+ # This may be slow, so, just do it if DEBUG is enabled.
+ import traceback
+ created_at_line = traceback.extract_stack(sys._getframe(), limit=10)
+ try:
+ if isinstance(created_at_line, ''.__class__):
+ created_at_str = created_at_line
+ else:
+ created_at_str = str('').join(traceback.format_list(created_at_line))
+
+ raise AssertionError(
+ 'A call with ImplementsInterface({}) was not properly done as a class decorator.\nCreated at: {}'.format(
+ ', '.join(tuple(str(x.__name__ if hasattr(x, '__name__') else x) for x in interfaces),), created_at_str))
+ except:
+ from oop_ext.foundation import handle_exception
+ handle_exception.HandleException()
+
+ import weakref
+ self._ref = weakref.ref(self, _OnDie)
+
+ def __call__(self, type_):
+ called[0] = True
+ namespace = type_
+ curr = getattr(namespace, '__implements__', None)
+ if curr is not None:
+ all_interfaces = curr + interfaces
+ else:
+ all_interfaces = interfaces
+ namespace.__implements__ = all_interfaces
+
+ if not no_check:
+ if IsDevelopment(): # Only doing check in dev mode.
+ for I in interfaces:
+ # Will do full checking this first time, and also cache the results
+ AssertImplements(type_, I)
+
+ return type_
+
+ def __bool__(self):
+ called[0] = True
+ raise RuntimeError('Invalid attempt to test interface.ImplementsInterface(). Did you '
+ 'mean interface.IsImplementation()?')
+
+ def __nonzero__(self):
+ # Py 2 compatibility
+ return self.__bool__()
+
+ return Check()
+
+
+#===================================================================================================
+# DeclareClassImplements
+#===================================================================================================
+def DeclareClassImplements(class_, *interfaces):
+ '''
+ This is a way to tell, from outside of the class, that a given :arg class_: implements the
+ given :arg interfaces:.
+
+ .. attention:: Use Implements whenever possible
+ This method should be used only when you can't use :py:func:`Implements`, or when you can't
+ change the code of the class being declared, i.e., when you:
+ * Can't add metaclass because the class already has one
+ * Class can't depend on the library where the interface is defined
+ * Class is defined from bindings
+ * Class is defined in an external library
+ * Class is defined by generated code
+
+ :type interfaces: list(Interface)
+ :type class_: type
+
+ :raises BadImplementationError:
+ If, after checking the methods, :arg class_: doesn't really implement the :arg interface:.
+
+ .. note:: Inheritance
+ When you use this method to declare that a base class implements a given interface, you
+ should *also* use this in the derived classes, it does not propagate automatically to
+ the derived classes. See testDeclareClassImplements.
+ '''
+ assert _IsClass(class_)
+
+ from itertools import chain
+
+ old_implements = getattr(class_, '__implements__', [])
+ class_.__implements__ = list(chain(old_implements, interfaces))
+
+ # This check must be done *after* adding the interfaces to __implements__, because it will
+ # also check that the interfaces are declared there.
+ try:
+ for interface in interfaces:
+ # Forget any previous checks
+ __ImplementsCache.ForgetResult((class_, interface))
+ __ImplementedInterfacesCache.ForgetResult(class_)
+
+ AssertImplements(class_, interface)
+ except:
+ # Roll back...
+ class_.__implements__ = old_implements
+ raise
+
+
+#===================================================================================================
+# _GetMROForOldStyleClass
+#===================================================================================================
+def _GetMROForOldStyleClass(class_):
+ '''
+ :type class_: classobj
+ :param class_:
+ An old-style class
+
+ :rtype: list(classobj)
+ :return:
+ A list with all the bases in the older MRO (method resolution order)
+ '''
+
+ def _CalculateMro(class_, mro):
+ for base in class_.__bases__:
+ if base not in mro:
+ mro.append(base)
+ _CalculateMro(base, mro)
+
+ mro = [class_]
+ _CalculateMro(class_, mro)
+ return mro
+
+
+#===================================================================================================
+# _GetClassImplementedInterfaces
+#===================================================================================================
+def _GetClassImplementedInterfaces(class_):
+ cache = __ImplementedInterfacesCache
+ result = cache.GetResult(class_)
+ if result is not None:
+ return result
+
+ result = set()
+
+ mro = inspect.getmro(class_)
+
+ for c in mro:
+ interfaces = getattr(c, '__implements__', ())
+ for interface in interfaces:
+ interface_mro = inspect.getmro(interface)
+
+ for interface_type in interface_mro:
+ if interface_type in (Interface, object):
+ continue
+ result.add(interface_type)
+
+ result = frozenset(result)
+
+ cache.SetResult(class_, result)
+ return result
+
+
+#===================================================================================================
+# GetImplementedInterfaces
+#===================================================================================================
+def GetImplementedInterfaces(class_or_object):
+ '''
+ :rtype: frozenset([interfaces])
+ The interfaces implemented by the object or class passed.
+ '''
+ class_ = _GetClassForInterfaceChecking(class_or_object)
+
+ # we have to build the cache attribute given the name of the class, otherwise setting in a base
+ # class before a subclass may give errors.
+ return _GetClassImplementedInterfaces(class_)
+
+
+#===================================================================================================
+# _IsInterfaceDeclared
+#===================================================================================================
+def _IsInterfaceDeclared(class_, interface):
+ '''
+ :type interface: Interface or iterable(Interface)
+ :param interface:
+ The target interface(s). If multitple interfaces are passed the method will return True
+ if the given class or instance implements any of the given interfaces.
+
+ :rtype: True if the object declares the interface passed and False otherwise. Note that
+ to declare an interface, the class MUST have declared
+
+ >>> ImplementsInterface(Class)
+ '''
+ if class_ is None:
+ return False
+
+ is_collection = False
+ if isinstance(interface, (set, list, tuple)):
+ is_collection = True
+ for i in interface:
+ if not issubclass(i, Interface):
+ raise InterfaceError('To check against an interface, an interface is required (received: %s -- mro:%s)' %
+ (interface, interface.__mro__))
+ elif not issubclass(interface, Interface):
+ raise InterfaceError('To check against an interface, an interface is required (received: %s -- mro:%s)' %
+ (interface, interface.__mro__))
+
+ declared_interfaces = GetImplementedInterfaces(class_)
+
+ # This set will include all interfaces (and its subclasses) declared for the given objec
+ declared_and_subclasses = set()
+ for implemented in declared_interfaces:
+ declared_and_subclasses.update(implemented.__mro__)
+
+ # Discarding object (it will always be returned in the mro collection)
+ declared_and_subclasses.discard(object)
+
+ if not is_collection:
+ return interface in declared_and_subclasses
+ else:
+ return bool(set(interface).intersection(declared_and_subclasses))
+
+#===================================================================================================
+# PROFILING FOR IsInterfaceDeclared
+
+# NOTE: There was code here for profiling IsInterfaceDeclared in revisions prior to 2013-03-19.
+# That code can be useful for seeing where exactly it is being called.
+#===================================================================================================
+
+
+#===================================================================================================
+# AssertDeclaresInterface
+#===================================================================================================
+@Deprecated(AssertImplements)
+def AssertDeclaresInterface(class_or_instance, interface):
+ return AssertImplements(class_or_instance, interface)
diff --git a/src/oop_ext/interface/_tests/__init__.py b/src/oop_ext/interface/_tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/oop_ext/interface/_tests/test_interface.py b/src/oop_ext/interface/_tests/test_interface.py
new file mode 100644
index 0000000..4a256a3
--- /dev/null
+++ b/src/oop_ext/interface/_tests/test_interface.py
@@ -0,0 +1,877 @@
+
+import textwrap
+
+import pytest
+
+from oop_ext.foundation.types_ import Method, Null
+from oop_ext.interface import (
+ AssertImplements, Attribute, BadImplementationError, DeclareClassImplements,
+ GetImplementedInterfaces, IAdaptable, ImplementsInterface, Interface, InterfaceError,
+ InterfaceImplementorStub, IsImplementation, ReadOnlyAttribute)
+
+
+#===================================================================================================
+# _InterfM1
+#===================================================================================================
+class _InterfM1(Interface):
+
+ def m1(self):
+ ''
+
+
+#===================================================================================================
+# _InterfM2
+#===================================================================================================
+class _InterfM2(Interface):
+
+ def m2(self):
+ ''
+
+
+#===================================================================================================
+# _InterfM3
+#===================================================================================================
+class _InterfM3(Interface):
+
+ def m3(self, arg1, arg2):
+ ''
+
+
+#===================================================================================================
+# _InterfM4
+#===================================================================================================
+class _InterfM4(_InterfM3):
+
+ def m4(self):
+ ''
+
+
+def testBasics():
+
+ class I(Interface):
+
+ def foo(self, a, b=None):
+ ''
+
+ def bar(self):
+ ''
+
+ @ImplementsInterface(I)
+ class C:
+
+ def foo(self, a, b=None):
+ ''
+
+ def bar(self):
+ ''
+
+ class C2:
+
+ def foo(self, a, b=None):
+ ''
+
+ def bar(self):
+ ''
+
+ class D:
+ ''
+
+ assert IsImplementation(I(C()), I) == True # OK
+
+ assert IsImplementation(C, I) == True # OK
+ assert IsImplementation(C2, I) == False # Does not declare
+ assert not IsImplementation(D, I) == True # nope
+
+ assert I(C) is C
+ assert I(C2) is C2
+ with pytest.raises(InterfaceError):
+ I()
+
+ with pytest.raises(BadImplementationError):
+ I(D)
+
+ # Now declare that C2 implements I
+ DeclareClassImplements(C2, I)
+
+ assert IsImplementation(C2, I) == True # Does not declare
+
+
+def testMissingMethod():
+
+ class I(Interface):
+
+ def foo(self, a, b=None):
+ ''
+
+ with pytest.raises(AssertionError, match=r"Method 'foo' is missing in class 'C' \(required by interface 'I'\)"):
+
+ @ImplementsInterface(I)
+ class C:
+ pass
+
+ def TestMissingSignature():
+
+ @ImplementsInterface(I)
+ class C:
+
+ def foo(self, a):
+ ''
+
+ with pytest.raises(AssertionError) as e:
+ TestMissingSignature()
+ assert str(e.value) == textwrap.dedent("""
+ Method C.foo signature:
+ (self, a)
+ differs from defined in interface I
+ (self, a, b=None)""")
+
+ def TestMissingSignatureOptional():
+
+ @ImplementsInterface(I)
+ class C:
+
+ def foo(self, a, b):
+ ''
+
+ with pytest.raises(AssertionError) as e:
+ TestMissingSignatureOptional()
+ assert str(e.value) == textwrap.dedent("""
+ Method C.foo signature:
+ (self, a, b)
+ differs from defined in interface I
+ (self, a, b=None)""")
+
+ def TestWrongParameterName():
+
+ @ImplementsInterface(I)
+ class C:
+
+ def foo(self, a, c):
+ ''
+
+ with pytest.raises(AssertionError) as e:
+ TestWrongParameterName()
+ assert str(e.value) == textwrap.dedent("""
+ Method C.foo signature:
+ (self, a, c)
+ differs from defined in interface I
+ (self, a, b=None)""")
+
+
+def testSubclasses():
+
+ class I(Interface):
+
+ def foo(self, a, b=None):
+ ''
+
+ @ImplementsInterface(I)
+ class C:
+
+ def foo(self, a, b=None):
+ ''
+
+ class D(C):
+ ''
+
+
+def testSubclasses2():
+
+ class I(Interface):
+
+ def foo(self):
+ ''
+
+ class I2(Interface):
+
+ def bar(self):
+ ''
+
+ @ImplementsInterface(I)
+ class C:
+
+ def foo(self):
+ ''
+
+ @ImplementsInterface(I2)
+ class D(C):
+
+ def bar(self):
+ ''
+
+ class E(D):
+ ''
+
+ assert GetImplementedInterfaces(C) == {I}
+ assert GetImplementedInterfaces(D) == {I2, I}
+ assert GetImplementedInterfaces(E) == {I2, I}
+
+
+def testNoName():
+
+ class I(Interface):
+
+ def MyMethod(self, foo):
+ ''
+
+ class C:
+
+ def MyMethod(self, bar):
+ ''
+
+ with pytest.raises(AssertionError):
+ AssertImplements(C(), I)
+
+
+def testAttributes():
+
+ class IZoo(Interface):
+ zoo = Attribute(int)
+
+ class I(Interface):
+ foo = Attribute(int)
+ bar = Attribute(str)
+ foobar = Attribute(int, None)
+ a_zoo = Attribute(IZoo)
+
+ @ImplementsInterface(IZoo)
+ class Zoo:
+ pass
+
+ # NOTE: This class 'C' doesn't REALLY implements 'I', although it says so. The problem is
+ # that there's a flaw with attributes *not being checked*.
+
+ # In fact: Attributes should not be in the (Abstract) properties COULD be in
+ # the interface, but they SHOULD NOT be type-checked (because it involves a
+ # getter call, and this affects runtime behaviour).
+ # This should be reviewed later.
+ @ImplementsInterface(I)
+ class C:
+ pass
+
+ c1 = C()
+ c1.foo = 10
+ c1.bar = 'hello'
+ c1.foobar = 20
+
+ a_zoo = Zoo()
+ a_zoo.zoo = 99
+ c1.a_zoo = a_zoo
+
+ c2 = C()
+
+ assert IsImplementation(C, I) == True
+ assert IsImplementation(c1, I) == True
+ assert IsImplementation(c2, I) == True
+
+ # NOTE: Testing private methods here
+ # If we do a deprecated "full check", then its behaviour is a little bit more correct.
+ from oop_ext.interface._interface import _IsImplementationFullChecking
+ assert not _IsImplementationFullChecking(C, I) == True # only works with instances
+ assert _IsImplementationFullChecking(c1, I) == True # OK, has attributes
+ assert not _IsImplementationFullChecking(c2, I) == True # not all the attributes necessary
+
+ # must not be true if including an object that doesn't implement IZoo interface expected for
+ # a_zoo attribute
+ c1.a_zoo = 'wrong'
+ assert not _IsImplementationFullChecking(c1, I) == True # failed, invalid attr type
+ c1.a_zoo = a_zoo
+
+ # test if we can set foobar to None
+ c1.foobar = None
+ assert IsImplementation(c1, I) == True # OK
+
+ c1.foobar = 'hello'
+ assert not _IsImplementationFullChecking(c1, I) == True # failed, invalid attr type
+
+
+def testPassNoneInAssertImplementsFullChecking():
+ with pytest.raises(AssertionError):
+ AssertImplements(None, _InterfM1)
+
+ with pytest.raises(AssertionError):
+ AssertImplements(10, _InterfM1)
+
+
+def testNoCheck():
+
+ @ImplementsInterface(_InterfM1, no_check=True)
+ class NoCheck:
+ pass
+
+ no_check = NoCheck()
+ with pytest.raises(AssertionError):
+ AssertImplements(no_check, _InterfM1)
+
+
+def testCallbackAndInterfaces():
+ '''
+ Tests if the interface "AssertImplements" works with "callbacked" methods.
+ '''
+
+ @ImplementsInterface(_InterfM1)
+ class My:
+
+ def m1(self):
+ ''
+
+ def MyCallback():
+ ''
+
+ from oop_ext.foundation.callback import After
+
+ o = My()
+ AssertImplements(o, _InterfM1)
+
+ After(o.m1, MyCallback)
+
+ AssertImplements(o, _InterfM1)
+ AssertImplements(o, _InterfM1) # Not raises BadImplementationError
+
+
+def testInterfaceStub():
+
+ @ImplementsInterface(_InterfM1)
+ class My:
+
+ def m1(self):
+ return 'm1'
+
+ def m2(self):
+ ''
+
+ m0 = My()
+ m1 = _InterfM1(m0) # will make sure that we only access the attributes/methods declared in the interface
+ assert 'm1' == m1.m1()
+ getattr(m0, 'm2') # Not raises AttributeError
+ with pytest.raises(AttributeError):
+ getattr(m1, 'm2')
+
+ _InterfM1(m1) # Not raise BadImplementationError
+
+
+def testIsImplementationWithSubclasses():
+ '''
+ Checks if the IsImplementation method works with subclasses interfaces.
+
+ Given that an interface I2 inherits from I1 of a given object declared that it implements I2
+ then it is implicitly declaring that implements I1.
+ '''
+
+ @ImplementsInterface(_InterfM2)
+ class My2:
+
+ def m2(self):
+ ''
+
+ @ImplementsInterface(_InterfM3)
+ class My3:
+
+ def m3(self, arg1, arg2):
+ ''
+
+ @ImplementsInterface(_InterfM4)
+ class My4:
+
+ def m3(self, arg1, arg2):
+ ''
+
+ def m4(self):
+ ''
+
+ m2 = My2()
+ m3 = My3()
+ m4 = My4()
+
+ # My2
+ assert IsImplementation(m2, _InterfM3) == False
+
+ # My3
+ assert IsImplementation(m3, _InterfM3) == True
+ assert IsImplementation(m3, _InterfM4) == False
+
+ # My4
+ assert IsImplementation(m4, _InterfM4) == True
+ assert IsImplementation(m4, _InterfM3) == True
+
+ # When wrapped in an m4 interface it should still accept m3 as a declared interface
+ wrapped_intef_m4 = _InterfM4(m4)
+ assert IsImplementation(wrapped_intef_m4, _InterfM4) == True
+ assert IsImplementation(wrapped_intef_m4, _InterfM3) == True
+
+
+def testIsImplementationWithBuiltInObjects():
+
+ my_number = 10
+ assert IsImplementation(my_number, _InterfM1) == False
+
+
+def testClassImplementMethod():
+ '''
+ Tests replacing a method that implements an interface with a class.
+
+ The class must be derived from "Method" in order to be accepted as a valid
+ implementation.
+ '''
+
+ @ImplementsInterface(_InterfM1)
+ class My:
+
+ def m1(self):
+ ''
+
+ class MyRightMethod(Method):
+
+ def __call__(self):
+ ''
+
+ class MyWrongMethod:
+
+ def __call__(self):
+ ''
+
+ # NOTE: It doesn't matter runtime modifications in the instance, what is really being tested
+ # is the *class* of the object (My) is what is really being tested.
+ m = My()
+ m.m1 = MyWrongMethod()
+ assert IsImplementation(m, _InterfM1) == True
+
+ m.m1 = MyRightMethod()
+ assert IsImplementation(m, _InterfM1) == True
+
+ # NOTE: Testing behaviour of private methods here.
+ from oop_ext.interface._interface import _IsImplementationFullChecking
+
+ m = My()
+ m.m1 = MyWrongMethod()
+ r = _IsImplementationFullChecking(m, _InterfM1)
+ assert r == False
+
+ m.m1 = MyRightMethod()
+ r = _IsImplementationFullChecking(m, _InterfM1)
+ assert r == True
+
+ del m.m1
+ assert IsImplementation(m, _InterfM1) == True
+
+
+def testGetImplementedInterfaces():
+
+ @ImplementsInterface(_InterfM1)
+ class A:
+
+ def m1(self):
+ ''
+
+ class B(A):
+ ''
+
+ @ImplementsInterface(_InterfM4)
+ class C:
+
+ def m4(self):
+ ''
+
+ def m3(self, arg1, arg2):
+ ''
+
+ assert 1 == len(GetImplementedInterfaces(B()))
+ assert set(GetImplementedInterfaces(C())) == {_InterfM4, _InterfM3}
+
+
+def testGetImplementedInterfaces2():
+
+ @ImplementsInterface(_InterfM1)
+ class A:
+
+ def m1(self):
+ ''
+
+ @ImplementsInterface(_InterfM2)
+ class B(A):
+
+ def m2(self):
+ ''
+
+ assert 2 == len(GetImplementedInterfaces(B()))
+ with pytest.raises(AssertionError):
+ AssertImplements(A(), _InterfM2)
+
+ AssertImplements(B(), _InterfM2)
+
+
+def testAdaptableInterface():
+
+ @ImplementsInterface(IAdaptable)
+ class A:
+
+ def GetAdapter(self, interface_class):
+ if interface_class == _InterfM1:
+ return B()
+
+ @ImplementsInterface(_InterfM1)
+ class B:
+
+ def m1(self):
+ ''
+
+ a = A()
+ b = _InterfM1(a) # will try to adapt, as it does not directly implements m1
+ assert b is not None
+ b.m1() # has m1
+ with pytest.raises(AttributeError):
+ getattr(b, 'non_existent')
+
+ assert isinstance(b, InterfaceImplementorStub)
+
+
+def testNull():
+ AssertImplements(Null(), _InterfM2) # Not raises BadImplementationError
+
+ class ExtendedNull(Null):
+ ''
+
+ AssertImplements(ExtendedNull(), _InterfM2) # Not raises BadImplementationError
+
+
+def testSetItem():
+
+ class InterfSetItem(Interface):
+
+ def __setitem__(self, item_id, subject):
+ ''
+
+ def __getitem__(self, item_id):
+ ''
+
+ @ImplementsInterface(InterfSetItem)
+ class A:
+
+ def __setitem__(self, item_id, subject):
+ self.set = (item_id, subject)
+
+ def __getitem__(self, item_id):
+ return self.set
+
+ a = InterfSetItem(A())
+ a['10'] = 1
+ assert ('10', 1) == a['10']
+
+
+def testAssertImplementsDoesNotDirObject():
+ '''
+ AssertImplements does not attempt to __getattr__ methods from an object, it only considers
+ methods that are actually bound to the class.
+ '''
+
+ class M1:
+
+ def __getattr__(self, attr):
+ assert attr == 'm1' # This test only accepts this attribute
+
+ class MyMethod(Method):
+
+ def __call__(self):
+ ''
+
+ return MyMethod()
+
+ m1 = M1()
+ m1.m1()
+ with pytest.raises(AssertionError):
+ AssertImplements(m1, _InterfM1)
+
+
+def testImplementorWithAny():
+ '''
+ You must explicitly declare that you implement an Interface.
+ '''
+
+ class M3:
+
+ def m3(self, *args, **kwargs):
+ ''
+
+ with pytest.raises(AssertionError):
+ AssertImplements(M3(), _InterfM3)
+
+
+def testInterfaceCheckRequiresInterface():
+
+ class M3:
+
+ def m3(self, *args, **kwargs):
+ ''
+
+ with pytest.raises(InterfaceError):
+ AssertImplements(M3(), M3)
+
+ with pytest.raises(InterfaceError):
+ IsImplementation(M3(), M3)
+
+
+def testReadOnlyAttribute():
+
+ class IZoo(Interface):
+ zoo = ReadOnlyAttribute(int)
+
+ @ImplementsInterface(IZoo)
+ class Zoo:
+
+ def __init__(self, value):
+ self.zoo = value
+
+ a_zoo = Zoo(value=99)
+ AssertImplements(a_zoo, IZoo)
+
+
+def testReadOnlyAttributeMissingImplementation():
+ '''
+ First implementation of changes in interfaces to support read-only attributes was not
+ including read-only attributes when AssertImplements was called.
+
+ This caused missing read-only attributes to go unnoticed and sometimes false positives,
+ recognizing objects as valid implementations when in fact they weren't.
+ '''
+
+ class IZoo(Interface):
+ zoo = ReadOnlyAttribute(int)
+
+ # Doesn't have necessary 'zoo' attribute, should raise a bad implementation error
+ @ImplementsInterface(IZoo)
+ class FlawedZoo:
+
+ def __init__(self, value):
+ ''
+
+ # NOTE: Testing private methods here
+ from oop_ext.interface._interface import _AssertImplementsFullChecking
+
+ a_flawed_zoo = FlawedZoo(value=101)
+ with pytest.raises(BadImplementationError):
+ _AssertImplementsFullChecking(a_flawed_zoo, IZoo)
+
+
+def testImplementsTwice():
+
+ class I1(Interface):
+
+ def Method1(self):
+ ''
+
+ class I2(Interface):
+
+ def Method2(self):
+ ''
+
+ def Create():
+
+ @ImplementsInterface(I1)
+ @ImplementsInterface(I2)
+ class Foo:
+
+ def Method2(self):
+ ''
+
+ # Error because I1 is not implemented.
+ with pytest.raises(AssertionError):
+ Create()
+
+
+def testDeclareClassImplements():
+
+ class I1(Interface):
+
+ def M1(self):
+ ''
+
+ class I2(Interface):
+
+ def M2(self):
+ ''
+
+ class C0:
+ ''
+
+ class C1:
+
+ def M1(self):
+ ''
+
+ class C2:
+
+ def M2(self):
+ ''
+
+ @ImplementsInterface(I2)
+ class C2B:
+
+ def M2(self):
+ ''
+
+ class C12B(C1, C2B):
+ ''
+
+ with pytest.raises(AssertionError):
+ DeclareClassImplements(C0, I1)
+
+ assert IsImplementation(C1, I1) == False
+ with pytest.raises(AssertionError):
+ AssertImplements(C1, I1)
+
+ assert IsImplementation(C12B, I1) == False # C1 still does not implements I1
+
+ DeclareClassImplements(C1, I1)
+
+ assert IsImplementation(C1, I1) == True
+ AssertImplements(C1, I1)
+
+ # C1 is parent of C12B, and, above, it was declared that C1 implements I1, so C12B should
+ # automatically implement I1. But this is not automatic, so you must also declare for it!
+
+ assert IsImplementation(C12B, I1) == False # not automatic
+ assert IsImplementation(C12B, I2) == True # inheritance for Implements still works automatically
+
+ DeclareClassImplements(C12B, I1)
+
+ assert IsImplementation(C12B, I1) == True # now it implements
+ assert IsImplementation(C12B, I2) == True
+
+ DeclareClassImplements(C2, I2)
+
+ assert IsImplementation(C2, I2) == True
+ AssertImplements(C2, I2)
+
+ # Exception: if I define a class *after* using DeclareClassImplements in the base, it works:
+ class C12(C1, C2):
+ ''
+
+ AssertImplements(C12, I1)
+ AssertImplements(C12, I2)
+
+
+def testCallableInterfaceStub():
+ '''
+ Validates that is possible to create stubs for interfaces of callables (i.e. declaring
+ __call__ method in interface).
+
+ If a stub for a interface not declared as callable is tried to be executed as callable it
+ raises an error.
+ '''
+
+ # ok, calling a stub for a callable
+ class IFoo(Interface):
+
+ def __call__(self, bar):
+ ''
+
+ @ImplementsInterface(IFoo)
+ class Foo:
+
+ def __call__(self, bar):
+ ''
+
+ foo = Foo()
+ stub = IFoo(foo)
+ stub(bar=None) # NotRaises TypeError
+
+ # wrong, calling a stub for a non-callable
+ class IBar(Interface):
+
+ def something(self, stuff):
+ ''
+
+ @ImplementsInterface(IBar)
+ class Bar:
+
+ def something(self, stuff):
+ ''
+
+ bar = Bar()
+ stub = IBar(bar)
+ with pytest.raises(AttributeError):
+ stub(stuff=None)
+
+
+def testImplementsInterfaceAsBoolError():
+ '''
+ Test if the common erroneous use of interface.ImplementsInterface() instead of
+ interface.IsImplementation() to test if an object implements an interface correctly
+ raises a RuntimeError.
+ '''
+
+ class I1(Interface):
+
+ def M1(self):
+ pass
+
+ @ImplementsInterface(I1)
+ class C1:
+
+ def M1(self):
+ pass
+
+ obj = C1()
+
+ assert IsImplementation(obj, I1)
+
+ with pytest.raises(RuntimeError):
+ if ImplementsInterface(obj, I1):
+ pytest.fail('Managed to test "if ImplementsInterface(obj, I1):"')
+
+
+@pytest.mark.parametrize('check_before', [True, False])
+@pytest.mark.parametrize('autospec', [True, False])
+def test_interface_subclass_mocked(mocker, check_before, autospec):
+ """
+ Interfaces check implementation during class declaration. However a
+ subclass doesn't have its implementation checked.
+
+ Then if there is an implementation AFTER subclass is already mocked
+ the interface check will only work if `autospec` is used.
+
+ Note that interface implementation checks keeps a cache, so if
+ subclass was checked before it was mocked it would pass anyway.
+
+ :type mocker: MockTestFixture
+ :type check_before: bool
+ :type autospec: bool
+ """
+ from oop_ext import interface
+
+ class II(interface.Interface):
+
+ def foo(self, a, b, c):
+ pass
+
+ @interface.ImplementsInterface(II)
+ class Foo:
+
+ def foo(self, a, b, c):
+ pass
+
+ class Bar(Foo):
+ pass
+
+ if check_before:
+ interface.IsImplementation(Bar, II)
+
+ mocker.patch.object(Foo, 'foo', autospec=autospec)
+
+ assert interface.IsImplementation(Bar, II) == (autospec or check_before)
+
+@pytest.mark.xfail(run=False, reason="core dumped")
+def testErrorOnInterfaceDeclaration(handled_exceptions):
+
+ def Check():
+
+ class Foo:
+
+ from oop_ext import interface
+ interface.ImplementsInterface(_InterfM1)
+
+ from oop_ext.foundation.handle_exception import IgnoringHandleException
+ with IgnoringHandleException():
+ Check()
+ assert len(handled_exceptions.GetHandledExceptions()) == 1
+ handled_exceptions.ClearHandledExceptions()
diff --git a/src/oop_ext/main.py b/src/oop_ext/main.py
deleted file mode 100644
index 0992e31..0000000
--- a/src/oop_ext/main.py
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-def package_name():
- return 'oop_ext'
diff --git a/test/test_main.py b/test/test_main.py
deleted file mode 100644
index 1ede053..0000000
--- a/test/test_main.py
+++ /dev/null
@@ -1,3 +0,0 @@
-def test_main():
- from oop_ext import main
- assert main.package_name() == 'oop_ext'
diff --git a/tox.ini b/tox.ini
index 61eefdc..fd27910 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,11 +1,10 @@
[tox]
-envlist = py27, py36, linting, docs
+envlist = py36, linting, docs
[travis]
python =
3.6: py36
3.5: py35
- 2.7: py27
[testenv]
passenv = TOXENV CI TRAVIS TRAVIS_* APPVEYOR APPVEYOR_*
@@ -28,7 +27,3 @@ extras = docs
commands =
sphinx-build -W -b html . _build
-
-[flake8]
-max-line-length = 120
-ignore = E203,W503