Skip to content

Commit

Permalink
Reach 100% coverage and add coveralls
Browse files Browse the repository at this point in the history
This process revealed a bug in the Python implementation of
Base_getattro: it didn't support data descriptors due to a typo. I
fixed that.

Currently based on #20 because that's where I started the process.
  • Loading branch information
jamadden committed Aug 10, 2018
1 parent 93842c7 commit 2934bfd
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 71 deletions.
4 changes: 2 additions & 2 deletions .gitignore
Expand Up @@ -15,5 +15,5 @@ parts
.installed.cfg
develop-eggs/
eggs/
nosetests.xml
src/coverage.xml

htmlcov/
5 changes: 4 additions & 1 deletion .travis.yml
Expand Up @@ -16,9 +16,12 @@ matrix:
sudo: true
install:
- pip install -U pip setuptools
- pip install -U coverage coveralls
- pip install -U -e .[test]
script:
- zope-testrunner --test-path=src
- coverage run -m zope.testrunner --test-path=src
after_success:
- coveralls
notifications:
email: false
cache: pip
5 changes: 5 additions & 0 deletions CHANGES.rst
Expand Up @@ -10,6 +10,11 @@ Changelog

- Add support for Python 3.7.

- Fix getting attributes that are data descriptors in the Python
implementation.

- Reach and automatically maintain 100% test coverage.

4.3.0 (2017-02-22)
------------------

Expand Down
14 changes: 6 additions & 8 deletions src/ComputedAttribute/tests.py
Expand Up @@ -94,17 +94,15 @@ def test_computed_attribute_on_class_level1(self):
self.assertIsInstance(x.a, ComputedAttribute)

def test_compilation(self):
from ExtensionClass import C_EXTENSION
if C_EXTENSION:
from ExtensionClass import _IS_PYPY
try:
from ComputedAttribute import _ComputedAttribute
self.assertTrue(hasattr(_ComputedAttribute, 'ComputedAttribute'))
except ImportError: # pragma: no cover
self.assertTrue(_IS_PYPY)
else:
with self.assertRaises((AttributeError, ImportError)):
from ComputedAttribute import _ComputedAttribute

self.assertTrue(hasattr(_ComputedAttribute, 'ComputedAttribute'))

def test_suite():
suite = unittest.TestSuite()
suite = unittest.defaultTestLoader.loadTestsFromName(__name__)
suite.addTest(DocTestSuite())
suite.addTest(unittest.makeSuite(TestComputedAttribute))
return suite
26 changes: 16 additions & 10 deletions src/ExtensionClass/__init__.py
Expand Up @@ -103,12 +103,12 @@ class init called
import platform
import sys

if sys.version_info > (3, ): # pragma: no cover
if sys.version_info > (3, ):
import copyreg as copy_reg
else:
else: # pragma: no cover
import copy_reg

_IS_PYPY = getattr(platform, 'python_implementation', lambda: None)() == 'PyPy'
_IS_PYPY = platform.python_implementation() == 'PyPy'
_IS_PURE = 'PURE_PYTHON' in os.environ
C_EXTENSION = not (_IS_PYPY or _IS_PURE)

Expand Down Expand Up @@ -148,9 +148,8 @@ def _add_classic_mro(mro, cls):

class ExtensionClass(type):

def __new__(cls, name, bases=(), attrs={}):
global _Base, _NoInstanceDictionaryBase

def __new__(cls, name, bases=(), attrs=None):
attrs = {} if attrs is None else attrs
# Make sure we have an ExtensionClass instance as a base
if (name != 'Base' and
not any(isinstance(b, ExtensionClass) for b in bases)):
Expand Down Expand Up @@ -180,10 +179,9 @@ def __basicnew__(self):
"""Create a new empty object"""
return self.__new__(self)


def mro(self):
"""Compute an mro using the 'encapsulated base' scheme"""
global _Base, _NoInstanceDictionaryBase

mro = [self]
for base in self.__bases__:
if hasattr(base, '__mro__'):
Expand All @@ -193,7 +191,7 @@ def mro(self):
if c in mro:
continue
mro.append(c)
else:
else: # pragma: no cover (python 2 only)
_add_classic_mro(mro, base)

if _NoInstanceDictionaryBase in self.__bases__:
Expand Down Expand Up @@ -232,7 +230,7 @@ def Base_getattro(self, name):
descr = base.__dict__[name]
break

if descr is not None and inspect.isdatadescriptor(base):
if descr is not None and inspect.isdatadescriptor(descr):
return descr.__get__(self, type(self))

try:
Expand Down Expand Up @@ -335,3 +333,11 @@ class NoInstanceDictionaryBase(Base):

if C_EXTENSION: # pragma no cover
from ._ExtensionClass import * # NOQA

# We always want to get the CAPI2 value (if possible) so that
# MethodObject and anything else using the PyExtensionClass_Export
# macro from ExtensionClass.h doesn't break with an AttributeError
try:
from ._ExtensionClass import CAPI2
except ImportError: # pragma: no cover
pass
69 changes: 52 additions & 17 deletions src/ExtensionClass/tests.py
Expand Up @@ -168,14 +168,10 @@ def test__basicnew__():
"""


def cmpattrs(self, other, *attrs):
for attr in attrs:
if attr[:3] in ('_v_', '_p_'):
continue
c = getattr(self, attr, None) == getattr(other, attr, None)
if c:
return c
return 0
def eqattrs(self, other, *attrs):
self_data = [getattr(self, a, None) for a in attrs]
other_data = [getattr(other, a, None) for a in attrs]
return self_data == other_data


class Simple(Base):
Expand All @@ -186,7 +182,7 @@ def __init__(self, name, **kw):
self._p_foo = 'bar'

def __eq__(self, other):
return cmpattrs(self, other, '__class__', *(self.__dict__.keys()))
return eqattrs(self, other, '__class__', *(self.__dict__.keys()))


def test_basic_pickling():
Expand Down Expand Up @@ -292,7 +288,7 @@ def __init__(self, s1, s2, s3):
self.s3 = s3

def __eq__(self, other):
return cmpattrs(self, other, '__class__', 's1', 's2', 's3', 's4')
return eqattrs(self, other, '__class__', 's1', 's2', 's3', 's4')


def test_pickling_w_slots_only():
Expand Down Expand Up @@ -346,7 +342,7 @@ def __init__(self, s1, s2, s3, **kw):
self._p_foo = 'bar'

def __eq__(self, other):
return cmpattrs(self, other,
return eqattrs(self, other,
'__class__', 's1', 's2', 's3', 's4',
*(self.__dict__.keys()))

Expand Down Expand Up @@ -967,7 +963,7 @@ class _Derived(_One, _Another):
self._callFUT(mro, _Derived)
self.assertEqual(mro, [_Derived, _One, _Base, object, _Another])

@unittest.skipIf(sys.version_info[0] > 2, 'Py3k: no classic classes')

def test_w_filled_mro_oldstyle_class_w_bases(self):

class _Base:
Expand All @@ -979,19 +975,58 @@ class _Derived(_Base):
already = object()
mro = [already]
self._callFUT(mro, _Derived)
self.assertEqual(mro, [already, _Derived, _Base])
self.assertEqual(
mro,
[already, _Derived, _Base] + ([object] if sys.version_info[0] > 2 else []))


class TestExtensionClass(unittest.TestCase):

def test_compilation(self):
from ExtensionClass import _IS_PYPY
if not _IS_PYPY:
try:
from ExtensionClass import _ExtensionClass
self.assertTrue(hasattr(_ExtensionClass, 'CAPI2'))
except ImportError: # pragma: no cover
self.assertTrue(_IS_PYPY)
else:
with self.assertRaises((AttributeError, ImportError)):
from ExtensionClass import _ExtensionClass
self.assertTrue(hasattr(_ExtensionClass, 'CAPI2'))


def test_mro_classic_class(self):

class _Base:
pass

class _Derived(_Base, ExtensionClass):
pass


self.assertEqual(_Derived.__mro__,
(_Derived, _Base, ExtensionClass, type, object))

def test_class_init(self):
class _Derived(ExtensionClass):
init = 0
def __class_init__(cls):
cls.init = 1
Derived = _Derived('Derived', (), {})
self.assertEqual(0, _Derived.init)
self.assertEqual(1, Derived.init)

class TestBase(unittest.TestCase):

def test_data_descriptor(self):
class Descr(object):
def __get__(self, inst, klass):
return (inst, klass)
def __set__(self, value):
"Does nothing, needed to be a data descriptor"

class O(Base):
attr = Descr()

o = O()
self.assertEqual(o.attr, (o, O))


def test_suite():
Expand Down
55 changes: 25 additions & 30 deletions src/MethodObject/tests.py
Expand Up @@ -12,45 +12,40 @@
#
##############################################################################

from doctest import DocTestSuite
import unittest


def test_methodobject():
"""
>>> from ExtensionClass import Base
>>> from MethodObject import Method
class TestMethodObject(unittest.TestCase):

>>> class foo(Method):
... def __call__(self, ob, *args, **kw):
... print('called %s %s %s' % (ob, args, kw))
def test_compilation(self):
from ExtensionClass import _IS_PYPY
try:
from MethodObject import _MethodObject
except ImportError: # pragma: no cover
self.assertTrue(_IS_PYPY)
else:
self.assertTrue(hasattr(_MethodObject, 'Method'))

>>> class bar(Base):
... def __repr__(self):
... return "bar()"
... hi = foo()
def test_methodobject(self):
from ExtensionClass import Base
from MethodObject import Method

>>> x = bar()
>>> hi = x.hi
>>> hi(1,2,3,name='spam')
called bar() (1, 2, 3) {'name': 'spam'}
"""
class Callable(Method):
def __call__(self, ob, *args, **kw):
return (repr(ob), args, kw)

class ExClass(Base):
def __repr__(self):
return "bar()"

class TestMethodObject(unittest.TestCase):
hi = Callable()

def test_compilation(self):
from ExtensionClass import C_EXTENSION
if C_EXTENSION:
from MethodObject import _MethodObject
self.assertTrue(hasattr(_MethodObject, 'Method'))
else:
with self.assertRaises((AttributeError, ImportError)):
from MethodObject import _MethodObject
x = ExClass()
hi = x.hi
result = hi(1, 2, 3, name='spam')

self.assertEqual(result,
("bar()", (1, 2, 3), {'name': 'spam'}))

def test_suite():
suite = unittest.TestSuite()
suite.addTest(DocTestSuite())
suite.addTest(unittest.makeSuite(TestMethodObject))
return suite
return unittest.defaultTestLoader.loadTestsFromName(__name__)
5 changes: 2 additions & 3 deletions tox.ini
Expand Up @@ -36,11 +36,10 @@ setenv =
[testenv:coverage]
usedevelop = true
basepython =
python2.7
python3.6
commands =
coverage erase
coverage run -m zope.testrunner --test-path=src
coverage report
coverage report --fail-under=100
deps =
{[testenv]deps}
coverage
Expand Down

0 comments on commit 2934bfd

Please sign in to comment.