diff --git a/CHANGES.rst b/CHANGES.rst index ef005ff5..9b4c3afc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -2,7 +2,9 @@ Revision 0.4.8, released XX-09-2019 ----------------------------------- -No changes yet. +- Added ability of combining `SingleValueConstraint` and + `PermittedAlphabetConstraint` objects into one for proper modeling + `FROM ... EXCEPT ...` ASN.1 clause. Revision 0.4.7, released 01-09-2019 ----------------------------------- diff --git a/pyasn1/type/constraint.py b/pyasn1/type/constraint.py index b66627d2..8f152e9e 100644 --- a/pyasn1/type/constraint.py +++ b/pyasn1/type/constraint.py @@ -103,6 +103,11 @@ class SingleValueConstraint(AbstractConstraint): The SingleValueConstraint satisfies any value that is present in the set of permitted values. + Objects of this type are iterable (emitting constraint values) and + can act as operands for some arithmetic operations e.g. addition + and subtraction. The latter can be used for combining multiple + SingleValueConstraint objects into one. + The SingleValueConstraint object can be applied to any ASN.1 type. @@ -137,6 +142,23 @@ def _testValue(self, value, idx): if value not in self._set: raise error.ValueConstraintError(value) + # Constrains can be merged or reduced + + def __contains__(self, item): + return item in self._set + + def __iter__(self): + return iter(self._set) + + def __sub__(self, constraint): + return self.__class__(*(self._set.difference(constraint))) + + def __add__(self, constraint): + return self.__class__(*(self._set.union(constraint))) + + def __sub__(self, constraint): + return self.__class__(*(self._set.difference(constraint))) + class ContainedSubtypeConstraint(AbstractConstraint): """Create a ContainedSubtypeConstraint object. @@ -305,6 +327,10 @@ class PermittedAlphabetConstraint(SingleValueConstraint): string for as long as all its characters are present in the set of permitted characters. + Objects of this type are iterable (emitting constraint values) and + can act as operands for some arithmetic operations e.g. addition + and subtraction. + The PermittedAlphabetConstraint object can only be applied to the :ref:`character ASN.1 types ` such as :class:`~pyasn1.type.char.IA5String`. @@ -314,8 +340,8 @@ class PermittedAlphabetConstraint(SingleValueConstraint): *alphabet: :class:`str` Full set of characters permitted by this constraint object. - Examples - -------- + Example + ------- .. code-block:: python class BooleanValue(IA5String): @@ -332,6 +358,42 @@ class BooleanValue(IA5String): # this will raise ValueConstraintError garbage = BooleanValue('TAF') + + ASN.1 `FROM ... EXCEPT ...` clause can be modelled by combining multiple + PermittedAlphabetConstraint objects into one: + + Example + ------- + .. code-block:: python + + class Lipogramme(IA5String): + ''' + ASN.1 specification: + + Lipogramme ::= + IA5String (FROM (ALL EXCEPT ("e"|"E"))) + ''' + subtypeSpec = ( + PermittedAlphabetConstraint(*string.printable) - + PermittedAlphabetConstraint('e', 'E') + ) + + # this will succeed + lipogramme = Lipogramme('A work of fiction?') + + # this will raise ValueConstraintError + lipogramme = Lipogramme('Eel') + + Note + ---- + Although `ConstraintsExclusion` object could seemingly be used for this + purpose, practically, for it to work, it needs to represent its operand + constraints as sets and intersect one with the other. That would require + the insight into the constraint values (and their types) that are otherwise + hidden inside the constraint object. + + Therefore it's more practical to model `EXCEPT` clause at + `PermittedAlphabetConstraint` level instead. """ def _setValues(self, values): self._values = values @@ -526,49 +588,41 @@ class ConstraintsExclusion(AbstractConstraint): Parameters ---------- - constraint: - Constraint or logic operator object. + *constraints: + Constraint or logic operator objects. Examples -------- .. code-block:: python - class Lipogramme(IA5STRING): - ''' - ASN.1 specification: - - Lipogramme ::= - IA5String (FROM (ALL EXCEPT ("e"|"E"))) - ''' + class LuckyNumber(Integer): subtypeSpec = ConstraintsExclusion( - PermittedAlphabetConstraint('e', 'E') + SingleValueConstraint(13) ) # this will succeed - lipogramme = Lipogramme('A work of fiction?') + luckyNumber = LuckyNumber(12) # this will raise ValueConstraintError - lipogramme = Lipogramme('Eel') + luckyNumber = LuckyNumber(13) - Warning - ------- - The above example involving PermittedAlphabetConstraint might - not work due to the way how PermittedAlphabetConstraint works. - The other constraints might work with ConstraintsExclusion - though. + Note + ---- + The `FROM ... EXCEPT ...` ASN.1 clause should be modeled by combining + constraint objects into one. See `PermittedAlphabetConstraint` for more + information. """ def _testValue(self, value, idx): - try: - self._values[0](value, idx) - except error.ValueConstraintError: - return - else: + for constraint in self._values: + try: + constraint(value, idx) + + except error.ValueConstraintError: + continue + raise error.ValueConstraintError(value) def _setValues(self, values): - if len(values) != 1: - raise error.PyAsn1Error('Single constraint expected') - AbstractConstraint._setValues(self, values) diff --git a/tests/type/test_constraint.py b/tests/type/test_constraint.py index 0f49c786..7ef62937 100644 --- a/tests/type/test_constraint.py +++ b/tests/type/test_constraint.py @@ -21,8 +21,10 @@ class SingleValueConstraintTestCase(BaseTestCase): def setUp(self): BaseTestCase.setUp(self) - self.c1 = constraint.SingleValueConstraint(1, 2) - self.c2 = constraint.SingleValueConstraint(3, 4) + self.v1 = 1, 2 + self.v2 = 3, 4 + self.c1 = constraint.SingleValueConstraint(*self.v1) + self.c2 = constraint.SingleValueConstraint(*self.v2) def testCmp(self): assert self.c1 == self.c1, 'comparison fails' @@ -45,6 +47,27 @@ def testBadVal(self): else: assert 0, 'constraint check fails' + def testContains(self): + for v in self.v1: + assert v in self.c1 + assert v not in self.c2 + + for v in self.v2: + assert v in self.c2 + assert v not in self.c1 + + def testIter(self): + assert set(self.v1) == set(self.c1) + assert set(self.v2) == set(self.c2) + + def testSub(self): + subconst = self.c1 - constraint.SingleValueConstraint(self.v1[0]) + assert list(subconst) == [self.v1[1]] + + def testAdd(self): + superconst = self.c1 + self.c2 + assert set(superconst) == set(self.v1 + self.v2) + class ContainedSubtypeConstraintTestCase(BaseTestCase): def setUp(self): @@ -110,20 +133,25 @@ def testBadVal(self): class PermittedAlphabetConstraintTestCase(SingleValueConstraintTestCase): def setUp(self): - self.c1 = constraint.PermittedAlphabetConstraint('A', 'B', 'C') - self.c2 = constraint.PermittedAlphabetConstraint('DEF') + self.v1 = 'A', 'B' + self.v2 = 'C', 'D' + self.c1 = constraint.PermittedAlphabetConstraint(*self.v1) + self.c2 = constraint.PermittedAlphabetConstraint(*self.v2) def testGoodVal(self): try: self.c1('A') + except error.ValueConstraintError: assert 0, 'constraint check fails' def testBadVal(self): try: self.c1('E') + except error.ValueConstraintError: pass + else: assert 0, 'constraint check fails'