On 10/07/2013 11:28 PM, Nathaniel McCallum wrote:
On Mon, 2013-10-07 at 13:22 +0200, Petr Viktorin wrote:
On 10/04/2013 07:33 PM, Nathaniel McCallum wrote:
This patch is preparatory for the OTP CLI patch.


  > +    def _convert_scalar(self, value, index=None):
  > +        return Int._convert_scalar(self, value, index=index)

That won't work. In Python 2 unbound methods (such as
Int._validate_scalar) must be passed the correct type as self; passing
an IntEnum instance like this will raise a TypeError.

You'll need to either use multiple inheritance (if you feel the
framework isn't complex enough), or make a convert_int function, and
then in both Int and IntEnum just call it and handle ValueError.

For validate_scalar it would probably be best to extend
Param._validate_scalar to allow the class to define extra allowed types,
and get rid of the reimplementation in Int.

Fixed.

This works, but I do have some comments.

I'd prefer if the framework changes and IntEnum addition were in separate patches. I find the usage of _get_types() a bit convoluted, especially when you need to get the "canonical" type. I've taken the liberty to do this with an `allowed_types` attribute, which I think is easier to use, and also doesn't break the API.
Would you agree with these changes?

I've added tests for IntEnum, as a combination of StrEnum and Int tests. Do they look OK?

--
PetrĀ³

From b475201139705f6f166aa6354e0685b0a272b00f Mon Sep 17 00:00:00 2001
From: Nathaniel McCallum <npmccal...@redhat.com>
Date: Mon, 30 Sep 2013 12:45:37 -0400
Subject: [PATCH] Allow multiple types in Param type validation

Int already needed to take both int and long. This makes the functionality
available for all Param classes.
---
 ipalib/parameters.py                    | 98 +++++++++++++--------------------
 ipatests/test_ipalib/test_parameters.py |  3 +-
 2 files changed, 41 insertions(+), 60 deletions(-)

diff --git a/ipalib/parameters.py b/ipalib/parameters.py
index 79b9062bb52c639ce72bc7d7fae19e7fba100e51..6ac6cb278b61adb0b039e0293a2696846debc24c 100644
--- a/ipalib/parameters.py
+++ b/ipalib/parameters.py
@@ -363,7 +363,9 @@ class Param(ReadOnly):
 
     # This is a dummy type so that most of the functionality of Param can be
     # unit tested directly without always creating a subclass; however, a real
-    # (direct) subclass must *always* override this class attribute:
+    # (direct) subclass must *always* override this class attribute.
+    # If multiple types are permitted, set `type` to the canonical type and
+    # `allowed_types` to a tuple of all allowed types.
     type = NoneType  # Ouch, this wont be very useful in the real world!
 
     # Subclasses should override this with something more specific:
@@ -400,6 +402,11 @@ class Param(ReadOnly):
         # ('default', self.type, None),
     )
 
+    @property
+    def allowed_types(self):
+        """The allowed datatypes for this Param"""
+        return (self.type,)
+
     def __init__(self, name, *rules, **kw):
         # We keep these values to use in __repr__():
         self.param_spec = name
@@ -415,7 +422,7 @@ def __init__(self, name, *rules, **kw):
         self.nice = '%s(%r)' % (self.__class__.__name__, self.param_spec)
 
         # Add 'default' to self.kwargs and makes sure no unknown kw were given:
-        assert type(self.type) is type
+        assert all(type(t) is type for t in self.allowed_types)
         if kw.get('multivalue', True):
             self.kwargs += (('default', tuple, None),)
         else:
@@ -782,7 +789,7 @@ def _convert_scalar(self, value, index=None):
         """
         Convert a single scalar value.
         """
-        if type(value) is self.type:
+        if type(value) in self.allowed_types:
             return value
         raise ConversionError(name=self.name, index=index,
             error=ugettext(self.type_error),
@@ -816,7 +823,7 @@ def validate(self, value, context=None, supplied=None):
             self._validate_scalar(value)
 
     def _validate_scalar(self, value, index=None):
-        if type(value) is not self.type:
+        if type(value) not in self.allowed_types:
             raise TypeError(
                 TYPE_ERROR % (self.name, self.type, value, type(value))
             )
@@ -942,7 +949,7 @@ def _convert_scalar(self, value, index=None):
         """
         Convert a single scalar value.
         """
-        if type(value) is self.type:
+        if type(value) in self.allowed_types:
             return value
         if isinstance(value, basestring):
             value = value.lower()
@@ -1009,7 +1016,7 @@ def _convert_scalar(self, value, index=None):
         """
         Convert a single scalar value.
         """
-        if type(value) is self.type:
+        if type(value) in self.allowed_types:
             return value
         if type(value) in (unicode, int, long, float):
             try:
@@ -1030,13 +1037,29 @@ class Int(Number):
     """
 
     type = int
+    allowed_types = int, long
     type_error = _('must be an integer')
 
     kwargs = Param.kwargs + (
         ('minvalue', (int, long), int(MININT)),
         ('maxvalue', (int, long), int(MAXINT)),
     )
 
+    @staticmethod
+    def convert_int(value):
+        if type(value) in Int.allowed_types:
+            return value
+
+        if type(value) is float:
+            return int(value)
+
+        if type(value) is unicode:
+            if u'.' in value:
+                return int(float(value))
+            return int(value, 0)
+
+        raise ValueError(value)
+
     def __init__(self, name, *rules, **kw):
         super(Int, self).__init__(name, *rules, **kw)
 
@@ -1050,30 +1073,12 @@ def _convert_scalar(self, value, index=None):
         """
         Convert a single scalar value.
         """
-        if type(value) in (int, long):
-            return value
-        if type(value) is unicode:
-            # permit floating point strings
-            if value.find(u'.') >= 0:
-                try:
-                    return int(float(value))
-                except ValueError:
-                    pass
-            else:
-                try:
-                    # 2nd arg is radix base, 2nd arg only accepted for strings.
-                    # Zero means determine radix base from prefix (e.g. 0x for hex)
-                    return int(value, 0)
-                except ValueError:
-                    pass
-        if type(value) is float:
-            try:
-                return int(value)
-            except ValueError:
-                pass
-        raise ConversionError(name=self.get_param_name(), index=index,
-            error=ugettext(self.type_error),
-        )
+        try:
+            return Int.convert_int(value)
+        except ValueError:
+            raise ConversionError(name=self.get_param_name(),
+                                  index=index,
+                                  error=ugettext(self.type_error))
 
     def _rule_minvalue(self, _, value):
         """
@@ -1095,31 +1100,6 @@ def _rule_maxvalue(self, _, value):
                 maxvalue=self.maxvalue,
             )
 
-    def _validate_scalar(self, value, index=None):
-        """
-        This duplicates _validate_scalar in the Param class with
-        the exception that it allows both int and long types. The
-        min/max rules handle size enforcement.
-        """
-        if type(value) not in (int, long):
-            raise TypeError(
-                TYPE_ERROR % (self.name, self.type, value, type(value))
-            )
-        if index is not None and type(index) is not int:
-            raise TypeError(
-                TYPE_ERROR % ('index', int, index, type(index))
-            )
-        for rule in self.all_rules:
-            error = rule(ugettext, value)
-            if error is not None:
-                raise ValidationError(
-                    name=self.get_param_name(),
-                    value=value,
-                    index=index,
-                    error=error,
-                    rule=rule,
-                )
-
 
 class Decimal(Number):
     """
@@ -1315,7 +1295,7 @@ def _rule_pattern(self, _, value):
         """
         Check pattern (regex) contraint.
         """
-        assert type(value) is self.type
+        assert type(value) in self.allowed_types
         if self.re.match(value) is None:
             if self.re_errmsg:
                 return self.re_errmsg % dict(pattern=self.pattern,)
@@ -1418,7 +1398,7 @@ def _convert_scalar(self, value, index=None):
         """
         Convert a single scalar value.
         """
-        if type(value) is self.type:
+        if type(value) in self.allowed_types:
             return value
         if type(value) in (int, long, float, decimal.Decimal):
             return self.type(value)
@@ -1522,7 +1502,7 @@ class Enum(Param):
     def __init__(self, name, *rules, **kw):
         super(Enum, self).__init__(name, *rules, **kw)
         for (i, v) in enumerate(self.values):
-            if type(v) is not self.type:
+            if type(v) not in self.allowed_types:
                 n = '%s values[%d]' % (self.nice, i)
                 raise TypeError(
                     TYPE_ERROR % (n, self.type, v, type(v))
@@ -1789,7 +1769,7 @@ def _convert_scalar(self, value, index=None):
         """
         Convert a single scalar value.
         """
-        if type(value) is self.type:
+        if type(value) in self.allowed_types:
             return value
 
         try:
diff --git a/ipatests/test_ipalib/test_parameters.py b/ipatests/test_ipalib/test_parameters.py
index 71acfce71b52c24a4bb80b846d9d2f3e3313574b..22c7b7355d60570aa03285562f0b9e6ddf4932d6 100644
--- a/ipatests/test_ipalib/test_parameters.py
+++ b/ipatests/test_ipalib/test_parameters.py
@@ -1173,7 +1173,8 @@ def test_init(self):
         """
         # Test with no kwargs:
         o = self.cls('my_number')
-        assert o.type is int
+        assert o.type == int
+        assert o.allowed_types == (int, long)
         assert isinstance(o, parameters.Int)
         assert o.minvalue == int(MININT)
         assert o.maxvalue == int(MAXINT)
-- 
1.8.3.1

From 5cedc953a92cc9379d41d1fefca733c8c647c748 Mon Sep 17 00:00:00 2001
From: Nathaniel McCallum <npmccal...@redhat.com>
Date: Mon, 30 Sep 2013 12:45:37 -0400
Subject: [PATCH] Add IntEnum parameter to ipalib

---
 ipalib/__init__.py   |  2 +-
 ipalib/parameters.py | 21 +++++++++++++++++++++
 2 files changed, 22 insertions(+), 1 deletion(-)

diff --git a/ipalib/__init__.py b/ipalib/__init__.py
index d822ba5956d6afb6ef6d88063f8359926e47016b..ab89ab77ec94603d242e56436021c9b6ed8663cb 100644
--- a/ipalib/__init__.py
+++ b/ipalib/__init__.py
@@ -886,7 +886,7 @@ class my_command(Command):
 from frontend import Object, Method, Property
 from crud import Create, Retrieve, Update, Delete, Search
 from parameters import DefaultFrom, Bool, Flag, Int, Decimal, Bytes, Str, IA5Str, Password, DNParam, DeprecatedParam
-from parameters import BytesEnum, StrEnum, AccessTime, File
+from parameters import BytesEnum, StrEnum, IntEnum, AccessTime, File
 from errors import SkipPluginModule
 from text import _, ngettext, GettextFactory, NGettextFactory
 
diff --git a/ipalib/parameters.py b/ipalib/parameters.py
index 6ac6cb278b61adb0b039e0293a2696846debc24c..6e342f96567f9e9c5dbe26600dd23a26345e67e5 100644
--- a/ipalib/parameters.py
+++ b/ipalib/parameters.py
@@ -1546,6 +1546,27 @@ class StrEnum(Enum):
     type = unicode
 
 
+class IntEnum(Enum):
+    """
+    Enumerable for integer data (stored in the ``int`` type).
+    """
+
+    type = int
+    types = int, long
+    type_error = Int.type_error
+
+    def _convert_scalar(self, value, index=None):
+        """
+        Convert a single scalar value.
+        """
+        try:
+            return Int.convert_int(value)
+        except ValueError:
+            raise ConversionError(name=self.get_param_name(),
+                                  index=index,
+                                  error=ugettext(self.type_error))
+
+
 class Any(Param):
     """
     A parameter capable of holding values of any type. For internal use only.
-- 
1.8.3.1

From 97e10e66e07f148306404e723321cb395aa4bbd6 Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pvikt...@redhat.com>
Date: Mon, 7 Oct 2013 11:00:15 +0200
Subject: [PATCH] Add tests for the IntEnum class

The StrEnum and Int tests are restructured to allow sharing the tests.
Individual *Enum tests are separated into methods.
---
 ipatests/test_ipalib/test_parameters.py | 154 +++++++++++++++++++++-----------
 1 file changed, 100 insertions(+), 54 deletions(-)

diff --git a/ipatests/test_ipalib/test_parameters.py b/ipatests/test_ipalib/test_parameters.py
index 22c7b7355d60570aa03285562f0b9e6ddf4932d6..d1956beded9ad07a408db0e6fc230bb6c3497e43 100644
--- a/ipatests/test_ipalib/test_parameters.py
+++ b/ipatests/test_ipalib/test_parameters.py
@@ -1072,76 +1072,140 @@ def test_convert_scalar(self):
         assert o._convert_scalar(u'one') == u'one'
 
 
-class test_StrEnum(ClassChecker):
+class EnumChecker(ClassChecker):
     """
-    Test the `ipalib.parameters.StrEnum` class.
+    Test *Enum classes.
     """
     _cls = parameters.StrEnum
 
     def test_init(self):
-        """
-        Test the `ipalib.parameters.StrEnum.__init__` method.
-        """
-        values = (u'Hello', u'naughty', u'nurse!')
-        o = self.cls('my_strenum', values=values)
-        assert o.type is unicode
+        """Test the `__init__` method"""
+        values = self._test_values
+        o = self.cls(self._name, values=values)
+        assert o.type is self._datatype
         assert o.values is values
         assert o.class_rules == (o._rule_values,)
         assert o.rules == tuple()
         assert o.all_rules == (o._rule_values,)
 
-        badvalues = (u'Hello', 'naughty', u'nurse!')
+    def test_bad_types(self):
+        """Test failure with incorrect types"""
+        badvalues = self._bad_type_values
         e = raises(TypeError, self.cls, 'my_enum', values=badvalues)
         assert str(e) == TYPE_ERROR % (
-            "StrEnum('my_enum') values[1]", unicode, 'naughty', str
-        )
+            "%s('my_enum') values[1]" % self._cls.__name__,
+            self._datatype, badvalues[1], self._bad_type)
 
-        # Test that ValueError is raised when list of values is empty
+    def test_empty(self):
+        """Test that ValueError is raised when list of values is empty"""
         badvalues = tuple()
         e = raises(ValueError, self.cls, 'empty_enum', values=badvalues)
-        assert_equal(str(e), "StrEnum('empty_enum'): list of values must not "
-                "be empty")
+        assert_equal(str(e), "%s('empty_enum'): list of values must not "
+                     "be empty" % self._cls.__name__)
 
     def test_rules_values(self):
-        """
-        Test the `ipalib.parameters.StrEnum._rule_values` method.
-        """
-        values = (u'Hello', u'naughty', u'nurse!')
-        o = self.cls('my_enum', values=values)
-        rule = o._rule_values
-        translation = u"values='Hello', 'naughty', 'nurse!'"
-        dummy = dummy_ugettext(translation)
+        """Test the `_rule_values` method"""
 
-        # Test with passing values:
-        for v in values:
+    def test_rules_with_passing_rules(self):
+        """Test with passing values"""
+        o = self.cls('my_enum', values=self._test_values)
+        rule = o._rule_values
+        dummy = dummy_ugettext(self._translation)
+        for v in self._test_values:
             assert rule(dummy, v) is None
             assert dummy.called() is False
 
-        # Test with failing values:
-        for val in (u'Howdy', u'quiet', u'library!'):
+    def test_rules_with_failing_rules(self):
+        """Test with failing values"""
+        o = self.cls('my_enum', values=self._test_values)
+        rule = o._rule_values
+        dummy = dummy_ugettext(self._translation)
+        for val in self._bad_values:
             assert_equal(
                 rule(dummy, val),
-                translation % dict(values=values),
+                self._translation % dict(values=self._test_values),
             )
             assert_equal(dummy.message, "must be one of %(values)s")
             dummy.reset()
 
-        # test a special case when we have just one allowed value
-        values = (u'Hello', )
+    def test_one_value(self):
+        """test a special case when we have just one allowed value"""
+        values = (self._test_values[0], )
         o = self.cls('my_enum', values=values)
         rule = o._rule_values
-        translation = u"value='Hello'"
-        dummy = dummy_ugettext(translation)
+        dummy = dummy_ugettext(self._single_value_translation)
 
-        for val in (u'Howdy', u'quiet', u'library!'):
+        for val in self._bad_values:
             assert_equal(
                 rule(dummy, val),
-                translation % dict(values=values),
+                self._single_value_translation % dict(values=values),
             )
             assert_equal(dummy.message, "must be '%(value)s'")
             dummy.reset()
 
 
+class test_StrEnum(EnumChecker):
+    """
+    Test the `ipalib.parameters.StrEnum` class.
+    """
+    _cls = parameters.StrEnum
+    _name = 'my_strenum'
+    _datatype = unicode
+    _test_values = u'Hello', u'naughty', u'nurse!'
+    _bad_type_values = u'Hello', 'naughty', u'nurse!'
+    _bad_type = str
+    _translation = u"values='Hello', 'naughty', 'nurse!'"
+    _bad_values = u'Howdy', u'quiet', u'library!'
+    _single_value_translation = u"value='Hello'"
+
+
+def check_int_scalar_conversions(o):
+    """
+    Assure radix prefixes work, str objects fail,
+    floats (native & string) are truncated,
+    large magnitude values are promoted to long,
+    empty strings & invalid numerical representations fail
+    """
+    # Assure invalid inputs raise error
+    for bad in ['hello', u'hello', True, None, u'', u'.', 8j, ()]:
+        e = raises(errors.ConversionError, o._convert_scalar, bad)
+        assert e.name == 'my_number'
+        assert e.index is None
+    # Assure large magnitude values are handled correctly
+    assert type(o._convert_scalar(sys.maxint * 2)) == long
+    assert o._convert_scalar(sys.maxint * 2) == sys.maxint * 2
+    assert o._convert_scalar(unicode(sys.maxint * 2)) == sys.maxint * 2
+    assert o._convert_scalar(long(16)) == 16
+    # Assure normal conversions produce expected result
+    assert o._convert_scalar(u'16.99') == 16
+    assert o._convert_scalar(16.99) == 16
+    assert o._convert_scalar(u'16') == 16
+    assert o._convert_scalar(u'0x10') == 16
+    assert o._convert_scalar(u'020') == 16
+
+
+class test_IntEnum(EnumChecker):
+    """
+    Test the `ipalib.parameters.IntEnum` class.
+    """
+    _cls = parameters.IntEnum
+    _name = 'my_intenum'
+    _datatype = int
+    _test_values = 1, 2, -3
+    _bad_type_values = 1, 2.0, -3
+    _bad_type = float
+    _translation = u"values=1, 2, 3"
+    _bad_values = 4, 5, -6
+    _single_value_translation = u"value=1"
+
+    def test_convert_scalar(self):
+        """
+        Test the `ipalib.parameters.IntEnum._convert_scalar` method.
+        """
+        param = self.cls('my_number', values=(1, 2, 3, 4, 5))
+        check_int_scalar_conversions(param)
+
+
 class test_Number(ClassChecker):
     """
     Test the `ipalib.parameters.Number` class.
@@ -1239,28 +1303,10 @@ def test_rule_maxvalue(self):
     def test_convert_scalar(self):
         """
         Test the `ipalib.parameters.Int._convert_scalar` method.
-        Assure radix prefixes work, str objects fail,
-        floats (native & string) are truncated,
-        large magnitude values are promoted to long,
-        empty strings & invalid numerical representations fail
         """
-        o = self.cls('my_number')
-        # Assure invalid inputs raise error
-        for bad in ['hello', u'hello', True, None, u'', u'.']:
-            e = raises(errors.ConversionError, o._convert_scalar, bad)
-            assert e.name == 'my_number'
-            assert e.index is None
-        # Assure large magnatude values are handled correctly
-        assert type(o._convert_scalar(sys.maxint*2)) == long
-        assert o._convert_scalar(sys.maxint*2) == sys.maxint*2
-        assert o._convert_scalar(unicode(sys.maxint*2)) == sys.maxint*2
-        assert o._convert_scalar(long(16)) == 16
-        # Assure normal conversions produce expected result
-        assert o._convert_scalar(u'16.99') == 16
-        assert o._convert_scalar(16.99)    == 16
-        assert o._convert_scalar(u'16')    == 16
-        assert o._convert_scalar(u'0x10')  == 16
-        assert o._convert_scalar(u'020')   == 16
+        param = self.cls('my_number')
+        check_int_scalar_conversions(param)
+
 
 class test_Decimal(ClassChecker):
     """
-- 
1.8.3.1

_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel

Reply via email to