diff --git a/CHANGES.rst b/CHANGES.rst index 500e178..624d87f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -87,6 +87,16 @@ 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 + `_. + +- Make ``Iterable`` and ``Container`` properly implement ``IIterable`` + and ``IContainer``, respectively. + 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..5904068 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,24 @@ 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") + >>> t.fromUnicode(b"foo x spam") # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - WrongType: ('foo x spam', , '') + zope.schema._bootstrapinterfaces.WrongType: ('foo x spam', , '') >>> result = t.fromUnicode(u"foo x spam") >>> isinstance(result, bytes) False >>> str(result) 'foo x spam' - >>> t.fromUnicode(u"foo spam") + >>> t.fromUnicode(u"foo spam") # doctest: +IGNORE_EXCEPTION_DETAIL Traceback (most recent call last): ... - ConstraintNotSatisfied: (u'foo spam', '') + zope.schema._bootstrapinterfaces.ConstraintNotSatisfied: (u'foo spam', '') """ self.validate(str) return str @@ -453,32 +461,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..5ec0d1b 100644 --- a/src/zope/schema/_field.py +++ b/src/zope/schema/_field.py @@ -46,6 +46,8 @@ from zope.schema.interfaces import IBytesLine from zope.schema.interfaces import IChoice 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 @@ -57,7 +59,9 @@ 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 from zope.schema.interfaces import IList from zope.schema.interfaces import IMinMaxLen @@ -65,7 +69,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 +98,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 +106,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 @@ -113,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']) @@ -132,6 +141,14 @@ classImplements(Password, IPassword) classImplements(Bool, IBool) classImplements(Bool, IFromUnicode) +classImplements(Iterable, IIterable) +classImplements(Container, IContainer) + +classImplements(Number, INumber) +classImplements(Complex, IComplex) +classImplements(Real, IReal) +classImplements(Rational, IRational) +classImplements(Integral, IIntegral) classImplements(Int, IInt) @@ -203,48 +220,80 @@ 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): - - def __init__(self, literal): - super(InvalidDecimalLiteral, self).__init__( - "invalid literal for Decimal(): %s" % literal) + "Raised by decimal fields" @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 +554,10 @@ def _validate_sequence(value_type, value, errors=None): To validate a sequence of various values: - >>> errors = _validate_sequence(field, (b'foo', u'bar', 1)) - >>> errors # XXX assumes Python2 reprs - [WrongType('foo', , ''), WrongType(1, , '')] + >>> from zope.schema._compat import text_type + >>> 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. @@ -516,8 +566,8 @@ 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, , '')] + >>> errors + [WrongType(bytearray(b'foo'), <...>, ''), WrongType(1, <...>, ''), WrongType(2, <...>, '')] """ 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..419378a 100644 --- a/src/zope/schema/tests/test__bootstrapfields.py +++ b/src/zope/schema/tests/test__bootstrapfields.py @@ -11,8 +11,132 @@ # FOR A PARTICULAR PURPOSE. # ############################################################################## +import doctest import unittest +# pylint:disable=protected-access + +class EqualityTestsMixin(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 + + 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(object): + 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): @@ -142,86 +266,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): @@ -229,8 +273,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): @@ -432,6 +477,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) @@ -482,6 +531,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() @@ -566,6 +619,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() @@ -628,15 +685,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 @@ -675,14 +726,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() @@ -744,6 +797,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) @@ -777,39 +834,87 @@ def test_fromUnicode_hit(self): self.assertEqual(txt.fromUnicode(u'true'), True) -class OrderableMissingValueMixin(object): +class NumberTests(EqualityTestsMixin, + OrderableMissingValueMixin, + unittest.TestCase): - mvm_missing_value = -1 - mvm_default = 0 + def _getTargetClass(self): + from zope.schema._bootstrapfields import Number + return Number - 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) + def _getTargetInterface(self): + from zope.schema.interfaces import INumber + return INumber - 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) + 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 IntTests(EqualityTestsMixin, - OrderableMissingValueMixin, - unittest.TestCase): +class ComplexTests(NumberTests): def _getTargetClass(self): - from zope.schema._bootstrapfields import Int - return Int + from zope.schema._bootstrapfields import Complex + return Complex - 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 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 +975,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 +1000,14 @@ 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 + from zope.testing.renormalizing import IGNORE_EXCEPTION_MODULE_IN_PYTHON2 + suite = unittest.defaultTestLoader.loadTestsFromName(__name__) + 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 c035e85..cb0e127 100644 --- a/src/zope/schema/tests/test__field.py +++ b/src/zope/schema/tests/test__field.py @@ -13,9 +13,12 @@ ############################################################################## import datetime import decimal +import doctest import unittest from zope.schema.tests.test__bootstrapfields import OrderableMissingValueMixin +from zope.schema.tests.test__bootstrapfields import EqualityTestsMixin + # pylint:disable=protected-access # pylint:disable=too-many-lines @@ -23,24 +26,15 @@ # pylint:disable=no-member # pylint:disable=blacklisted-name -class BytesTests(unittest.TestCase): +class BytesTests(EqualityTestsMixin, 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 @@ -89,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 @@ -142,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 @@ -202,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 @@ -262,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 @@ -272,18 +242,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) @@ -343,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") @@ -353,18 +314,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) @@ -443,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() @@ -453,18 +405,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 @@ -529,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() @@ -539,18 +482,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 @@ -625,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) @@ -635,18 +569,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 @@ -699,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) @@ -709,18 +634,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 @@ -774,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 @@ -788,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( @@ -1024,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 @@ -1101,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() @@ -1219,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 @@ -1310,24 +1207,16 @@ def test_fromUnicode_invalid(self): field.fromUnicode, u'http://example.com/\nDAV:') -class InterfaceFieldTests(unittest.TestCase): +class InterfaceFieldTests(EqualityTestsMixin, + 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 @@ -1375,7 +1264,8 @@ class DummyInterface(Interface): self.assertRaises(RequiredMissing, field.validate, None) -class CollectionTests(unittest.TestCase): +class CollectionTests(EqualityTestsMixin, + unittest.TestCase): _DEFAULT_UNIQUE = False @@ -1387,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 @@ -1714,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 @@ -1728,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 @@ -2125,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 @@ -2135,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()) @@ -2347,3 +2221,13 @@ 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, + optionflags=doctest.ELLIPSIS + )) + return suite