Skip to content

Rule - constrained type

In utype, the role of Rule is to impose constraints on types, and we will explain its use in detail in this document.

Built-in constraints

The Rule class has a series of built-in constraints. When you inherit from Rule, you only need to declare the constraint name as an attribute to get the ability of the constraint. Rule currently supports the following built-in constraints

Range constraints

Range constraints are used to limit the range of data, such as maximum, minimum, and so on. They include

  • gt: The input value must be greater than gt (>)
  • ge: The input value must be greater than or equal to ge (>=)
  • lt: The input value must be less than lt (<)
  • le: The input value must be less than or equal to le (<=)
    from utype import Rule, exc
    
    class WeekDay(int, Rule):  
        ge = 1  
        le = 7
    
    assert WeekDay('3.0') == 3
    
    # Input that violate constrants
    try:
        WeekDay(8)
    except exc.ConstraintError as e:
        print(e) 
        """
        Constraint: <le>: 7 violated
        """
    

Warning

If you specified maximum ( lt / le ) and minimum ( gt / ge ) at the same time, the maxinum value should greater or equal than the minimum, and have the same type as the minimum

Range constraints are not restricted to types, and can be supported as long as the type have corresponding comparison method ( __gt__, __ge__, __lt__, __le__), such as

from utype import Rule, exc
from datetime import datetime

class Year2020(Rule, datetime):  
    ge = datetime(2020, 1, 1)  
    lt = datetime(2021, 1, 1)

assert Year2020('2020-03-04') == datetime(2020, 3, 4) 

try:
    Year2020('2021-01-01')
except exc.ConstraintError as e:
    print(e)
    """
    Constraint: <lt>: datetime.datetime(2021, 1, 1, 0, 0) violated
    """

Note

Every constraint violation will throw an utype.exc.ConstraintError, which has the information of the violated contraint name, value and the input value But if are not sure about the cause of the parsing error, you shoul use the base Exception class utype.exc.ParseError to capture

Length constraints

Length constraints are used to limit the length of data and are typically used to validate data such as strings or lists, the constraints including

  • length: length of input data must be equal to length value
  • max_length: length of the input data must be less than or equal to max_length
  • min_length: length of the input data must be greater than or equal to min_length
    from utype import Rule, exc
    
    class LengthRule(Rule):
        max_length = 3
        min_length = 1
    
    assert LengthRule([1, 2, 3]) == [1, 2, 3]
    
    try:
        LengthRule('abcde')
    except exc.ConstraintError as e:
        print(e)
        """
        Constraint: <max_length>: 3 violated
        """
    

All length constraints must be positive integers. If is length set, you cannot set max_length or min_length

Note

If the input type does not defining the __len__ methods (such as int), utype will validate the length of the value converted to string (like len(str(value))), so if you are validating the digits of the numbers, use max_digits instead

Regex constraints

Regular expressions are often able to declare more complex string validation rules for many purposes.

  • regex: specifies a regular expression that the data must exactly match
    from utype import Rule, exc
    
    class Email(str, Rule):  
        regex = r"([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.[A-Z|a-z]{2,})+"
    
    assert Email('dev@utype.io') == 'dev@utype.io'
    
    try:
        Email('invalid#email.com')
    except exc.ConstraintError as e:
        print(e)
        """
        Constraint: <regex>: 
        '([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\\.[A-Z|a-z]{2,})+' violated
        """
    

In the example, we declare a constraint type Email that is used to validate email addresses.

Const and enum

  • const: input data must exactly equa to the const constant.
    from utype import Rule, exc
    
    class Const1(Rule):
        const = 1
    
    class ConstKey(str, Rule):
        const = 'SECRET_KEY'
    
    assert ConstKey(b'SECRET_KEY') == 'SECRET_KEY'
    
    try:
        Const1(True)
    except exc.ConstraintError as e:
        print(e)
        """
        Constraint: <const>: 1 violated
        """
    

If you specify a source type for a constant constraint, the Rule performs the type conversion first and then checks whether the constant is equal, otherwise it makes a direct comparison

Note

const not only verifies that a value is “equal” to a constant using Python’s equal symbol ( == ) , but also check that their types are equal, because a type can be made equal to any value by overriding the __eq__ method. For example True == 1 is true, and True is bool of type while 1 is of int type, So True can’t pass the const=1 verification.

  • enum: pass in a list, set, or an Enum class. The data must be within the value range specified by enum.
    from utype import Rule, exc
    
    class Infinity(float, Rule):  
        enum = [float("inf"), float("-inf")]
    
    assert Infinity('-infinity') == float("-inf")
    
    try:
        Infinity(10.5)
    except exc.ConstraintError as e:
        print(e)
        """
        Constraint: <enum>: [inf, -inf] violated
        """
    

Warning

enum accept Enum subclass does not means it will convert the input to an instance of that Enum subclass, but rather using the range specified by Enum subclass. if you like to convert data to an instance of Enum subclass, use that class directly as the type annotation

Note

In type annotation, you can use Literal[...] to declare constant or enums with the same effect

Numeric constraints

Numeric constraints are used to place restrictions on numeric types (int, float, Decimal), including

  • max_digits: limit the maximum digits in a number (excluding the sign bit or decimal point)
  • multiple_of: the input number must be multiple of the multiple_of
    from utype import Rule, exc
    
    class Hundreds(int, Rule):
        max_digits = 3
        multiple_of = 100
    
    assert Hundreds('200') == 200
    
    try:
        Hundreds(1000)
    except exc.ConstraintError as e:
        print(e)
        """
        Constraint: <max_digits>: 3 violated
        """
    
    try:
        Hundreds(120)
    except exc.ConstraintError as e:
        print(e)
        """
        Constraint: <multiple_of>: 100 violated
        """
    

Note

If the number is between 0 and 1 (like 0.0123), the 0 on the integer side does not count as a digit, we only calculate 4 digits in the decimal places, so max_digits can be understand as "maximum significant digits"

  • decimal_places limit the maximum number of digits in the decimal part of a number to this value
    from utype import Rule, exc
    import decimal
    
    class ConDecimal(decimal.Decimal, Rule):  
        decimal_places = 2  
        max_digits = 4
    
    assert ConDecimal(1.5) == decimal.Decimal('1.50')
    
    try:
        ConDecimal(123.4)   
    except exc.ConstraintError as e:
        print(e)
        """
        Constraint: <max_digits>: 4 violated
        """
    
    try:
        ConDecimal('1.500')   
    except exc.ConstraintError as e:
        print(e)
        """
        Constraint: <decimal_places>: 2 violated
        """
    

If the source type of the constraint is decimal.Decimal, when the decimal digits of the input data are insufficient, it will be completed first, so the data 123.4 will be completed first Decimal('123.40'), and then the verification max_digits will not pass.

And if the incoming data contains a decimal place, it will be calculated whether the end is 0 or not, for example Decimal('1.500'), 3 digits will be calculated according to the decimal place of the fixed-point number.

Array constraints

Array constraints are used to constrain lists, tuples, sets, and other data that can traverse a single element, the constrants including

  • contains: specifies a type, which can be a normal type or a constrainted type. The data must contain (at least 1) matching elements.
  • max_contains: the maximum number of elements that matching contains type
  • min_contains: the minimum number of elements that matching contains type
    from utype import Rule, exc
    
    class Const1(int, Rule):
        const = 1
    
    class ConTuple(tuple, Rule):
        contains = Const1
        max_contains = 3
    
    assert ConTuple([1, True]) == (1, True)
    
    try:
        ConTuple([0, 2]) 
    except exc.ConstraintError as e:
        print(e)
        """
        Constraint: <contains>: Const1(int, const=1) violated: 
        Const1(int, const=1) not contained in value
        """
    
    try:
        ConTuple([1, True, b'1', '1.0'])
    except exc.ConstraintError as e:
        print(e)
        """
        Constraint: <max_contains>: 3 violated: 
        value contains 4 of Const1(int, const=1), 
        which is bigger than max_contains
        """
    

contains of ConTuple in the example specifies the input must equal to 1 after convert to integer, and the maximum number max_contains of matches is 3, so if no element in the data matches the type of Const1, or if the number of matched elements exceeds 3, an error will be thrown

Note

contains only validate the match of the elements, which does not affect the output data, if you want to convert the elements, you should declare a nested type or use __args__ attribute to specify the element type

  • unique_items: whether the element needs to be unique
    from utype import Rule, exc, types
    
    class UniqueList(types.Array):
        unique_items = True
    
    assert UniqueList[int]([1, '2', 3.5]) == [1, 2, 3]
    
    try:
        UniqueList[int]([1, '1', True])
    except exc.ConstraintError as e:
        print(e)
        """
        Constraint: <unique_items>: True violated: value is not unique
        """
    

In the example, we use UniqueList[int] to convert the input, so we will convert the element type first, and then check the constraint.

Note

If your source type is set, the data after conversion is de-duplicated already, so you don't need to specify unique_items in that case

Lax constraints

Lax constraints (aka loose constraints, transformational constraints) is a kind of constraints that is not strict, and can transform the input data to satisfying the constraint in its best effort

Declaring a lax constraint simply requires importing Lax from the utype and wrapping the constraint value in Lax, as shown in

from utype import Rule, Lax

class LaxLength(Rule):
    max_length = Lax(3)

print(LaxLength('ab'))
# > ab

print(LaxLength('abcd'))
# > abc

In the example, when the input 'abcd' does not meet the lax constraint max_length, it will be truncated directly according to max_length the length of the data and output 'abc', instead of throwing an error directly like the strict constraint.

Different constraints behave differently in the lax mode. The constraints supported by the lax mode and their respective behaviors are

  • max_length: defines a lax maximum length, truncating to the given maximum length if the value is longer than it.
  • length: If the data is greater than this length, it is truncated to length the corresponding length. If the data is less than this length, an error is thrown.
  • ge If the value is less than ge, output ge the value of directly, otherwise use the input value
  • le If the value is greater than le, the value of is output le directly, otherwise the input value is used
  • decimal_places: Instead of checking the decimal place, the decimal place is reserved directly to decimals_places, which is consistent with the effect of Python’s built-in method round().
  • max_digits If the number of digits exceeds the maximum number of digits, round off the decimal places from the smallest to the largest, and throw an error if it still cannot be satisfied.
  • multiple_of: If the data is not multiple_of an integer multiple of, the value of an integer multiple of the nearest input data that is smaller than the input data is taken.
  • const: direct;y output the constant value
  • enum: If the data is not within the range, the first value of the range will be output directly
  • unique_items: If the data is duplicated, the de-duplicated data will be returned

Note

decimal_places in the Lax mode is commonly used, so Field provided a round param as a shortcut, you could use Field(round=3) as a shortcut for Field(decimal_places=Lax(3))

Strict constraints are only used for validation, but lax constraints may convert the input data. Although information loss may occur during the conversion, lax constraints will also ensure that the conversion is Idempotent, that is, the value obtained from a data after multiple conversions is the same as that obtained from a single conversion

Note

As the lax constraints only been abled to compress data, not adding information, so it cannot applied to min_length, gt and lt