Skip to content

Commit

Permalink
Allow combining constraints operating on sets (#181)
Browse files Browse the repository at this point in the history
Added ability of combining `SingleValueConstraint` and
`PermittedAlphabetConstraint` objects into one for proper
modeling `FROM ... EXCEPT ...` ASN.1 clause.
  • Loading branch information
etingof authored Nov 3, 2019
1 parent 40d5a7f commit c42c23e
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 33 deletions.
4 changes: 3 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-----------------------------------
Expand Down
110 changes: 82 additions & 28 deletions pyasn1/type/constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 <type.char>` such as
:class:`~pyasn1.type.char.IA5String`.
Expand All @@ -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):
Expand All @@ -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
Expand Down Expand Up @@ -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)


Expand Down
36 changes: 32 additions & 4 deletions tests/type/test_constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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):
Expand Down Expand Up @@ -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'

Expand Down

0 comments on commit c42c23e

Please sign in to comment.