From 28d574d0e7bae7dc9c9aedd77e6e5ef7077ad5e9 Mon Sep 17 00:00:00 2001 From: Jason Madden Date: Thu, 30 Aug 2018 11:15:21 -0500 Subject: [PATCH 1/5] Add fields and interfaces matching the numeric tower. In descending order of generality these are ``Number``, ``Complex``, ``Real``, ``Rational`` and ``Integral``. The ``Int`` class extends ``Integral``, the ``Float`` class extends ``Real``, and the ``Decimal`` class extends ``Number``. Generalize the parsing of numbers in the Number class. Add tests and document them. Run the field and bootstrap field doctests as part of unittesting, not just under tox with sphinx, and make them run on both Python 2 and 3. Fixes #49 --- CHANGES.rst | 7 + docs/api.rst | 56 +++-- src/zope/schema/__init__.py | 14 +- src/zope/schema/_bootstrapfields.py | 225 ++++++++++++++++-- src/zope/schema/_field.py | 130 +++++++--- src/zope/schema/interfaces.py | 170 ++++++++++++- .../schema/tests/test__bootstrapfields.py | 135 ++++++++++- src/zope/schema/tests/test__field.py | 118 +++------ 8 files changed, 668 insertions(+), 187 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 500e178..5c80464 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -87,6 +87,13 @@ subclass, enabling a simpler constructor call. See `issue 23 `_. +- Add fields and interfaces representing Python's numeric tower. In + descending order of generality these are ``Number``, ``Complex``, + ``Real``, ``Rational`` and ``Integral``. The ``Int`` class extends + ``Integral``, the ``Float`` class extends ``Real``, and the + ``Decimal`` class extends ``Number``. See `issue 49 + `_. + 4.5.0 (2017-07-10) ================== diff --git a/docs/api.rst b/docs/api.rst index a133111..284fae3 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -41,6 +41,12 @@ Strings Numbers ------- +.. autoclass:: zope.schema.interfaces.INumber +.. autoclass:: zope.schema.interfaces.IComplex +.. autoclass:: zope.schema.interfaces.IReal +.. autoclass:: zope.schema.interfaces.IRational +.. autoclass:: zope.schema.interfaces.IIntegral + .. autoclass:: zope.schema.interfaces.IInt .. autoclass:: zope.schema.interfaces.IFloat .. autoclass:: zope.schema.interfaces.IDecimal @@ -141,16 +147,9 @@ Fields .. autoclass:: zope.schema.Field .. autoclass:: zope.schema.Collection .. autoclass:: zope.schema._field.AbstractCollection -.. autoclass:: zope.schema.ASCII - :no-show-inheritance: -.. autoclass:: zope.schema.ASCIILine - :no-show-inheritance: + .. autoclass:: zope.schema.Bool :no-show-inheritance: -.. autoclass:: zope.schema.Bytes - :no-show-inheritance: -.. autoclass:: zope.schema.BytesLine - :no-show-inheritance: .. autoclass:: zope.schema.Choice :no-show-inheritance: .. autoclass:: zope.schema.Container @@ -159,20 +158,14 @@ Fields :no-show-inheritance: .. autoclass:: zope.schema.Datetime :no-show-inheritance: -.. autoclass:: zope.schema.Decimal - :no-show-inheritance: .. autoclass:: zope.schema.Dict .. autoclass:: zope.schema.DottedName :no-show-inheritance: -.. autoclass:: zope.schema.Float - :no-show-inheritance: .. autoclass:: zope.schema.FrozenSet :no-show-inheritance: .. autoclass:: zope.schema.Id :no-show-inheritance: -.. autoclass:: zope.schema.Int - :no-show-inheritance: .. autoclass:: zope.schema.InterfaceField :no-show-inheritance: .. autoclass:: zope.schema.Iterable @@ -192,12 +185,6 @@ Fields :no-show-inheritance: .. autoclass:: zope.schema.Set .. autoclass:: zope.schema.Sequence -.. autoclass:: zope.schema.SourceText - :no-show-inheritance: -.. autoclass:: zope.schema.Text - :no-show-inheritance: -.. autoclass:: zope.schema.TextLine - :no-show-inheritance: .. autoclass:: zope.schema.Time :no-show-inheritance: .. autoclass:: zope.schema.Timedelta @@ -206,6 +193,35 @@ Fields .. autoclass:: zope.schema.URI :no-show-inheritance: +Strings +------- +.. autoclass:: zope.schema.ASCII + :no-show-inheritance: +.. autoclass:: zope.schema.ASCIILine + :no-show-inheritance: +.. autoclass:: zope.schema.Bytes + :no-show-inheritance: +.. autoclass:: zope.schema.BytesLine + :no-show-inheritance: +.. autoclass:: zope.schema.SourceText + :no-show-inheritance: +.. autoclass:: zope.schema.Text + :no-show-inheritance: +.. autoclass:: zope.schema.TextLine + :no-show-inheritance: + +Numbers +------- +.. autoclass:: zope.schema.Number +.. autoclass:: zope.schema.Complex +.. autoclass:: zope.schema.Real +.. autoclass:: zope.schema.Rational +.. autoclass:: zope.schema.Integral +.. autoclass:: zope.schema.Float +.. autoclass:: zope.schema.Int +.. autoclass:: zope.schema.Decimal + + Accessors ========= diff --git a/src/zope/schema/__init__.py b/src/zope/schema/__init__.py index 8fc6825..92300fd 100644 --- a/src/zope/schema/__init__.py +++ b/src/zope/schema/__init__.py @@ -21,6 +21,7 @@ from zope.schema._field import BytesLine from zope.schema._field import Choice from zope.schema._field import Collection +from zope.schema._field import Complex from zope.schema._field import Container from zope.schema._field import Date from zope.schema._field import Datetime @@ -32,20 +33,24 @@ from zope.schema._field import FrozenSet from zope.schema._field import Id from zope.schema._field import Int +from zope.schema._field import Integral from zope.schema._field import InterfaceField from zope.schema._field import Iterable from zope.schema._field import List from zope.schema._field import Mapping +from zope.schema._field import MinMaxLen from zope.schema._field import MutableMapping from zope.schema._field import MutableSequence -from zope.schema._field import MinMaxLen from zope.schema._field import NativeString from zope.schema._field import NativeStringLine +from zope.schema._field import Number from zope.schema._field import Object from zope.schema._field import Orderable from zope.schema._field import Password -from zope.schema._field import Set +from zope.schema._field import Rational +from zope.schema._field import Real from zope.schema._field import Sequence +from zope.schema._field import Set from zope.schema._field import SourceText from zope.schema._field import Text from zope.schema._field import TextLine @@ -77,6 +82,7 @@ 'BytesLine', 'Choice', 'Collection', + 'Complex', 'Container', 'Date', 'Datetime', @@ -88,6 +94,7 @@ 'FrozenSet', 'Id', 'Int', + 'Integral', 'InterfaceField', 'Iterable', 'List', @@ -97,9 +104,12 @@ 'MinMaxLen', 'NativeString', 'NativeStringLine', + 'Number', 'Object', 'Orderable', 'Password', + 'Rational', + 'Real', 'Set', 'Sequence', 'SourceText', diff --git a/src/zope/schema/_bootstrapfields.py b/src/zope/schema/_bootstrapfields.py index 6d613b4..f61d9a4 100644 --- a/src/zope/schema/_bootstrapfields.py +++ b/src/zope/schema/_bootstrapfields.py @@ -15,6 +15,11 @@ """ __docformat__ = 'restructuredtext' +import decimal +import fractions +import numbers +from math import isinf + from zope.interface import Attribute from zope.interface import providedBy from zope.interface import implementer @@ -359,21 +364,26 @@ def __init__(self, *args, **kw): def fromUnicode(self, str): """ + >>> from zope.schema.interfaces import WrongType + >>> from zope.schema.interfaces import ConstraintNotSatisfied >>> from zope.schema import Text + >>> from zope.schema._compat import text_type >>> t = Text(constraint=lambda v: 'x' in v) - >>> t.fromUnicode(b"foo x spam") - Traceback (most recent call last): - ... - WrongType: ('foo x spam', , '') + >>> try: + ... t.fromUnicode(b"foo x spam") + ... except WrongType as e: + ... e.args == (b"foo x spam", text_type, '') + True >>> result = t.fromUnicode(u"foo x spam") >>> isinstance(result, bytes) False >>> str(result) 'foo x spam' - >>> t.fromUnicode(u"foo spam") - Traceback (most recent call last): - ... - ConstraintNotSatisfied: (u'foo spam', '') + >>> try: + ... t.fromUnicode(u"foo spam") + ... except ConstraintNotSatisfied as e: + ... e.args == (u'foo spam', '') + True """ self.validate(str) return str @@ -453,32 +463,197 @@ def fromUnicode(self, str): self.validate(v) return v +class InvalidNumberLiteral(ValueError, ValidationError): + """Invalid number literal.""" + +@implementer(IFromUnicode) +class Number(Orderable, Field): + """ + A field representing a :class:`numbers.Number` and implementing + :class:`zope.schema.interfaces.INumber`. + + The :meth:`fromUnicode` method will attempt to use the smallest or + strictest possible type to represent incoming strings:: + + >>> from zope.schema._bootstrapfields import Number + >>> f = Number() + >>> f.fromUnicode("1") + 1 + >>> f.fromUnicode("125.6") + 125.6 + >>> f.fromUnicode("1+0j") + (1+0j) + >>> f.fromUnicode("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 + Traceback (most recent call last): + ... + InvalidNumberLiteral: Invalid literal for Decimal: 'not a number' + + .. versionadded:: 4.6.0 + """ + _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 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 + # more generally useful and expected than a fraction, so we attempt to parse + # as a float before a fraction. + _unicode_converters = (int, float, fractions.Fraction, complex, decimal.Decimal) + + # The type of error we will raise if all conversions fail. + _validation_error = InvalidNumberLiteral + + def fromUnicode(self, value): + last_exc = None + for converter in self._unicode_converters: + try: + val = converter(value) + if converter is float and isinf(val) and decimal.Decimal in self._unicode_converters: + # Pass this on to decimal, if we're allowed + val = decimal.Decimal(value) + except (ValueError, decimal.InvalidOperation) as e: + last_exc = e + else: + self.validate(val) + return val + try: + raise self._validation_error(*last_exc.args).with_field_and_value(self, value) + finally: + last_exc = None + + +class Complex(Number): + """ + A field representing a :class:`numbers.Complex` and implementing + :class:`zope.schema.interfaces.IComplex`. + + The :meth:`fromUnicode` method is like that for :class:`Number`, + but doesn't allow Decimals:: + + >>> from zope.schema._bootstrapfields import Complex + >>> f = Complex() + >>> f.fromUnicode("1") + 1 + >>> f.fromUnicode("125.6") + 125.6 + >>> f.fromUnicode("1+0j") + (1+0j) + >>> f.fromUnicode("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 + Traceback (most recent call last): + ... + InvalidNumberLiteral: Invalid literal for Decimal: 'not a number' + + .. versionadded:: 4.6.0 + """ + _type = numbers.Complex + _unicode_converters = (int, float, complex, fractions.Fraction) + + +class Real(Complex): + """ + A field representing a :class:`numbers.Real` and implementing + :class:`zope.schema.interfaces.IReal`. + + The :meth:`fromUnicode` method is like that for :class:`Complex`, + but doesn't allow Decimals or complex numbers:: + + >>> from zope.schema._bootstrapfields import Real + >>> f = Real() + >>> f.fromUnicode("1") + 1 + >>> f.fromUnicode("125.6") + 125.6 + >>> f.fromUnicode("1+0j") # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + InvalidNumberLiteral: Invalid literal for Fraction: '1+0j' + >>> f.fromUnicode("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 + Traceback (most recent call last): + ... + InvalidNumberLiteral: Invalid literal for Decimal: 'not a number' + + .. versionadded:: 4.6.0 + """ + _type = numbers.Real + _unicode_converters = (int, float, fractions.Fraction) + + +class Rational(Real): + """ + A field representing a :class:`numbers.Rational` and implementing + :class:`zope.schema.interfaces.IRational`. + + The :meth:`fromUnicode` method is like that for :class:`Real`, + but does not allow arbitrary floating point numbers:: + + >>> from zope.schema._bootstrapfields import Rational + >>> f = Rational() + >>> f.fromUnicode("1") + 1 + >>> f.fromUnicode("1/2") + Fraction(1, 2) + >>> f.fromUnicode("125.6") + Fraction(628, 5) + >>> f.fromUnicode("1+0j") # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + InvalidNumberLiteral: Invalid literal for Fraction: '1+0j' + >>> f.fromUnicode(str(2**31234) + '.' + str(2**256)) # doctest: +ELLIPSIS + Fraction(777..., 330...) + >>> f.fromUnicode("not a number") # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + InvalidNumberLiteral: Invalid literal for Decimal: 'not a number' + + .. versionadded:: 4.6.0 + """ + _type = numbers.Rational + _unicode_converters = (int, fractions.Fraction) + + class InvalidIntLiteral(ValueError, ValidationError): """Invalid int literal.""" -@implementer(IFromUnicode) -class Int(Orderable, Field): - """A field representing an Integer.""" - _type = integer_types +class Integral(Rational): + """ + A field representing a :class:`numbers.Integral` and implementing + :class:`zope.schema.interfaces.IIntegral`. - def __init__(self, *args, **kw): - super(Int, self).__init__(*args, **kw) + The :meth:`fromUnicode` method only allows integral values:: - def fromUnicode(self, str): - """ - >>> from zope.schema._bootstrapfields import Int - >>> f = Int() + >>> from zope.schema._bootstrapfields import Integral + >>> f = Integral() >>> f.fromUnicode("125") 125 >>> f.fromUnicode("125.6") #doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... InvalidIntLiteral: invalid literal for int(): 125.6 - """ - try: - v = int(str) - except ValueError as v: - raise InvalidIntLiteral(*v.args).with_field_and_value(self, str) - self.validate(v) - return v + + .. versionadded:: 4.6.0 + """ + _type = numbers.Integral + _unicode_converters = (int,) + _validation_error = InvalidIntLiteral + + +class Int(Integral): + """A field representing a native integer type. and implementing + :class:`zope.schema.interfaces.IInt`. + """ + _type = integer_types + _unicode_converters = (int,) diff --git a/src/zope/schema/_field.py b/src/zope/schema/_field.py index d80ee3c..6445da1 100644 --- a/src/zope/schema/_field.py +++ b/src/zope/schema/_field.py @@ -45,6 +45,7 @@ from zope.schema.interfaces import IBytes from zope.schema.interfaces import IBytesLine from zope.schema.interfaces import IChoice +from zope.schema.interfaces import IComplex from zope.schema.interfaces import ICollection from zope.schema.interfaces import IContextSourceBinder from zope.schema.interfaces import IDate @@ -58,6 +59,7 @@ from zope.schema.interfaces import IFrozenSet from zope.schema.interfaces import IId from zope.schema.interfaces import IInt +from zope.schema.interfaces import IIntegral from zope.schema.interfaces import IInterfaceField from zope.schema.interfaces import IList from zope.schema.interfaces import IMinMaxLen @@ -65,7 +67,10 @@ from zope.schema.interfaces import IMutableMapping from zope.schema.interfaces import IMutableSequence from zope.schema.interfaces import IObject +from zope.schema.interfaces import INumber from zope.schema.interfaces import IPassword +from zope.schema.interfaces import IReal +from zope.schema.interfaces import IRational from zope.schema.interfaces import ISet from zope.schema.interfaces import ISequence from zope.schema.interfaces import ISource @@ -91,6 +96,7 @@ from zope.schema.interfaces import ConstraintNotSatisfied from zope.schema._bootstrapfields import Field +from zope.schema._bootstrapfields import Complex from zope.schema._bootstrapfields import Container # API import for __init__ from zope.schema._bootstrapfields import Iterable from zope.schema._bootstrapfields import Orderable @@ -98,7 +104,11 @@ from zope.schema._bootstrapfields import TextLine from zope.schema._bootstrapfields import Bool from zope.schema._bootstrapfields import Int +from zope.schema._bootstrapfields import Integral +from zope.schema._bootstrapfields import Number from zope.schema._bootstrapfields import Password +from zope.schema._bootstrapfields import Rational +from zope.schema._bootstrapfields import Real from zope.schema._bootstrapfields import MinMaxLen from zope.schema._bootstrapfields import _NotGiven from zope.schema.fieldproperty import FieldProperty @@ -132,6 +142,12 @@ classImplements(Password, IPassword) classImplements(Bool, IBool) classImplements(Bool, IFromUnicode) + +classImplements(Number, INumber) +classImplements(Complex, IComplex) +classImplements(Real, IReal) +classImplements(Rational, IRational) +classImplements(Integral, IIntegral) classImplements(Int, IInt) @@ -203,22 +219,41 @@ class InvalidFloatLiteral(ValueError, ValidationError): @implementer(IFloat, IFromUnicode) -class Float(Orderable, Field): - __doc__ = IFloat.__doc__ +class Float(Real): + """ + A field representing a native :class:`float` and implementing + :class:`zope.schema.interfaces.IFloat`. + + The class :class:`zope.schema.Real` is a more general version, + accepting floats, integers, and fractions. + + The :meth:`fromUnicode` method only accepts values that can be parsed + by the ``float`` constructor:: + + >>> from zope.schema._field import Float + >>> f = Float() + >>> f.fromUnicode("1") + 1.0 + >>> f.fromUnicode("125.6") + 125.6 + >>> f.fromUnicode("1+0j") # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + InvalidFloatLiteral: Invalid literal for float(): 1+0j + >>> f.fromUnicode("1/2") # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + InvalidFloatLiteral: invalid literal for float(): 1/2 + >>> f.fromUnicode(str(2**31234) + '.' + str(2**256)) # doctest: +ELLIPSIS + inf + >>> f.fromUnicode("not a number") # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + InvalidFloatLiteral: could not convert string to float: not a number + """ _type = float - - def __init__(self, *args, **kw): - super(Float, self).__init__(*args, **kw) - - def fromUnicode(self, uc): - """ See IFromUnicode. - """ - try: - v = float(uc) - except ValueError as v: - raise InvalidFloatLiteral(*v.args).with_field_and_value(self, uc) - self.validate(v) - return v + _unicode_converters = (float,) + _validation_error = InvalidFloatLiteral class InvalidDecimalLiteral(ValueError, ValidationError): @@ -229,22 +264,38 @@ def __init__(self, literal): @implementer(IDecimal, IFromUnicode) -class Decimal(Orderable, Field): - __doc__ = IDecimal.__doc__ +class Decimal(Number): + """ + A field representing a native :class:`decimal.Decimal` and implementing + :class:`zope.schema.interfaces.IDecimal`. + + The :meth:`fromUnicode` method only accepts values that can be parsed + by the ``Decimal`` constructor:: + + >>> from zope.schema._field import Decimal + >>> f = Decimal() + >>> f.fromUnicode("1") + Decimal('1') + >>> f.fromUnicode("125.6") + Decimal('125.6') + >>> f.fromUnicode("1+0j") # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + InvalidDecimalLiteral: Invalid literal for Decimal(): 1+0j + >>> f.fromUnicode("1/2") # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + InvalidDecimalLiteral: Invalid literal for Decimal(): 1/2 + >>> f.fromUnicode(str(2**31234) + '.' + str(2**256)) # doctest: +ELLIPSIS + Decimal('2349...936') + >>> f.fromUnicode("not a number") # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + InvalidDecimalLiteral: could not convert string to float: not a number + """ _type = decimal.Decimal - - def __init__(self, *args, **kw): - super(Decimal, self).__init__(*args, **kw) - - def fromUnicode(self, uc): - """ See IFromUnicode. - """ - try: - v = decimal.Decimal(uc) - except decimal.InvalidOperation: - raise InvalidDecimalLiteral(uc).with_field_and_value(self, uc) - self.validate(v) - return v + _unicode_converters = (decimal.Decimal,) + _validation_error = InvalidDecimalLiteral @implementer(IDatetime) @@ -505,9 +556,14 @@ def _validate_sequence(value_type, value, errors=None): To validate a sequence of various values: + >>> from zope.schema._compat import text_type >>> errors = _validate_sequence(field, (b'foo', u'bar', 1)) - >>> errors # XXX assumes Python2 reprs - [WrongType('foo', , ''), WrongType(1, , '')] + >>> len(errors) + 2 + >>> errors[0].args == (b'foo', text_type, '') + True + >>> errors[1].args == (1, text_type, '') + True The only valid value in the sequence is the second item. The others generated errors. @@ -516,8 +572,14 @@ def _validate_sequence(value_type, value, errors=None): for a new sequence: >>> errors = _validate_sequence(field, (2, u'baz'), errors) - >>> errors # XXX assumes Python2 reprs - [WrongType('foo', , ''), WrongType(1, , ''), WrongType(2, , '')] + >>> len(errors) + 3 + >>> errors[0].args == (b'foo', text_type, '') + True + >>> errors[1].args == (1, text_type, '') + True + >>> errors[2].args == (2, text_type, '') + True """ if errors is None: diff --git a/src/zope/schema/interfaces.py b/src/zope/schema/interfaces.py index a1c7f35..2868b71 100644 --- a/src/zope/schema/interfaces.py +++ b/src/zope/schema/interfaces.py @@ -25,6 +25,11 @@ from zope.schema._bootstrapfields import Text from zope.schema._bootstrapfields import TextLine from zope.schema._bootstrapfields import Bool +from zope.schema._bootstrapfields import Number +from zope.schema._bootstrapfields import Complex +from zope.schema._bootstrapfields import Rational +from zope.schema._bootstrapfields import Real +from zope.schema._bootstrapfields import Integral from zope.schema._bootstrapfields import Int from zope.schema._bootstrapinterfaces import StopValidation from zope.schema._bootstrapinterfaces import ValidationError @@ -364,9 +369,152 @@ class ITextLine(IText): class IPassword(ITextLine): "Field containing a unicode string without newlines that is a password." +### +# Numbers +### -class IInt(IMinMax, IField): - """Field containing an Integer Value.""" +## +# Abstract numbers +## + +class INumber(IMinMax, IField): + """ + Field containing a generic number: :class:`numbers.Number`. + + .. seealso:: :class:`zope.schema.Number` + .. versionadded:: 4.6.0 + """ + min = Number( + title=_("Start of the range"), + required=False, + default=None + ) + + max = Number( + title=_("End of the range (including the value itself)"), + required=False, + default=None + ) + + default = Number( + title=_("Default Value"), + description=_("""The field default value may be None or a legal + field value""") + ) + + +class IComplex(INumber): + """ + Field containing a complex number: :class:`numbers.Complex`. + + .. seealso:: :class:`zope.schema.Real` + .. versionadded:: 4.6.0 + """ + min = Complex( + title=_("Start of the range"), + required=False, + default=None + ) + + max = Complex( + title=_("End of the range (including the value itself)"), + required=False, + default=None + ) + + default = Complex( + title=_("Default Value"), + description=_("""The field default value may be None or a legal + field value""") + ) + + +class IReal(IComplex): + """ + Field containing a real number: :class:`numbers.IReal`. + + .. seealso:: :class:`zope.schema.Real` + .. versionadded:: 4.6.0 + """ + min = Real( + title=_("Start of the range"), + required=False, + default=None + ) + + max = Real( + title=_("End of the range (including the value itself)"), + required=False, + default=None + ) + + default = Real( + title=_("Default Value"), + description=_("""The field default value may be None or a legal + field value""") + ) + +class IRational(IReal): + """ + Field containing a rational number: :class:`numbers.IRational`. + + .. seealso:: :class:`zope.schema.Rational` + .. versionadded:: 4.6.0 + """ + + min = Rational( + title=_("Start of the range"), + required=False, + default=None + ) + + max = Rational( + title=_("End of the range (including the value itself)"), + required=False, + default=None + ) + + default = Rational( + title=_("Default Value"), + description=_("""The field default value may be None or a legal + field value""") + ) + +class IIntegral(IRational): + """ + Field containing an integral number: class:`numbers.Integral`. + + .. seealso:: :class:`zope.schema.Integral` + .. versionadded:: 4.6.0 + """ + min = Integral( + title=_("Start of the range"), + required=False, + default=None + ) + + max = Integral( + title=_("End of the range (including the value itself)"), + required=False, + default=None + ) + + default = Integral( + title=_("Default Value"), + description=_("""The field default value may be None or a legal + field value""") + ) +## +# Concrete numbers +## + +class IInt(IIntegral): + """ + Field containing exactly the native class :class:`int` (or, on + Python 2, ``long``). + + .. seealso:: :class:`zope.schema.Int` + """ min = Int( title=_("Start of the range"), @@ -387,13 +535,23 @@ class IInt(IMinMax, IField): ) -class IFloat(IMinMax, IField): - """Field containing a Float.""" +class IFloat(IReal): + """ + Field containing exactly the native class :class:`float`. + + :class:`IReal` is a more general interface, allowing all of + floats, ints, and fractions. + + .. seealso:: :class:`zope.schema.Float` + """ -class IDecimal(IMinMax, IField): - """Field containing a Decimal.""" +class IDecimal(INumber): + """Field containing a :class:`decimal.Decimal`""" +### +# End numbers +### class IDatetime(IMinMax, IField): """Field containing a datetime.""" diff --git a/src/zope/schema/tests/test__bootstrapfields.py b/src/zope/schema/tests/test__bootstrapfields.py index 3e6365a..e097bc0 100644 --- a/src/zope/schema/tests/test__bootstrapfields.py +++ b/src/zope/schema/tests/test__bootstrapfields.py @@ -11,8 +11,10 @@ # FOR A PARTICULAR PURPOSE. # ############################################################################## +import doctest import unittest +# pylint:disable=protected-access class ValidatedPropertyTests(unittest.TestCase): @@ -776,9 +778,29 @@ def test_fromUnicode_hit(self): self.assertEqual(txt.fromUnicode(u'True'), True) self.assertEqual(txt.fromUnicode(u'true'), True) +class ConformanceMixin(object): -class OrderableMissingValueMixin(object): + def _getTargetClass(self): + raise NotImplementedError + + def _getTargetInterface(self): + raise NotImplementedError + def _makeOne(self, *args, **kwargs): + return self._getTargetClass()(*args, **kwargs) + + def test_class_conforms_to_iface(self): + from zope.interface.verify import verifyClass + verifyClass(self._getTargetInterface(), self._getTargetClass()) + return verifyClass + + def test_instance_conforms_to_iface(self): + from zope.interface.verify import verifyObject + verifyObject(self._getTargetInterface(), self._makeOne()) + return verifyObject + + +class OrderableMissingValueMixin(ConformanceMixin): mvm_missing_value = -1 mvm_default = 0 @@ -798,18 +820,88 @@ def test_missing_value_no_min_or_max(self): self.assertEqual(self.mvm_missing_value, field.missing_value) -class IntTests(EqualityTestsMixin, - OrderableMissingValueMixin, - unittest.TestCase): + +class NumberTests(EqualityTestsMixin, + OrderableMissingValueMixin, + unittest.TestCase): def _getTargetClass(self): - from zope.schema._bootstrapfields import Int - return Int + from zope.schema._bootstrapfields import Number + return Number - def test_ctor_defaults(self): - from zope.schema._compat import integer_types - txt = self._makeOne() - self.assertEqual(txt._type, integer_types) + def _getTargetInterface(self): + from zope.schema.interfaces import INumber + return INumber + + def test_class_conforms_to_iface(self): + from zope.schema._bootstrapinterfaces import IFromUnicode + verifyClass = super(NumberTests, self).test_class_conforms_to_iface() + verifyClass(IFromUnicode, self._getTargetClass()) + + def test_instance_conforms_to_iface(self): + from zope.schema._bootstrapinterfaces import IFromUnicode + verifyObject = super(NumberTests, self).test_instance_conforms_to_iface() + verifyObject(IFromUnicode, self._makeOne()) + + +class ComplexTests(NumberTests): + + def _getTargetClass(self): + from zope.schema._bootstrapfields import Complex + return Complex + + def _getTargetInterface(self): + from zope.schema.interfaces import IComplex + return IComplex + +class RealTests(NumberTests): + + def _getTargetClass(self): + from zope.schema._bootstrapfields import Real + return Real + + def _getTargetInterface(self): + from zope.schema.interfaces import IReal + return IReal + + def test_ctor_real_min_max(self): + from zope.schema.interfaces import WrongType + from zope.schema.interfaces import TooSmall + from zope.schema.interfaces import TooBig + from fractions import Fraction + + with self.assertRaises(WrongType): + self._makeOne(min='') + with self.assertRaises(WrongType): + self._makeOne(max='') + + field = self._makeOne(min=Fraction(1, 2), max=2) + field.validate(1.0) + field.validate(2.0) + self.assertRaises(TooSmall, field.validate, 0) + self.assertRaises(TooSmall, field.validate, 0.4) + self.assertRaises(TooBig, field.validate, 2.1) + +class RationalTests(NumberTests): + + def _getTargetClass(self): + from zope.schema._bootstrapfields import Rational + return Rational + + def _getTargetInterface(self): + from zope.schema.interfaces import IRational + return IRational + + +class IntegralTests(RationalTests): + + def _getTargetClass(self): + from zope.schema._bootstrapfields import Integral + return Integral + + def _getTargetInterface(self): + from zope.schema.interfaces import IIntegral + return IIntegral def test_validate_not_required(self): field = self._makeOne(required=False) @@ -870,6 +962,22 @@ def test_fromUnicode_hit(self): self.assertEqual(txt.fromUnicode(u'-1'), -1) +class IntTests(IntegralTests): + + def _getTargetClass(self): + from zope.schema._bootstrapfields import Int + return Int + + def _getTargetInterface(self): + from zope.schema.interfaces import IInt + return IInt + + def test_ctor_defaults(self): + from zope.schema._compat import integer_types + txt = self._makeOne() + self.assertEqual(txt._type, integer_types) + + class DummyInst(object): missing_value = object() @@ -879,3 +987,10 @@ def __init__(self, exc=None): def validate(self, value): if self._exc is not None: raise self._exc() + + +def test_suite(): + import zope.schema._bootstrapfields + suite = unittest.defaultTestLoader.loadTestsFromName(__name__) + suite.addTests(doctest.DocTestSuite(zope.schema._bootstrapfields)) + return suite diff --git a/src/zope/schema/tests/test__field.py b/src/zope/schema/tests/test__field.py index c035e85..dd03bc4 100644 --- a/src/zope/schema/tests/test__field.py +++ b/src/zope/schema/tests/test__field.py @@ -13,9 +13,11 @@ ############################################################################## import datetime import decimal +import doctest import unittest from zope.schema.tests.test__bootstrapfields import OrderableMissingValueMixin +from zope.schema.tests.test__bootstrapfields import ConformanceMixin # pylint:disable=protected-access # pylint:disable=too-many-lines @@ -23,24 +25,15 @@ # pylint:disable=no-member # pylint:disable=blacklisted-name -class BytesTests(unittest.TestCase): +class BytesTests(ConformanceMixin, unittest.TestCase): def _getTargetClass(self): from zope.schema._field import Bytes return Bytes - def _makeOne(self, *args, **kw): - return self._getTargetClass()(*args, **kw) - - def test_class_conforms_to_IBytes(self): - from zope.interface.verify import verifyClass - from zope.schema.interfaces import IBytes - verifyClass(IBytes, self._getTargetClass()) - - def test_instance_conforms_to_IBytes(self): - from zope.interface.verify import verifyObject + def _getTargetInterface(self): from zope.schema.interfaces import IBytes - verifyObject(IBytes, self._makeOne()) + return IBytes def test_validate_wrong_types(self): from zope.schema.interfaces import WrongType @@ -272,18 +265,9 @@ def _getTargetClass(self): from zope.schema._field import Float return Float - def _makeOne(self, *args, **kw): - return self._getTargetClass()(*args, **kw) - - def test_class_conforms_to_IFloat(self): - from zope.interface.verify import verifyClass - from zope.schema.interfaces import IFloat - verifyClass(IFloat, self._getTargetClass()) - - def test_instance_conforms_to_IFloat(self): - from zope.interface.verify import verifyObject + def _getTargetInterface(self): from zope.schema.interfaces import IFloat - verifyObject(IFloat, self._makeOne()) + return IFloat def test_validate_not_required(self): field = self._makeOne(required=False) @@ -353,18 +337,9 @@ def _getTargetClass(self): from zope.schema._field import Decimal return Decimal - def _makeOne(self, *args, **kw): - return self._getTargetClass()(*args, **kw) - - def test_class_conforms_to_IDecimal(self): - from zope.interface.verify import verifyClass - from zope.schema.interfaces import IDecimal - verifyClass(IDecimal, self._getTargetClass()) - - def test_instance_conforms_to_IDecimal(self): - from zope.interface.verify import verifyObject + def _getTargetInterface(self): from zope.schema.interfaces import IDecimal - verifyObject(IDecimal, self._makeOne()) + return IDecimal def test_validate_not_required(self): field = self._makeOne(required=False) @@ -453,18 +428,9 @@ def _getTargetClass(self): from zope.schema._field import Datetime return Datetime - def _makeOne(self, *args, **kw): - return self._getTargetClass()(*args, **kw) - - def test_class_conforms_to_IDatetime(self): - from zope.interface.verify import verifyClass - from zope.schema.interfaces import IDatetime - verifyClass(IDatetime, self._getTargetClass()) - - def test_instance_conforms_to_IDatetime(self): - from zope.interface.verify import verifyObject + def _getTargetInterface(self): from zope.schema.interfaces import IDatetime - verifyObject(IDatetime, self._makeOne()) + return IDatetime def test_validate_wrong_types(self): from datetime import date @@ -539,18 +505,9 @@ def _getTargetClass(self): from zope.schema._field import Date return Date - def _makeOne(self, *args, **kw): - return self._getTargetClass()(*args, **kw) - - def test_class_conforms_to_IDate(self): - from zope.interface.verify import verifyClass - from zope.schema.interfaces import IDate - verifyClass(IDate, self._getTargetClass()) - - def test_instance_conforms_to_IDate(self): - from zope.interface.verify import verifyObject + def _getTargetInterface(self): from zope.schema.interfaces import IDate - verifyObject(IDate, self._makeOne()) + return IDate def test_validate_wrong_types(self): from zope.schema.interfaces import WrongType @@ -635,18 +592,9 @@ def _getTargetClass(self): from zope.schema._field import Timedelta return Timedelta - def _makeOne(self, *args, **kw): - return self._getTargetClass()(*args, **kw) - - def test_class_conforms_to_ITimedelta(self): - from zope.interface.verify import verifyClass - from zope.schema.interfaces import ITimedelta - verifyClass(ITimedelta, self._getTargetClass()) - - def test_instance_conforms_to_ITimedelta(self): - from zope.interface.verify import verifyObject + def _getTargetInterface(self): from zope.schema.interfaces import ITimedelta - verifyObject(ITimedelta, self._makeOne()) + return ITimedelta def test_validate_not_required(self): from datetime import timedelta @@ -709,18 +657,9 @@ def _getTargetClass(self): from zope.schema._field import Time return Time - def _makeOne(self, *args, **kw): - return self._getTargetClass()(*args, **kw) - - def test_class_conforms_to_ITime(self): - from zope.interface.verify import verifyClass - from zope.schema.interfaces import ITime - verifyClass(ITime, self._getTargetClass()) - - def test_instance_conforms_to_ITime(self): - from zope.interface.verify import verifyObject + def _getTargetInterface(self): from zope.schema.interfaces import ITime - verifyObject(ITime, self._makeOne()) + return ITime def test_validate_not_required(self): from datetime import time @@ -1310,24 +1249,16 @@ def test_fromUnicode_invalid(self): field.fromUnicode, u'http://example.com/\nDAV:') -class InterfaceFieldTests(unittest.TestCase): +class InterfaceFieldTests(ConformanceMixin, + unittest.TestCase): def _getTargetClass(self): from zope.schema._field import InterfaceField return InterfaceField - def _makeOne(self, *args, **kw): - return self._getTargetClass()(*args, **kw) - - def test_class_conforms_to_IInterfaceField(self): - from zope.interface.verify import verifyClass - from zope.schema.interfaces import IInterfaceField - verifyClass(IInterfaceField, self._getTargetClass()) - - def test_instance_conforms_to_IInterfaceField(self): - from zope.interface.verify import verifyObject + def _getTargetInterface(self): from zope.schema.interfaces import IInterfaceField - verifyObject(IInterfaceField, self._makeOne()) + return IInterfaceField def test_validate_wrong_types(self): from datetime import date @@ -2347,3 +2278,10 @@ def __init__(self, vocabulary): def get(self, object, name): return self._vocabulary return DummyRegistry(v) + + +def test_suite(): + import zope.schema._field + suite = unittest.defaultTestLoader.loadTestsFromName(__name__) + suite.addTests(doctest.DocTestSuite(zope.schema._field)) + return suite From 73627cf222688ae30a7bdfda5abc6fd19fde1c87 Mon Sep 17 00:00:00 2001 From: Jason Madden Date: Thu, 30 Aug 2018 11:36:53 -0500 Subject: [PATCH 2/5] Fix tests on PyPy3. Previously they would fail like this: File /home/travis/build/zopefoundation/zope.schema/src/zope/schema/_bootstrapfields.py, line 522, in fromUnicode raise self._validation_error(*last_exc.args).with_field_and_value(self, value) TypeError: __init__() missing 1 required positional argument: 'literal' --- src/zope/schema/_field.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/zope/schema/_field.py b/src/zope/schema/_field.py index 6445da1..7ee034c 100644 --- a/src/zope/schema/_field.py +++ b/src/zope/schema/_field.py @@ -257,10 +257,7 @@ class Float(Real): class InvalidDecimalLiteral(ValueError, ValidationError): - - def __init__(self, literal): - super(InvalidDecimalLiteral, self).__init__( - "invalid literal for Decimal(): %s" % literal) + "Raised by decimal fields" @implementer(IDecimal, IFromUnicode) From 99f265f1ed958f0d37e24b69d8b1a74f52e2cd8a Mon Sep 17 00:00:00 2001 From: Jason Madden Date: Fri, 31 Aug 2018 12:50:40 -0500 Subject: [PATCH 3/5] Expand the equality tests and conformance tests to all fields. Fix Iterable and Container to conform to their interfaces. --- CHANGES.rst | 3 + src/zope/schema/_field.py | 9 +- .../schema/tests/test__bootstrapfields.py | 289 +++++++++--------- src/zope/schema/tests/test__field.py | 173 ++++------- 4 files changed, 218 insertions(+), 256 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 5c80464..624d87f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -94,6 +94,9 @@ ``Decimal`` class extends ``Number``. See `issue 49 `_. +- Make ``Iterable`` and ``Container`` properly implement ``IIterable`` + and ``IContainer``, respectively. + 4.5.0 (2017-07-10) ================== diff --git a/src/zope/schema/_field.py b/src/zope/schema/_field.py index 7ee034c..fa2d6df 100644 --- a/src/zope/schema/_field.py +++ b/src/zope/schema/_field.py @@ -45,8 +45,9 @@ from zope.schema.interfaces import IBytes from zope.schema.interfaces import IBytesLine from zope.schema.interfaces import IChoice -from zope.schema.interfaces import IComplex from zope.schema.interfaces import ICollection +from zope.schema.interfaces import IComplex +from zope.schema.interfaces import IContainer from zope.schema.interfaces import IContextSourceBinder from zope.schema.interfaces import IDate from zope.schema.interfaces import IDatetime @@ -58,6 +59,7 @@ from zope.schema.interfaces import IFromUnicode from zope.schema.interfaces import IFrozenSet from zope.schema.interfaces import IId +from zope.schema.interfaces import IIterable from zope.schema.interfaces import IInt from zope.schema.interfaces import IIntegral from zope.schema.interfaces import IInterfaceField @@ -123,9 +125,6 @@ from zope.schema._compat import PY3 from zope.schema._compat import make_binary -# pep 8 friendlyness -Container - # Fix up bootstrap field types Field.title = FieldProperty(IField['title']) Field.description = FieldProperty(IField['description']) @@ -142,6 +141,8 @@ classImplements(Password, IPassword) classImplements(Bool, IBool) classImplements(Bool, IFromUnicode) +classImplements(Iterable, IIterable) +classImplements(Container, IContainer) classImplements(Number, INumber) classImplements(Complex, IComplex) diff --git a/src/zope/schema/tests/test__bootstrapfields.py b/src/zope/schema/tests/test__bootstrapfields.py index e097bc0..c46778b 100644 --- a/src/zope/schema/tests/test__bootstrapfields.py +++ b/src/zope/schema/tests/test__bootstrapfields.py @@ -16,6 +16,131 @@ # pylint:disable=protected-access +class ConformanceMixin(object): + + def _getTargetClass(self): + raise NotImplementedError + + def _getTargetInterface(self): + raise NotImplementedError + + def _makeOne(self, *args, **kwargs): + return self._makeOneFromClass(self._getTargetClass(), + *args, + **kwargs) + + def _makeOneFromClass(self, cls, *args, **kwargs): + return cls(*args, **kwargs) + + def test_class_conforms_to_iface(self): + from zope.interface.verify import verifyClass + cls = self._getTargetClass() + __traceback_info__ = cls + verifyClass(self._getTargetInterface(), cls) + return verifyClass + + def test_instance_conforms_to_iface(self): + from zope.interface.verify import verifyObject + instance = self._makeOne() + __traceback_info__ = instance + verifyObject(self._getTargetInterface(), instance) + return verifyObject + + +class EqualityTestsMixin(ConformanceMixin): + + def test_is_hashable(self): + field = self._makeOne() + hash(field) # doesn't raise + + def test_equal_instances_have_same_hash(self): + # Equal objects should have equal hashes + field1 = self._makeOne() + field2 = self._makeOne() + self.assertIsNot(field1, field2) + self.assertEqual(field1, field2) + self.assertEqual(hash(field1), hash(field2)) + + def test_instances_in_different_interfaces_not_equal(self): + from zope import interface + + field1 = self._makeOne() + field2 = self._makeOne() + self.assertEqual(field1, field2) + self.assertEqual(hash(field1), hash(field2)) + + class IOne(interface.Interface): + one = field1 + + class ITwo(interface.Interface): + two = field2 + + self.assertEqual(field1, field1) + self.assertEqual(field2, field2) + self.assertNotEqual(field1, field2) + self.assertNotEqual(hash(field1), hash(field2)) + + def test_hash_across_unequal_instances(self): + # Hash equality does not imply equal objects. + # Our implementation only considers property names, + # not values. That's OK, a dict still does the right thing. + field1 = self._makeOne(title=u'foo') + field2 = self._makeOne(title=u'bar') + self.assertIsNot(field1, field2) + self.assertNotEqual(field1, field2) + self.assertEqual(hash(field1), hash(field2)) + + d = {field1: 42} + self.assertIn(field1, d) + self.assertEqual(42, d[field1]) + self.assertNotIn(field2, d) + with self.assertRaises(KeyError): + d.__getitem__(field2) + + def test___eq___different_type(self): + left = self._makeOne() + + class Derived(self._getTargetClass()): + pass + right = self._makeOneFromClass(Derived) + self.assertNotEqual(left, right) + self.assertTrue(left != right) + + def test___eq___same_type_different_attrs(self): + left = self._makeOne(required=True) + right = self._makeOne(required=False) + self.assertNotEqual(left, right) + self.assertTrue(left != right) + + def test___eq___same_type_same_attrs(self): + left = self._makeOne() + self.assertEqual(left, left) + + right = self._makeOne() + self.assertEqual(left, right) + self.assertFalse(left != right) + + +class OrderableMissingValueMixin(EqualityTestsMixin): + mvm_missing_value = -1 + mvm_default = 0 + + def test_missing_value_no_min_or_max(self): + # We should be able to provide a missing_value without + # also providing a min or max. But note that we must still + # provide a default. + # See https://github.com/zopefoundation/zope.schema/issues/9 + Kind = self._getTargetClass() + self.assertTrue(Kind.min._allow_none) + self.assertTrue(Kind.max._allow_none) + + field = self._makeOne(missing_value=self.mvm_missing_value, + default=self.mvm_default) + self.assertIsNone(field.min) + self.assertIsNone(field.max) + self.assertEqual(self.mvm_missing_value, field.missing_value) + + class ValidatedPropertyTests(unittest.TestCase): def _getTargetClass(self): @@ -144,86 +269,6 @@ def _factory(context): self.assertEqual(_called_with, [inst.context]) -class EqualityTestsMixin(object): - - def _getTargetClass(self): - raise NotImplementedError - - def _makeOne(self, *args, **kw): - return self._getTargetClass()(*args, **kw) - - def test_is_hashable(self): - field = self._makeOne() - hash(field) # doesn't raise - - def test_equal_instances_have_same_hash(self): - # Equal objects should have equal hashes - field1 = self._makeOne() - field2 = self._makeOne() - self.assertIsNot(field1, field2) - self.assertEqual(field1, field2) - self.assertEqual(hash(field1), hash(field2)) - - def test_instances_in_different_interfaces_not_equal(self): - from zope import interface - - field1 = self._makeOne() - field2 = self._makeOne() - self.assertEqual(field1, field2) - self.assertEqual(hash(field1), hash(field2)) - - class IOne(interface.Interface): - one = field1 - - class ITwo(interface.Interface): - two = field2 - - self.assertEqual(field1, field1) - self.assertEqual(field2, field2) - self.assertNotEqual(field1, field2) - self.assertNotEqual(hash(field1), hash(field2)) - - def test_hash_across_unequal_instances(self): - # Hash equality does not imply equal objects. - # Our implementation only considers property names, - # not values. That's OK, a dict still does the right thing. - field1 = self._makeOne(title=u'foo') - field2 = self._makeOne(title=u'bar') - self.assertIsNot(field1, field2) - self.assertNotEqual(field1, field2) - self.assertEqual(hash(field1), hash(field2)) - - d = {field1: 42} - self.assertIn(field1, d) - self.assertEqual(42, d[field1]) - self.assertNotIn(field2, d) - with self.assertRaises(KeyError): - d.__getitem__(field2) - - def test___eq___different_type(self): - left = self._makeOne() - - class Derived(self._getTargetClass()): - pass - right = Derived() - self.assertNotEqual(left, right) - self.assertTrue(left != right) - - def test___eq___same_type_different_attrs(self): - left = self._makeOne(required=True) - right = self._makeOne(required=False) - self.assertNotEqual(left, right) - self.assertTrue(left != right) - - def test___eq___same_type_same_attrs(self): - left = self._makeOne() - self.assertEqual(left, left) - - right = self._makeOne() - self.assertEqual(left, right) - self.assertFalse(left != right) - - class FieldTests(EqualityTestsMixin, unittest.TestCase): @@ -231,8 +276,9 @@ def _getTargetClass(self): from zope.schema._bootstrapfields import Field return Field - def _makeOne(self, *args, **kw): - return self._getTargetClass()(*args, **kw) + def _getTargetInterface(self): + from zope.schema.interfaces import IField + return IField def test_ctor_defaults(self): @@ -434,6 +480,10 @@ def _getTargetClass(self): from zope.schema._bootstrapfields import Container return Container + def _getTargetInterface(self): + from zope.schema.interfaces import IContainer + return IContainer + def test_validate_not_required(self): field = self._makeOne(required=False) field.validate(None) @@ -484,6 +534,10 @@ def _getTargetClass(self): from zope.schema._bootstrapfields import Iterable return Iterable + def _getTargetInterface(self): + from zope.schema.interfaces import IIterable + return IIterable + def test__validate_collection_but_not_iterable(self): from zope.schema._bootstrapinterfaces import NotAnIterator itr = self._makeOne() @@ -568,6 +622,10 @@ def _getTargetClass(self): from zope.schema._bootstrapfields import Text return Text + def _getTargetInterface(self): + from zope.schema.interfaces import IText + return IText + def test_ctor_defaults(self): from zope.schema._compat import text_type txt = self._makeOne() @@ -630,15 +688,9 @@ def _getTargetClass(self): from zope.schema._field import TextLine return TextLine - def test_class_conforms_to_ITextLine(self): - from zope.interface.verify import verifyClass - from zope.schema.interfaces import ITextLine - verifyClass(ITextLine, self._getTargetClass()) - - def test_instance_conforms_to_ITextLine(self): - from zope.interface.verify import verifyObject + def _getTargetInterface(self): from zope.schema.interfaces import ITextLine - verifyObject(ITextLine, self._makeOne()) + return ITextLine def test_validate_wrong_types(self): from zope.schema.interfaces import WrongType @@ -677,14 +729,16 @@ def test_constraint(self): self.assertEqual(field.constraint(u'abc\ndef'), False) -class PasswordTests(unittest.TestCase): +class PasswordTests(EqualityTestsMixin, + unittest.TestCase): def _getTargetClass(self): from zope.schema._bootstrapfields import Password return Password - def _makeOne(self, *args, **kw): - return self._getTargetClass()(*args, **kw) + def _getTargetInterface(self): + from zope.schema.interfaces import IPassword + return IPassword def test_set_unchanged(self): klass = self._getTargetClass() @@ -746,6 +800,10 @@ def _getTargetClass(self): from zope.schema._bootstrapfields import Bool return Bool + def _getTargetInterface(self): + from zope.schema.interfaces import IBool + return IBool + def test_ctor_defaults(self): txt = self._makeOne() self.assertEqual(txt._type, bool) @@ -778,51 +836,8 @@ def test_fromUnicode_hit(self): self.assertEqual(txt.fromUnicode(u'True'), True) self.assertEqual(txt.fromUnicode(u'true'), True) -class ConformanceMixin(object): - - def _getTargetClass(self): - raise NotImplementedError - - def _getTargetInterface(self): - raise NotImplementedError - - def _makeOne(self, *args, **kwargs): - return self._getTargetClass()(*args, **kwargs) - - def test_class_conforms_to_iface(self): - from zope.interface.verify import verifyClass - verifyClass(self._getTargetInterface(), self._getTargetClass()) - return verifyClass - - def test_instance_conforms_to_iface(self): - from zope.interface.verify import verifyObject - verifyObject(self._getTargetInterface(), self._makeOne()) - return verifyObject - - -class OrderableMissingValueMixin(ConformanceMixin): - mvm_missing_value = -1 - mvm_default = 0 - - def test_missing_value_no_min_or_max(self): - # We should be able to provide a missing_value without - # also providing a min or max. But note that we must still - # provide a default. - # See https://github.com/zopefoundation/zope.schema/issues/9 - Kind = self._getTargetClass() - self.assertTrue(Kind.min._allow_none) - self.assertTrue(Kind.max._allow_none) - - field = self._makeOne(missing_value=self.mvm_missing_value, - default=self.mvm_default) - self.assertIsNone(field.min) - self.assertIsNone(field.max) - self.assertEqual(self.mvm_missing_value, field.missing_value) - - -class NumberTests(EqualityTestsMixin, - OrderableMissingValueMixin, +class NumberTests(OrderableMissingValueMixin, unittest.TestCase): def _getTargetClass(self): diff --git a/src/zope/schema/tests/test__field.py b/src/zope/schema/tests/test__field.py index dd03bc4..1f36f52 100644 --- a/src/zope/schema/tests/test__field.py +++ b/src/zope/schema/tests/test__field.py @@ -17,7 +17,8 @@ import unittest from zope.schema.tests.test__bootstrapfields import OrderableMissingValueMixin -from zope.schema.tests.test__bootstrapfields import ConformanceMixin +from zope.schema.tests.test__bootstrapfields import EqualityTestsMixin + # pylint:disable=protected-access # pylint:disable=too-many-lines @@ -25,7 +26,7 @@ # pylint:disable=no-member # pylint:disable=blacklisted-name -class BytesTests(ConformanceMixin, unittest.TestCase): +class BytesTests(EqualityTestsMixin, unittest.TestCase): def _getTargetClass(self): from zope.schema._field import Bytes @@ -82,24 +83,16 @@ def test_fromUnicode_hit(self): self.assertEqual(byt.fromUnicode(u'DEADBEEF'), b'DEADBEEF') -class ASCIITests(unittest.TestCase): +class ASCIITests(EqualityTestsMixin, + unittest.TestCase): def _getTargetClass(self): from zope.schema._field import ASCII return ASCII - def _makeOne(self, *args, **kw): - return self._getTargetClass()(*args, **kw) - - def test_class_conforms_to_IASCII(self): - from zope.interface.verify import verifyClass - from zope.schema.interfaces import IASCII - verifyClass(IASCII, self._getTargetClass()) - - def test_instance_conforms_to_IASCII(self): - from zope.interface.verify import verifyObject + def _getTargetInterface(self): from zope.schema.interfaces import IASCII - verifyObject(IASCII, self._makeOne()) + return IASCII def test_validate_wrong_types(self): from zope.schema.interfaces import WrongType @@ -135,24 +128,16 @@ def test__validate_non_empty_hit(self): asc._validate(chr(i)) # doesn't raise -class BytesLineTests(unittest.TestCase): +class BytesLineTests(EqualityTestsMixin, + unittest.TestCase): def _getTargetClass(self): from zope.schema._field import BytesLine return BytesLine - def _makeOne(self, *args, **kw): - return self._getTargetClass()(*args, **kw) - - def test_class_conforms_to_IBytesLine(self): - from zope.interface.verify import verifyClass - from zope.schema.interfaces import IBytesLine - verifyClass(IBytesLine, self._getTargetClass()) - - def test_instance_conforms_to_IBytesLine(self): - from zope.interface.verify import verifyObject + def _getTargetInterface(self): from zope.schema.interfaces import IBytesLine - verifyObject(IBytesLine, self._makeOne()) + return IBytesLine def test_validate_wrong_types(self): from zope.schema.interfaces import WrongType @@ -195,24 +180,16 @@ def test_constraint(self): self.assertEqual(field.constraint(b'abc\ndef'), False) -class ASCIILineTests(unittest.TestCase): +class ASCIILineTests(EqualityTestsMixin, + unittest.TestCase): def _getTargetClass(self): from zope.schema._field import ASCIILine return ASCIILine - def _makeOne(self, *args, **kw): - return self._getTargetClass()(*args, **kw) - - def test_class_conforms_to_IASCIILine(self): - from zope.interface.verify import verifyClass - from zope.schema.interfaces import IASCIILine - verifyClass(IASCIILine, self._getTargetClass()) - - def test_instance_conforms_to_IASCIILine(self): - from zope.interface.verify import verifyObject + def _getTargetInterface(self): from zope.schema.interfaces import IASCIILine - verifyObject(IASCIILine, self._makeOne()) + return IASCIILine def test_validate_wrong_types(self): from zope.schema.interfaces import WrongType @@ -713,7 +690,8 @@ def test_validate_min_and_max(self): self.assertRaises(TooBig, field.validate, t5) -class ChoiceTests(unittest.TestCase): +class ChoiceTests(EqualityTestsMixin, + unittest.TestCase): def setUp(self): from zope.schema.vocabulary import _clear @@ -727,27 +705,31 @@ def _getTargetClass(self): from zope.schema._field import Choice return Choice - def _makeOne(self, *args, **kw): - return self._getTargetClass()(*args, **kw) + from zope.schema.vocabulary import SimpleVocabulary + # SimpleVocabulary uses identity semantics for equality + _default_vocabulary = SimpleVocabulary.fromValues([1, 2, 3]) - def test_class_conforms_to_IChoice(self): - from zope.interface.verify import verifyClass - from zope.schema.interfaces import IChoice - verifyClass(IChoice, self._getTargetClass()) + def _makeOneFromClass(self, cls, *args, **kwargs): + if (not args + and 'vocabulary' not in kwargs + and 'values' not in kwargs + and 'source' not in kwargs): + kwargs['vocabulary'] = self._default_vocabulary + return super(ChoiceTests, self)._makeOneFromClass(cls, *args, **kwargs) - def test_instance_conforms_to_IChoice(self): - from zope.interface.verify import verifyObject + def _getTargetInterface(self): from zope.schema.interfaces import IChoice - verifyObject(IChoice, self._makeOne(values=[1, 2, 3])) + return IChoice + def test_ctor_wo_values_vocabulary_or_source(self): - self.assertRaises(ValueError, self._makeOne) + self.assertRaises(ValueError, self._getTargetClass()) def test_ctor_invalid_vocabulary(self): - self.assertRaises(ValueError, self._makeOne, vocabulary=object()) + self.assertRaises(ValueError, self._getTargetClass(), vocabulary=object()) def test_ctor_invalid_source(self): - self.assertRaises(ValueError, self._makeOne, source=object()) + self.assertRaises(ValueError, self._getTargetClass(), source=object()) def test_ctor_both_vocabulary_and_source(self): self.assertRaises( @@ -963,24 +945,16 @@ def __call__(self, context): self.assertRaises(ConstraintNotSatisfied, clone._validate, 42) -class URITests(unittest.TestCase): +class URITests(EqualityTestsMixin, + unittest.TestCase): def _getTargetClass(self): from zope.schema._field import URI return URI - def _makeOne(self, *args, **kw): - return self._getTargetClass()(*args, **kw) - - def test_class_conforms_to_IURI(self): - from zope.interface.verify import verifyClass - from zope.schema.interfaces import IURI - verifyClass(IURI, self._getTargetClass()) - - def test_instance_conforms_to_IURI(self): - from zope.interface.verify import verifyObject + def _getTargetInterface(self): from zope.schema.interfaces import IURI - verifyObject(IURI, self._makeOne()) + return IURI def test_validate_wrong_types(self): from zope.schema.interfaces import WrongType @@ -1040,24 +1014,16 @@ def test_fromUnicode_invalid(self): field.fromUnicode, u'http://example.com/\nDAV:') -class DottedNameTests(unittest.TestCase): +class DottedNameTests(EqualityTestsMixin, + unittest.TestCase): def _getTargetClass(self): from zope.schema._field import DottedName return DottedName - def _makeOne(self, *args, **kw): - return self._getTargetClass()(*args, **kw) - - def test_class_conforms_to_IDottedName(self): - from zope.interface.verify import verifyClass - from zope.schema.interfaces import IDottedName - verifyClass(IDottedName, self._getTargetClass()) - - def test_instance_conforms_to_IDottedName(self): - from zope.interface.verify import verifyObject + def _getTargetInterface(self): from zope.schema.interfaces import IDottedName - verifyObject(IDottedName, self._makeOne()) + return IDottedName def test_ctor_defaults(self): dotted = self._makeOne() @@ -1158,24 +1124,16 @@ def test_fromUnicode_invalid(self): field.fromUnicode, u'http://example.com/\nDAV:') -class IdTests(unittest.TestCase): +class IdTests(EqualityTestsMixin, + unittest.TestCase): def _getTargetClass(self): from zope.schema._field import Id return Id - def _makeOne(self, *args, **kw): - return self._getTargetClass()(*args, **kw) - - def test_class_conforms_to_IId(self): - from zope.interface.verify import verifyClass - from zope.schema.interfaces import IId - verifyClass(IId, self._getTargetClass()) - - def test_instance_conforms_to_IId(self): - from zope.interface.verify import verifyObject + def _getTargetInterface(self): from zope.schema.interfaces import IId - verifyObject(IId, self._makeOne()) + return IId def test_validate_wrong_types(self): from zope.schema.interfaces import WrongType @@ -1249,7 +1207,7 @@ def test_fromUnicode_invalid(self): field.fromUnicode, u'http://example.com/\nDAV:') -class InterfaceFieldTests(ConformanceMixin, +class InterfaceFieldTests(EqualityTestsMixin, unittest.TestCase): def _getTargetClass(self): @@ -1306,7 +1264,8 @@ class DummyInterface(Interface): self.assertRaises(RequiredMissing, field.validate, None) -class CollectionTests(unittest.TestCase): +class CollectionTests(EqualityTestsMixin, + unittest.TestCase): _DEFAULT_UNIQUE = False @@ -1318,19 +1277,8 @@ def _getTargetInterface(self): from zope.schema.interfaces import ICollection return ICollection - def _makeOne(self, *args, **kw): - return self._getTargetClass()(*args, **kw) - _makeCollection = list - def test_class_conforms_to_iface(self): - from zope.interface.verify import verifyClass - verifyClass(self._getTargetInterface(), self._getTargetClass()) - - def test_instance_conforms_to_iface(self): - from zope.interface.verify import verifyObject - verifyObject(self._getTargetInterface(), self._makeOne()) - def test_schema_defined_by_subclass(self): from zope import interface @@ -1645,7 +1593,8 @@ def _getTargetInterface(self): return IFrozenSet -class ObjectTests(unittest.TestCase): +class ObjectTests(EqualityTestsMixin, + unittest.TestCase): def setUp(self): from zope.event import subscribers @@ -1659,10 +1608,14 @@ def _getTargetClass(self): from zope.schema._field import Object return Object - def _makeOne(self, schema=None, *args, **kw): + def _getTargetInterface(self): + from zope.schema.interfaces import IObject + return IObject + + def _makeOneFromClass(self, cls, schema=None, *args, **kw): if schema is None: schema = self._makeSchema() - return self._getTargetClass()(schema, *args, **kw) + return super(ObjectTests, self)._makeOneFromClass(cls, schema, *args, **kw) def _makeSchema(self, **kw): from zope.interface import Interface @@ -2056,7 +2009,8 @@ class ValueType(object): field.validate(ValueType()) -class MappingTests(unittest.TestCase): +class MappingTests(EqualityTestsMixin, + unittest.TestCase): def _getTargetClass(self): from zope.schema._field import Mapping @@ -2066,17 +2020,6 @@ def _getTargetInterface(self): from zope.schema.interfaces import IMapping return IMapping - def _makeOne(self, *args, **kw): - return self._getTargetClass()(*args, **kw) - - def test_class_conforms_to_iface(self): - from zope.interface.verify import verifyClass - verifyClass(self._getTargetInterface(), self._getTargetClass()) - - def test_instance_conforms_to_iface(self): - from zope.interface.verify import verifyObject - verifyObject(self._getTargetInterface(), self._makeOne()) - def test_ctor_key_type_not_IField(self): self.assertRaises(ValueError, self._makeOne, key_type=object()) From 835e781a884b7b91109ae22e0cefed88849ec059 Mon Sep 17 00:00:00 2001 From: Jason Madden Date: Fri, 31 Aug 2018 13:39:12 -0500 Subject: [PATCH 4/5] Collapse ConformanceMixin and EqualityTestsMixin together since no one inherited just from ConformanceMixin. Remove EqualityTestsMixin from OrderableMissingValueMixin and repeat that everywhere OrderableMissingValueMixin is used to be explicit. --- src/zope/schema/tests/test__bootstrapfields.py | 10 ++++------ src/zope/schema/tests/test__field.py | 12 ++++++------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/zope/schema/tests/test__bootstrapfields.py b/src/zope/schema/tests/test__bootstrapfields.py index c46778b..b675d56 100644 --- a/src/zope/schema/tests/test__bootstrapfields.py +++ b/src/zope/schema/tests/test__bootstrapfields.py @@ -16,7 +16,7 @@ # pylint:disable=protected-access -class ConformanceMixin(object): +class EqualityTestsMixin(object): def _getTargetClass(self): raise NotImplementedError @@ -46,9 +46,6 @@ def test_instance_conforms_to_iface(self): verifyObject(self._getTargetInterface(), instance) return verifyObject - -class EqualityTestsMixin(ConformanceMixin): - def test_is_hashable(self): field = self._makeOne() hash(field) # doesn't raise @@ -121,7 +118,7 @@ def test___eq___same_type_same_attrs(self): self.assertFalse(left != right) -class OrderableMissingValueMixin(EqualityTestsMixin): +class OrderableMissingValueMixin(object): mvm_missing_value = -1 mvm_default = 0 @@ -837,7 +834,8 @@ def test_fromUnicode_hit(self): self.assertEqual(txt.fromUnicode(u'true'), True) -class NumberTests(OrderableMissingValueMixin, +class NumberTests(EqualityTestsMixin, + OrderableMissingValueMixin, unittest.TestCase): def _getTargetClass(self): diff --git a/src/zope/schema/tests/test__field.py b/src/zope/schema/tests/test__field.py index 1f36f52..74dfda5 100644 --- a/src/zope/schema/tests/test__field.py +++ b/src/zope/schema/tests/test__field.py @@ -232,7 +232,7 @@ def test_constraint(self): self.assertEqual(field.constraint('abc\ndef'), False) -class FloatTests(OrderableMissingValueMixin, +class FloatTests(OrderableMissingValueMixin, EqualityTestsMixin, unittest.TestCase): mvm_missing_value = -1.0 @@ -304,7 +304,7 @@ def test_fromUnicode_hit(self): self.assertEqual(flt.fromUnicode(u'1.23e6'), 1230000.0) -class DecimalTests(OrderableMissingValueMixin, +class DecimalTests(OrderableMissingValueMixin, EqualityTestsMixin, unittest.TestCase): mvm_missing_value = decimal.Decimal("-1") @@ -395,7 +395,7 @@ def test_fromUnicode_hit(self): self.assertEqual(flt.fromUnicode(u'12345.6'), Decimal('12345.6')) -class DatetimeTests(OrderableMissingValueMixin, +class DatetimeTests(OrderableMissingValueMixin, EqualityTestsMixin, unittest.TestCase): mvm_missing_value = datetime.datetime.now() @@ -472,7 +472,7 @@ def test_validate_w_min_and_max(self): self.assertRaises(TooBig, field.validate, d5) -class DateTests(OrderableMissingValueMixin, +class DateTests(OrderableMissingValueMixin, EqualityTestsMixin, unittest.TestCase): mvm_missing_value = datetime.date.today() @@ -559,7 +559,7 @@ def test_validate_w_min_and_max(self): self.assertRaises(TooBig, field.validate, d5) -class TimedeltaTests(OrderableMissingValueMixin, +class TimedeltaTests(OrderableMissingValueMixin, EqualityTestsMixin, unittest.TestCase): mvm_missing_value = datetime.timedelta(minutes=15) @@ -624,7 +624,7 @@ def test_validate_min_and_max(self): self.assertRaises(TooBig, field.validate, t5) -class TimeTests(OrderableMissingValueMixin, +class TimeTests(OrderableMissingValueMixin, EqualityTestsMixin, unittest.TestCase): mvm_missing_value = datetime.time(12, 15, 37) From dec22f54b460e345f97c0aefb3273e1468092035 Mon Sep 17 00:00:00 2001 From: Jason Madden Date: Fri, 31 Aug 2018 13:53:32 -0500 Subject: [PATCH 5/5] Make liberal use of ellipsis and +IGNORE_EXCEPTION_DETAIL to restore the value of doctests as documentation. --- src/zope/schema/_bootstrapfields.py | 18 ++++++++--------- src/zope/schema/_field.py | 20 +++++-------------- .../schema/tests/test__bootstrapfields.py | 6 +++++- src/zope/schema/tests/test__field.py | 5 ++++- 4 files changed, 22 insertions(+), 27 deletions(-) diff --git a/src/zope/schema/_bootstrapfields.py b/src/zope/schema/_bootstrapfields.py index f61d9a4..5904068 100644 --- a/src/zope/schema/_bootstrapfields.py +++ b/src/zope/schema/_bootstrapfields.py @@ -369,21 +369,19 @@ def fromUnicode(self, str): >>> from zope.schema import Text >>> from zope.schema._compat import text_type >>> t = Text(constraint=lambda v: 'x' in v) - >>> try: - ... t.fromUnicode(b"foo x spam") - ... except WrongType as e: - ... e.args == (b"foo x spam", text_type, '') - True + >>> t.fromUnicode(b"foo x spam") # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + zope.schema._bootstrapinterfaces.WrongType: ('foo x spam', , '') >>> result = t.fromUnicode(u"foo x spam") >>> isinstance(result, bytes) False >>> str(result) 'foo x spam' - >>> try: - ... t.fromUnicode(u"foo spam") - ... except ConstraintNotSatisfied as e: - ... e.args == (u'foo spam', '') - True + >>> t.fromUnicode(u"foo spam") # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + zope.schema._bootstrapinterfaces.ConstraintNotSatisfied: (u'foo spam', '') """ self.validate(str) return str diff --git a/src/zope/schema/_field.py b/src/zope/schema/_field.py index fa2d6df..5ec0d1b 100644 --- a/src/zope/schema/_field.py +++ b/src/zope/schema/_field.py @@ -555,13 +555,9 @@ def _validate_sequence(value_type, value, errors=None): To validate a sequence of various values: >>> from zope.schema._compat import text_type - >>> errors = _validate_sequence(field, (b'foo', u'bar', 1)) - >>> len(errors) - 2 - >>> errors[0].args == (b'foo', text_type, '') - True - >>> errors[1].args == (1, text_type, '') - True + >>> errors = _validate_sequence(field, (bytearray(b'foo'), u'bar', 1)) + >>> errors + [WrongType(bytearray(b'foo'), <...>, ''), WrongType(1, <...>, '')] The only valid value in the sequence is the second item. The others generated errors. @@ -570,14 +566,8 @@ def _validate_sequence(value_type, value, errors=None): for a new sequence: >>> errors = _validate_sequence(field, (2, u'baz'), errors) - >>> len(errors) - 3 - >>> errors[0].args == (b'foo', text_type, '') - True - >>> errors[1].args == (1, text_type, '') - True - >>> errors[2].args == (2, text_type, '') - True + >>> errors + [WrongType(bytearray(b'foo'), <...>, ''), WrongType(1, <...>, ''), WrongType(2, <...>, '')] """ if errors is None: diff --git a/src/zope/schema/tests/test__bootstrapfields.py b/src/zope/schema/tests/test__bootstrapfields.py index b675d56..419378a 100644 --- a/src/zope/schema/tests/test__bootstrapfields.py +++ b/src/zope/schema/tests/test__bootstrapfields.py @@ -1004,6 +1004,10 @@ def validate(self, value): def test_suite(): import zope.schema._bootstrapfields + from zope.testing.renormalizing import IGNORE_EXCEPTION_MODULE_IN_PYTHON2 suite = unittest.defaultTestLoader.loadTestsFromName(__name__) - suite.addTests(doctest.DocTestSuite(zope.schema._bootstrapfields)) + suite.addTests(doctest.DocTestSuite( + zope.schema._bootstrapfields, + optionflags=doctest.ELLIPSIS|IGNORE_EXCEPTION_MODULE_IN_PYTHON2 + )) return suite diff --git a/src/zope/schema/tests/test__field.py b/src/zope/schema/tests/test__field.py index 74dfda5..cb0e127 100644 --- a/src/zope/schema/tests/test__field.py +++ b/src/zope/schema/tests/test__field.py @@ -2226,5 +2226,8 @@ def get(self, object, name): def test_suite(): import zope.schema._field suite = unittest.defaultTestLoader.loadTestsFromName(__name__) - suite.addTests(doctest.DocTestSuite(zope.schema._field)) + suite.addTests(doctest.DocTestSuite( + zope.schema._field, + optionflags=doctest.ELLIPSIS + )) return suite