From aea680ac046c184d5cabf870cfe060235b1711af Mon Sep 17 00:00:00 2001 From: Jason Madden Date: Mon, 13 Aug 2018 10:33:29 -0500 Subject: [PATCH] 100% test coverage on Python 3 - Fix sorting ValidationError on Python 3 to work like Python 2. - Fix DottedName and Id to raise the same exception for invalid unicode characters on Python 2 and Python 3. --- CHANGES.rst | 9 ++++ src/zope/schema/_bootstrapinterfaces.py | 13 +++--- src/zope/schema/_field.py | 42 +++++++++++-------- src/zope/schema/tests/__init__.py | 2 +- .../schema/tests/test__bootstrapinterfaces.py | 21 ++++------ src/zope/schema/tests/test__field.py | 3 +- tox.ini | 2 +- 7 files changed, 55 insertions(+), 37 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 72abcad..efe42c6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -13,6 +13,14 @@ ``validate_invariants=False`` to the ``Object`` constructor. See `issue 10 `_. +- ``ValidationError`` can be sorted on Python 3. + +- ``DottedName`` and ``Id`` consistently handle non-ASCII unicode + values on Python 2 and 3 by raising ``InvalidDottedName`` and + ``InvalidId`` in ``fromUnicode`` respectively. Previously, a + ``UnicodeEncodeError`` would be raised on Python 2 while Python 3 + would raise the descriptive exception. + - ``Field`` instances are hashable on Python 3, and use a defined hashing algorithm that matches what equality does on all versions of Python. Previously, on Python 2, fields were hashed based on their @@ -34,6 +42,7 @@ `_ and `PR 6 `_. + 4.5.0 (2017-07-10) ================== diff --git a/src/zope/schema/_bootstrapinterfaces.py b/src/zope/schema/_bootstrapinterfaces.py index acb9a23..e249744 100644 --- a/src/zope/schema/_bootstrapinterfaces.py +++ b/src/zope/schema/_bootstrapinterfaces.py @@ -13,8 +13,10 @@ ############################################################################## """Bootstrap schema interfaces and exceptions """ +from functools import total_ordering import zope.interface + from zope.schema._messageid import _ @@ -25,17 +27,17 @@ class StopValidation(Exception): a way for the validator to save time. """ - +@total_ordering class ValidationError(zope.interface.Invalid): """Raised if the Validation process fails.""" def doc(self): return self.__class__.__doc__ - def __cmp__(self, other): + def __lt__(self, other): if not hasattr(other, 'args'): return -1 - return cmp(self.args, other.args) + return self.args < other.args def __eq__(self, other): if not hasattr(other, 'args'): @@ -45,8 +47,9 @@ def __eq__(self, other): __hash__ = zope.interface.Invalid.__hash__ # python3 def __repr__(self): # pragma: no cover - return '%s(%s)' % (self.__class__.__name__, - ', '.join(repr(arg) for arg in self.args)) + return '%s(%s)' % ( + self.__class__.__name__, + ', '.join(repr(arg) for arg in self.args)) class RequiredMissing(ValidationError): diff --git a/src/zope/schema/_field.py b/src/zope/schema/_field.py index 0d02e40..3857a54 100644 --- a/src/zope/schema/_field.py +++ b/src/zope/schema/_field.py @@ -370,14 +370,35 @@ def fromUnicode(self, value): # use the whole line r"$").match +class _StrippedNativeStringLine(NativeStringLine): + + _invalid_exc_type = None + + def fromUnicode(self, value): + v = value.strip() + # On Python 2, self._type is bytes, so we need to encode + # unicode down to ASCII bytes. On Python 3, self._type is + # unicode, but we don't want to allow non-ASCII values, to match + # Python 2 (our regexs would reject that anyway.) + try: + v = v.encode('ascii') # bytes + except UnicodeEncodeError: + raise self._invalid_exc_type(value) + if not isinstance(v, self._type): + v = v.decode('ascii') + self.validate(v) + return v + @implementer(IDottedName) -class DottedName(NativeStringLine): +class DottedName(_StrippedNativeStringLine): """Dotted name field. Values of DottedName fields must be Python-style dotted names. """ + _invalid_exc_type = InvalidDottedName + def __init__(self, *args, **kw): self.min_dots = int(kw.pop("min_dots", 0)) if self.min_dots < 0: @@ -405,21 +426,17 @@ def _validate(self, value): raise InvalidDottedName("too many dots; no more than %d allowed" % self.max_dots, value) - def fromUnicode(self, value): - v = value.strip() - if not isinstance(v, self._type): - v = v.encode('ascii') - self.validate(v) - return v @implementer(IId, IFromUnicode) -class Id(NativeStringLine): +class Id(_StrippedNativeStringLine): """Id field Values of id fields must be either uris or dotted names. """ + _invalid_exc_type = InvalidId + def _validate(self, value): super(Id, self)._validate(value) if _isuri(value): @@ -429,15 +446,6 @@ def _validate(self, value): raise InvalidId(value) - def fromUnicode(self, value): - """ See IFromUnicode. - """ - v = value.strip() - if not isinstance(v, self._type): - v = v.encode('ascii') - self.validate(v) - return v - @implementer(IInterfaceField) class InterfaceField(Field): diff --git a/src/zope/schema/tests/__init__.py b/src/zope/schema/tests/__init__.py index 58ecb2c..b7d1a56 100644 --- a/src/zope/schema/tests/__init__.py +++ b/src/zope/schema/tests/__init__.py @@ -42,7 +42,7 @@ (re.compile(r"zope.schema._bootstrapinterfaces.WrongType:"), r"WrongType:"), ]) -else: +else: # pragma: no cover py3_checker = renormalizing.RENormalizing([ (re.compile(r"([^'])b'([^']*)'"), r"\1'\2'"), diff --git a/src/zope/schema/tests/test__bootstrapinterfaces.py b/src/zope/schema/tests/test__bootstrapinterfaces.py index 6d11334..52c8c8c 100644 --- a/src/zope/schema/tests/test__bootstrapinterfaces.py +++ b/src/zope/schema/tests/test__bootstrapinterfaces.py @@ -13,10 +13,11 @@ ############################################################################## import unittest - -def _skip_under_py3(testcase): - from zope.schema._compat import PY3 - return unittest.skipIf(PY3, "Not under Python 3")(testcase) +try: + compare = cmp +except NameError: + def compare(a, b): + return -1 if a < b else (0 if a == b else 1) class ValidationErrorTests(unittest.TestCase): @@ -33,20 +34,16 @@ class Derived(self._getTargetClass()): inst = Derived() self.assertEqual(inst.doc(), 'DERIVED') - @_skip_under_py3 def test___cmp___no_args(self): - # Py3k?? ve = self._makeOne() - self.assertEqual(cmp(ve, object()), -1) + self.assertEqual(compare(ve, object()), -1) - @_skip_under_py3 def test___cmp___hit(self): - # Py3k?? left = self._makeOne('abc') right = self._makeOne('def') - self.assertEqual(cmp(left, right), -1) - self.assertEqual(cmp(left, left), 0) - self.assertEqual(cmp(right, left), 1) + self.assertEqual(compare(left, right), -1) + self.assertEqual(compare(left, left), 0) + self.assertEqual(compare(right, left), 1) def test___eq___no_args(self): ve = self._makeOne() diff --git a/src/zope/schema/tests/test__field.py b/src/zope/schema/tests/test__field.py index 7ce5ab4..09456fc 100644 --- a/src/zope/schema/tests/test__field.py +++ b/src/zope/schema/tests/test__field.py @@ -1152,7 +1152,6 @@ def test_validate_not_a_dotted_name(self): field.validate, 'http://example.com/\nDAV:') def test_fromUnicode_dotted_name_ok(self): - field = self._makeOne() self.assertEqual(field.fromUnicode(u'dotted.name'), 'dotted.name') @@ -1162,6 +1161,7 @@ def test_fromUnicode_invalid(self): field = self._makeOne() self.assertRaises(InvalidDottedName, field.fromUnicode, u'') + self.assertRaises(InvalidDottedName, field.fromUnicode, u'\u2603') self.assertRaises(ConstraintNotSatisfied, field.fromUnicode, u'http://example.com/\nDAV:') @@ -1240,6 +1240,7 @@ def test_fromUnicode_invalid(self): field = self._makeOne() self.assertRaises(InvalidId, field.fromUnicode, u'') self.assertRaises(InvalidId, field.fromUnicode, u'abc') + self.assertRaises(InvalidId, field.fromUnicode, u'\u2603') self.assertRaises(ConstraintNotSatisfied, field.fromUnicode, u'http://example.com/\nDAV:') diff --git a/tox.ini b/tox.ini index 604820d..8012429 100644 --- a/tox.ini +++ b/tox.ini @@ -17,7 +17,7 @@ basepython = python3.6 commands = coverage run -m zope.testrunner --test-path=src - coverage report + coverage report --fail-under=100 deps = .[test] coverage