Skip to content

Commit

Permalink
100% test coverage on Python 3
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
jamadden committed Aug 13, 2018
1 parent 38d449d commit d18e4ee
Show file tree
Hide file tree
Showing 7 changed files with 54 additions and 37 deletions.
8 changes: 8 additions & 0 deletions CHANGES.rst
Expand Up @@ -7,6 +7,14 @@

- Add support for Python 3.7.

- ``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
Expand Down
13 changes: 8 additions & 5 deletions src/zope/schema/_bootstrapinterfaces.py
Expand Up @@ -13,8 +13,10 @@
##############################################################################
"""Bootstrap schema interfaces and exceptions
"""
from functools import total_ordering
import zope.interface


from zope.schema._messageid import _


Expand All @@ -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'):
Expand All @@ -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):
Expand Down
42 changes: 25 additions & 17 deletions src/zope/schema/_field.py
Expand Up @@ -369,14 +369,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:
Expand Down Expand Up @@ -404,21 +425,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):
Expand All @@ -428,15 +445,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):
Expand Down
2 changes: 1 addition & 1 deletion src/zope/schema/tests/__init__.py
Expand Up @@ -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'"),
Expand Down
21 changes: 9 additions & 12 deletions src/zope/schema/tests/test__bootstrapinterfaces.py
Expand Up @@ -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):

Expand All @@ -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()
Expand Down
3 changes: 2 additions & 1 deletion src/zope/schema/tests/test__field.py
Expand Up @@ -1143,7 +1143,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')

Expand All @@ -1153,6 +1152,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:')

Expand Down Expand Up @@ -1231,6 +1231,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:')

Expand Down
2 changes: 1 addition & 1 deletion tox.ini
Expand Up @@ -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
Expand Down

0 comments on commit d18e4ee

Please sign in to comment.