Skip to content

Commit

Permalink
Merge pull request #71 from zopefoundation/feature/IFromBytes
Browse files Browse the repository at this point in the history
Add IFromBytes
  • Loading branch information
jamadden committed Sep 19, 2018
2 parents 7d715ed + b25afd5 commit b5964f5
Show file tree
Hide file tree
Showing 8 changed files with 298 additions and 71 deletions.
6 changes: 4 additions & 2 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
Changes
=========

4.7.1 (unreleased)
4.8.0 (unreleased)
==================

- Nothing changed yet.
- Add the interface ``IFromBytes``, which is implemented by the
numeric and bytes fields, as well as ``URI``, ``DottedName``, and
``Id``.


4.7.0 (2018-09-11)
Expand Down
125 changes: 110 additions & 15 deletions src/zope/schema/_bootstrapfields.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from zope.schema._bootstrapinterfaces import ConstraintNotSatisfied
from zope.schema._bootstrapinterfaces import IBeforeObjectAssignedEvent
from zope.schema._bootstrapinterfaces import IContextAwareDefaultFactory
from zope.schema._bootstrapinterfaces import IFromBytes
from zope.schema._bootstrapinterfaces import IFromUnicode
from zope.schema._bootstrapinterfaces import IValidatable
from zope.schema._bootstrapinterfaces import NotAContainer
Expand All @@ -55,6 +56,7 @@

from zope.schema._compat import text_type
from zope.schema._compat import integer_types
from zope.schema._compat import PY2


class _NotGiven(object):
Expand Down Expand Up @@ -564,8 +566,14 @@ def validate(self, value):
return super(Password, self).validate(value)


@implementer(IFromUnicode, IFromBytes)
class Bool(Field):
"""A field representing a Bool."""
"""
A field representing a Bool.
.. versionchanged:: 4.8.0
Implement :class:`zope.schema.interfaces.IFromBytes`
"""

_type = bool

Expand All @@ -581,7 +589,7 @@ def set(self, object, value):
value = bool(value)
Field.set(self, object, value)

def fromUnicode(self, str):
def fromUnicode(self, value):
"""
>>> from zope.schema._bootstrapfields import Bool
>>> from zope.schema.interfaces import IFromUnicode
Expand All @@ -596,15 +604,42 @@ def fromUnicode(self, str):
True
>>> b.fromUnicode('false') or b.fromUnicode('False')
False
>>> b.fromUnicode(u'\u2603')
False
"""
v = str == 'True' or str == 'true'
# On Python 2, we're relying on the implicit decoding
# that happens during string comparisons of unicode to native
# (byte) strings; decoding errors are silently dropped
v = value == 'True' or value == 'true'
self.validate(v)
return v

def fromBytes(self, value):
"""
>>> from zope.schema._bootstrapfields import Bool
>>> from zope.schema.interfaces import IFromBytes
>>> b = Bool()
>>> IFromBytes.providedBy(b)
True
>>> b.fromBytes(b'True')
True
>>> b.fromBytes(b'')
False
>>> b.fromBytes(b'true')
True
>>> b.fromBytes(b'false') or b.fromBytes(b'False')
False
>>> b.fromBytes(u'\u2603'.encode('utf-8'))
False
"""
return self.fromUnicode(value.decode("utf-8"))


class InvalidNumberLiteral(ValueError, ValidationError):
"""Invalid number literal."""

@implementer(IFromUnicode)

@implementer(IFromUnicode, IFromBytes)
class Number(Orderable, Field):
"""
A field representing a :class:`numbers.Number` and implementing
Expand All @@ -615,27 +650,49 @@ class Number(Orderable, Field):
>>> from zope.schema._bootstrapfields import Number
>>> f = Number()
>>> f.fromUnicode("1")
>>> f.fromUnicode(u"1")
1
>>> f.fromUnicode("125.6")
>>> f.fromUnicode(u"125.6")
125.6
>>> f.fromUnicode("1+0j")
>>> f.fromUnicode(u"1+0j")
(1+0j)
>>> f.fromUnicode("1/2")
>>> f.fromUnicode(u"1/2")
Fraction(1, 2)
>>> f.fromUnicode(str(2**31234) + '.' + str(2**256)) # doctest: +ELLIPSIS
Decimal('234...936')
>>> f.fromUnicode("not a number") # doctest: +IGNORE_EXCEPTION_DETAIL
>>> f.fromUnicode(u"not a number") # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
InvalidNumberLiteral: Invalid literal for Decimal: 'not a number'
Similarly, :meth:`fromBytes` will do the same for incoming byte strings::
>>> from zope.schema._bootstrapfields import Number
>>> f = Number()
>>> f.fromBytes(b"1")
1
>>> f.fromBytes(b"125.6")
125.6
>>> f.fromBytes(b"1+0j")
(1+0j)
>>> f.fromBytes(b"1/2")
Fraction(1, 2)
>>> f.fromBytes((str(2**31234) + '.' + str(2**256)).encode('ascii')) # doctest: +ELLIPSIS
Decimal('234...936')
>>> f.fromBytes(b"not a number") # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
InvalidNumberLiteral: Invalid literal for Decimal: 'not a number'
.. versionadded:: 4.6.0
.. versionchanged:: 4.8.0
Implement :class:`zope.schema.interfaces.IFromBytes`
"""
_type = numbers.Number

# An ordered sequence of conversion routines. These should accept
# a string and produce an object that is an instance of `_type`, or raise
# a native string and produce an object that is an instance of `_type`, or raise
# a ValueError. The order should be most specific/strictest towards least
# restrictive (in other words, lowest in the numeric tower towards highest).
# We break this rule with fractions, though: a floating point number is
Expand Down Expand Up @@ -664,6 +721,14 @@ def fromUnicode(self, value):
finally:
last_exc = None

# On Python 2, native strings are byte strings, which is
# what the converters expect, so we don't need to do any decoding.
if PY2: # pragma: no cover
fromBytes = fromUnicode
else:
def fromBytes(self, value):
return self.fromUnicode(value.decode('utf-8'))


class Complex(Number):
"""
Expand All @@ -675,17 +740,36 @@ class Complex(Number):
>>> from zope.schema._bootstrapfields import Complex
>>> f = Complex()
>>> f.fromUnicode("1")
>>> f.fromUnicode(u"1")
1
>>> f.fromUnicode("125.6")
>>> f.fromUnicode(u"125.6")
125.6
>>> f.fromUnicode("1+0j")
>>> f.fromUnicode(u"1+0j")
(1+0j)
>>> f.fromUnicode("1/2")
>>> f.fromUnicode(u"1/2")
Fraction(1, 2)
>>> f.fromUnicode(str(2**31234) + '.' + str(2**256)) # doctest: +ELLIPSIS
inf
>>> f.fromUnicode("not a number") # doctest: +IGNORE_EXCEPTION_DETAIL
>>> f.fromUnicode(u"not a number") # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
InvalidNumberLiteral: Invalid literal for Decimal: 'not a number'
Similarly for :meth:`fromBytes`:
>>> from zope.schema._bootstrapfields import Complex
>>> f = Complex()
>>> f.fromBytes(b"1")
1
>>> f.fromBytes(b"125.6")
125.6
>>> f.fromBytes(b"1+0j")
(1+0j)
>>> f.fromBytes(b"1/2")
Fraction(1, 2)
>>> f.fromBytes((str(2**31234) + '.' + str(2**256)).encode('ascii')) # doctest: +ELLIPSIS
inf
>>> f.fromBytes(b"not a number") # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
InvalidNumberLiteral: Invalid literal for Decimal: 'not a number'
Expand Down Expand Up @@ -782,6 +866,17 @@ class Integral(Rational):
...
InvalidIntLiteral: invalid literal for int(): 125.6
Similarly for :meth:`fromBytes`:
>>> from zope.schema._bootstrapfields import Integral
>>> f = Integral()
>>> f.fromBytes(b"125")
125
>>> f.fromBytes(b"125.6") #doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
InvalidIntLiteral: invalid literal for int(): 125.6
.. versionadded:: 4.6.0
"""
_type = numbers.Integral
Expand Down
14 changes: 13 additions & 1 deletion src/zope/schema/_bootstrapinterfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -282,11 +282,23 @@ class IFromUnicode(zope.interface.Interface):
values.
"""

def fromUnicode(str):
def fromUnicode(value):
"""Convert a unicode string to a value.
"""


class IFromBytes(zope.interface.Interface):
"""
Parse a byte string to a value.
If the string needs to be decoded, decoding is done using UTF-8.
"""

def fromBytes(value):
"""Convert a byte string to a value.
"""


class IContextAwareDefaultFactory(zope.interface.Interface):
"""A default factory that requires a context.
Expand Down
1 change: 1 addition & 0 deletions src/zope/schema/_compat.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import sys

PY3 = sys.version_info[0] >= 3
PY2 = not PY3

if PY3: # pragma: no cover

Expand Down
Loading

0 comments on commit b5964f5

Please sign in to comment.