type_enforced

Type Enforced

PyPI version License: MIT

A pure python (no special compiler required) type enforcer for type annotations. Enforce types in python functions and methods.

Setup

Make sure you have Python 3.10.x (or higher) installed on your system. You can download it here.

  • Unsupported python versions can be used, however newer features will not be available.
    • For 3.7: use type_enforced==0.0.16 (only very basic type checking is supported)
    • For 3.8: use type_enforced==0.0.16 (only very basic type checking is supported)
    • For 3.9: use type_enforced<=1.9.0 (staticmethod, union with | and from __future__ import annotations typechecking are not supported)
  • Other notes:
    • For python 3.10: from __future__ import annotations may cause errors (EG: when using staticmethods and classmethods)

Installation

pip install type_enforced

Basic Usage

import type_enforced

@type_enforced.Enforcer(enabled=True)
def my_fn(a: int , b: [int, str] =2, c: int =3) -> None:
    pass
  • Note: enabled=True by default if not specified. You can set enabled=False to disable type checking for a specific function, method, or class. This is useful for a production vs debugging environment or for undecorating a single method in a larger wrapped class.

Getting Started

type_enforcer contains a basic Enforcer wrapper that can be used to enforce many basic python typing hints. Technical Docs Here.

type_enforcer currently supports many single and multi level python types. This includes class instances and classes themselves. For example, you can force an input to be an int, a number [int, float], an instance of the self defined MyClass, or a even a vector with list[int]. Items like typing.List, typing.Dict, typing.Union and typing.Optional are supported.

You can pass union types to validate one of multiple types. For example, you could validate an input was an int or a float with [int, float], [int | float] or even typing.Union[int, float].

Nesting is allowed as long as the nested items are iterables (e.g. typing.List, dict, ...). For examle, you could validate that a list is a vector with list[int] or possibly typing.List[int].

Variables without an annotation for type are not enforced.

Supported Type Checking Features:

  • Function/Method Input Typing
  • Function/Method Return Typing
  • Dataclass Typing
  • All standard python types (str, list, int, dict, ...)
  • Union types
    • typing.Union
    • , separated list (e.g. [int, float])
    • | separated list (e.g. [int | float])
  • Nested types (e.g. dict[str] or list[int,float])
    • Note: Each parent level must be an iterable
      • Specifically a variant of list, set, tuple or dict
    • Note: dict keys are not validated, only values
    • Deeply nested types are supported too:
      • dict[dict[int]]
      • list[set[str]]
  • Many of the typing (package) functions and methods including:
    • Standard typing functions:
      • List, Set, Dict, Tuple
    • Union
    • Optional
    • Sized
      • Essentially creates a union of:
        • list, tuple, dict, set, str, bytes, bytearray, memoryview, range
      • Note: Can not have a nested type
        • Because this does not always meet the criteria for Nested types above
    • Literal
      • Only allow certain values to be passed. Operates slightly differently than other checks.
      • e.g. Literal['a', 'b'] will require any passed values that are equal (==) to 'a' or 'b'.
        • This compares the value of the passed input and not the type of the passed input.
      • Note: Multiple types can be passed in the same Literal.
      • Note: Literals are evaluated after type checking occurs.
    • Callable
      • Essentially creates a union of:
        • staticmethod, classmethod, types.FunctionType, types.BuiltinFunctionType, types.MethodType, types.BuiltinMethodType, types.GeneratorType
    • Note: Other functions might have support, but there are not currently tests to validate them
      • Feel free to create an issue (or better yet a PR) if you want to add tests/support
  • Constraint validation.
    • This is a special type of validation that allows passed input to be validated.
      • Standard and custom constraints are supported.
    • This is useful for validating that a passed input is within a certain range or meets a certain criteria.
    • Note: The constraint is checked after type checking occurs.
    • Note: See the example below or technical constraint and generic constraint docs for more information. ```

Interactive Example

>>> import type_enforced
>>> @type_enforced.Enforcer
... def my_fn(a: int , b: [int, str] =2, c: int =3) -> None:
...     pass
...
>>> my_fn(a=1, b=2, c=3)
>>> my_fn(a=1, b='2', c=3)
>>> my_fn(a='a', b=2, c=3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/conmak/development/personal/type_enforced/type_enforced/enforcer.py", line 85, in __call__
    self.__check_type__(assigned_vars.get(key), value, key)
  File "/home/conmak/development/personal/type_enforced/type_enforced/enforcer.py", line 107, in __check_type__
    self.__exception__(
  File "/home/conmak/development/personal/type_enforced/type_enforced/enforcer.py", line 34, in __exception__
    raise TypeError(f"({self.__fn__.__qualname__}): {message}")
TypeError: (my_fn): Type mismatch for typed variable `a`. Expected one of the following `[<class 'int'>]` but got `<class 'str'>` instead.

Nested Examples

import type_enforced
import typing

@type_enforced.Enforcer
def my_fn(
    a: dict[dict[int, float]], # Note: dict keys are not validated, only values
    b: list[typing.Set[str]] # Could also just use set
) -> None:
    return None

my_fn(a={'i':{'j':1}}, b=[{'x'}]) # Success

my_fn(a={'i':{'j':'k'}}, b=[{'x'}]) # Error:
# TypeError: (my_fn): Type mismatch for typed variable `a[i][j]`. Expected one of the following `[<class 'int'>]` but got `<class 'str'>` instead. 

Class and Method Use

Type enforcer can be applied to methods individually:

import type_enforced

class my_class:
    @type_enforced.Enforcer
    def my_fn(self, b:int):
        pass

You can also enforce all typing for all methods in a class by decorating the class itself.

import type_enforced

@type_enforced.Enforcer
class my_class:
    def my_fn(self, b:int):
        pass

    def my_other_fn(self, a: int, b: [int, str]):
      pass

You can also enforce types on staticmethods and classmethods if you are using python >= 3.10. If you are using a python version less than this, classmethods and staticmethods methods will not have their types enforced.

import type_enforced

@type_enforced.Enforcer
class my_class:
    @classmethod
    def my_fn(self, b:int):
        pass

    @staticmethod
    def my_other_fn(a: int, b: [int, str]):
      pass

Dataclasses are suported too.

import type_enforced
from dataclasses import dataclass

@type_enforced.Enforcer
@dataclass
class my_class:
    foo: int
    bar: str

You can skip enforcement if you add the argument enabled=False in the Enforcer call.

  • This is useful for a production vs debugging environment.
  • This is also useful for undecorating a single method in a larger wrapped class.
  • Note: You can set enabled=False for an entire class or simply disable a specific method in a larger wrapped class.
  • Note: Method level wrapper enabled values take precedence over class level wrappers.
import type_enforced
@type_enforced.Enforcer
class my_class:
    def my_fn(self, a: int) -> None:
        pass

    @type_enforced.Enforcer(enabled=False)
    def my_other_fn(self, a: int) -> None:
        pass

Validate with Constraints

Type enforcer can enforce constraints for passed variables. These constraints are vaildated after any type checks are made.

To enforce basic input values are integers greater than or equal to zero, you can use the Constraint class like so:

import type_enforced
from type_enforced.utils import Constraint

@type_enforced.Enforcer()
def positive_int_test(value: [int, Constraint(ge=0)]) -> bool:
    return True

positive_int_test(1) # Passes
positive_int_test(-1) # Fails
positive_int_test(1.0) # Fails

To enforce a GenericConstraint:

import type_enforced
from type_enforced.utils import GenericConstraint

CustomConstraint = GenericConstraint(
    {
        'in_rgb': lambda x: x in ['red', 'green', 'blue'],
    }
)

@type_enforced.Enforcer()
def rgb_test(value: [str, CustomConstraint]) -> bool:
    return True

rgb_test('red') # Passes
rgb_test('yellow') # Fails

Validate class instances and classes

Type enforcer can enforce class instances and classes. There are a few caveats between the two.

To enforce a class instance, simply pass the class itself as a type hint:

import type_enforced

class Foo():
    def __init__(self) -> None:
        pass

@type_enforced.Enforcer
class my_class():
    def __init__(self, object: Foo) -> None:
        self.object = object

x=my_class(Foo()) # Works great!
y=my_class(Foo) # Fails!

Notice how an initialized class instance Foo() must be passed for the enforcer to not raise an exception.

To enforce an uninitialized class object use typing.Type[classHere] on the class to enforce inputs to be an uninitialized class:

import type_enforced
import typing

class Foo():
    def __init__(self) -> None:
        pass

@type_enforced.Enforcer
class my_class():
    def __init__(self, object_class: typing.Type[Foo]) -> None:
        self.object = object_class()

y=my_class(Foo) # Works great!
x=my_class(Foo()) # Fails

Validate classes with inheritance

import type_enforced
from type_enforced.utils import WithSubclasses

class Foo:
    pass

class Bar(Foo):
    pass

class Baz:
    pass

@type_enforced.Enforcer
def my_fn(custom_class: WithSubclasses(Foo)):
    pass

print(WithSubclasses.get_subclasses(Foo)) # Prints: [<class '__main__.Foo'>, <class '__main__.Bar'>]
my_fn(Foo()) # Passes as expected
my_fn(Bar()) # Passes as expected
my_fn(Baz()) # Raises TypeError as expected

Development

Running Tests

Debug and Test using Docker

  • Creates a docker container and runs all tests in the test folder.
    • Alternately, you can comment out the ENTRYPOINT line in the Dockerfile and drop into a shell to run tests individually.
  • Runs the tests on the python version specified in the Dockerfile.
    • Modify this as needed to ensure function across all supported python versions (3.9+)
./run_test.sh
  1"""
  2# Type Enforced
  3[![PyPI version](https://badge.fury.io/py/type_enforced.svg)](https://badge.fury.io/py/type_enforced)
  4[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
  5
  6A pure python (no special compiler required) type enforcer for type annotations. Enforce types in python functions and methods.
  7
  8# Setup
  9
 10Make sure you have Python 3.10.x (or higher) installed on your system. You can download it [here](https://www.python.org/downloads/).
 11
 12- Unsupported python versions can be used, however newer features will not be available.
 13    - For 3.7: use type_enforced==0.0.16 (only very basic type checking is supported)
 14    - For 3.8: use type_enforced==0.0.16 (only very basic type checking is supported)
 15    - For 3.9: use type_enforced<=1.9.0 (`staticmethod`, union with `|` and `from __future__ import annotations` typechecking are not supported)
 16- Other notes:
 17    - For python 3.10: `from __future__ import annotations` may cause errors (EG: when using staticmethods and classmethods)
 18
 19### Installation
 20
 21```
 22pip install type_enforced
 23```
 24
 25## Basic Usage
 26```py
 27import type_enforced
 28
 29@type_enforced.Enforcer(enabled=True)
 30def my_fn(a: int , b: [int, str] =2, c: int =3) -> None:
 31    pass
 32```
 33- Note: `enabled=True` by default if not specified. You can set `enabled=False` to disable type checking for a specific function, method, or class. This is useful for a production vs debugging environment or for undecorating a single method in a larger wrapped class.
 34
 35# Getting Started
 36
 37`type_enforcer` contains a basic `Enforcer` wrapper that can be used to enforce many basic python typing hints. [Technical Docs Here](https://connor-makowski.github.io/type_enforced/type_enforced/enforcer.html).
 38
 39`type_enforcer` currently supports many single and multi level python types. This includes class instances and classes themselves. For example, you can force an input to be an `int`, a number `[int, float]`, an instance of the self defined `MyClass`, or a even a vector with `list[int]`. Items like `typing.List`, `typing.Dict`, `typing.Union` and `typing.Optional` are supported.
 40
 41You can pass union types to validate one of multiple types. For example, you could validate an input was an int or a float with `[int, float]`, `[int | float]` or even `typing.Union[int, float]`.
 42
 43Nesting is allowed as long as the nested items are iterables (e.g. `typing.List`, `dict`, ...). For examle, you could validate that a list is a vector with `list[int]` or possibly `typing.List[int]`.
 44
 45Variables without an annotation for type are not enforced.
 46
 47## Supported Type Checking Features:
 48
 49- Function/Method Input Typing
 50- Function/Method Return Typing
 51- Dataclass Typing
 52- All standard python types (`str`, `list`, `int`, `dict`, ...)
 53- Union types
 54    - typing.Union
 55    - `,` separated list (e.g. `[int, float]`)
 56    - `|` separated list (e.g. `[int | float]`)
 57- Nested types (e.g. `dict[str]` or `list[int,float]`)
 58    - Note: Each parent level must be an iterable
 59        - Specifically a variant of `list`, `set`, `tuple` or `dict`
 60    - Note: `dict` keys are not validated, only values
 61    - Deeply nested types are supported too:
 62        - `dict[dict[int]]`
 63        - `list[set[str]]`
 64- Many of the `typing` (package) functions and methods including:
 65    - Standard typing functions:
 66        - `List`, `Set`, `Dict`, `Tuple`
 67    - `Union`
 68    - `Optional` 
 69    - `Sized`
 70        - Essentially creates a union of: 
 71            - `list`, `tuple`, `dict`, `set`, `str`, `bytes`, `bytearray`, `memoryview`, `range`
 72        - Note: Can not have a nested type
 73            - Because this does not always meet the criteria for `Nested types` above
 74    - `Literal`
 75        - Only allow certain values to be passed. Operates slightly differently than other checks.
 76        - e.g. `Literal['a', 'b']` will require any passed values that are equal (`==`) to `'a'` or `'b'`.
 77            - This compares the value of the passed input and not the type of the passed input.
 78        - Note: Multiple types can be passed in the same `Literal`.
 79        - Note: Literals are evaluated after type checking occurs.
 80    - `Callable`
 81        - Essentially creates a union of:
 82            - `staticmethod`, `classmethod`, `types.FunctionType`, `types.BuiltinFunctionType`, `types.MethodType`, `types.BuiltinMethodType`, `types.GeneratorType`
 83    - Note: Other functions might have support, but there are not currently tests to validate them
 84        - Feel free to create an issue (or better yet a PR) if you want to add tests/support
 85- `Constraint` validation.
 86    - This is a special type of validation that allows passed input to be validated.
 87        - Standard and custom constraints are supported.
 88    - This is useful for validating that a passed input is within a certain range or meets a certain criteria.
 89    - Note: The constraint is checked after type checking occurs.
 90    - Note: See the example below or technical [constraint](https://connor-makowski.github.io/type_enforced/type_enforced/utils.html#Constraint) and [generic constraint](https://connor-makowski.github.io/type_enforced/type_enforced/utils.html#GenericConstraint) docs for more information.
 91    ```
 92
 93## Interactive Example
 94
 95```py
 96>>> import type_enforced
 97>>> @type_enforced.Enforcer
 98... def my_fn(a: int , b: [int, str] =2, c: int =3) -> None:
 99...     pass
100...
101>>> my_fn(a=1, b=2, c=3)
102>>> my_fn(a=1, b='2', c=3)
103>>> my_fn(a='a', b=2, c=3)
104Traceback (most recent call last):
105  File "<stdin>", line 1, in <module>
106  File "/home/conmak/development/personal/type_enforced/type_enforced/enforcer.py", line 85, in __call__
107    self.__check_type__(assigned_vars.get(key), value, key)
108  File "/home/conmak/development/personal/type_enforced/type_enforced/enforcer.py", line 107, in __check_type__
109    self.__exception__(
110  File "/home/conmak/development/personal/type_enforced/type_enforced/enforcer.py", line 34, in __exception__
111    raise TypeError(f"({self.__fn__.__qualname__}): {message}")
112TypeError: (my_fn): Type mismatch for typed variable `a`. Expected one of the following `[<class 'int'>]` but got `<class 'str'>` instead.
113```
114
115## Nested Examples
116```py
117import type_enforced
118import typing
119
120@type_enforced.Enforcer
121def my_fn(
122    a: dict[dict[int, float]], # Note: dict keys are not validated, only values
123    b: list[typing.Set[str]] # Could also just use set
124) -> None:
125    return None
126
127my_fn(a={'i':{'j':1}}, b=[{'x'}]) # Success
128
129my_fn(a={'i':{'j':'k'}}, b=[{'x'}]) # Error:
130# TypeError: (my_fn): Type mismatch for typed variable `a[i][j]`. Expected one of the following `[<class 'int'>]` but got `<class 'str'>` instead. 
131```
132
133## Class and Method Use
134
135Type enforcer can be applied to methods individually:
136
137```py
138import type_enforced
139
140class my_class:
141    @type_enforced.Enforcer
142    def my_fn(self, b:int):
143        pass
144```
145
146You can also enforce all typing for all methods in a class by decorating the class itself.
147
148```py
149import type_enforced
150
151@type_enforced.Enforcer
152class my_class:
153    def my_fn(self, b:int):
154        pass
155
156    def my_other_fn(self, a: int, b: [int, str]):
157      pass
158```
159
160You can also enforce types on `staticmethod`s and `classmethod`s if you are using `python >= 3.10`. If you are using a python version less than this, `classmethod`s and `staticmethod`s methods will not have their types enforced.
161
162```py
163import type_enforced
164
165@type_enforced.Enforcer
166class my_class:
167    @classmethod
168    def my_fn(self, b:int):
169        pass
170
171    @staticmethod
172    def my_other_fn(a: int, b: [int, str]):
173      pass
174```
175
176Dataclasses are suported too.
177
178```py
179import type_enforced
180from dataclasses import dataclass
181
182@type_enforced.Enforcer
183@dataclass
184class my_class:
185    foo: int
186    bar: str
187```
188
189You can skip enforcement if you add the argument `enabled=False` in the `Enforcer` call.
190- This is useful for a production vs debugging environment.
191- This is also useful for undecorating a single method in a larger wrapped class.
192- Note: You can set `enabled=False` for an entire class or simply disable a specific method in a larger wrapped class.
193- Note: Method level wrapper `enabled` values take precedence over class level wrappers.
194```py
195import type_enforced
196@type_enforced.Enforcer
197class my_class:
198    def my_fn(self, a: int) -> None:
199        pass
200        
201    @type_enforced.Enforcer(enabled=False)
202    def my_other_fn(self, a: int) -> None:
203        pass
204```
205
206## Validate with Constraints
207Type enforcer can enforce constraints for passed variables. These constraints are vaildated after any type checks are made.
208
209To enforce basic input values are integers greater than or equal to zero, you can use the [Constraint](https://connor-makowski.github.io/type_enforced/type_enforced/utils.html#Constraint) class like so:
210```py
211import type_enforced
212from type_enforced.utils import Constraint
213
214@type_enforced.Enforcer()
215def positive_int_test(value: [int, Constraint(ge=0)]) -> bool:
216    return True
217
218positive_int_test(1) # Passes
219positive_int_test(-1) # Fails
220positive_int_test(1.0) # Fails
221```
222
223To enforce a [GenericConstraint](https://connor-makowski.github.io/type_enforced/type_enforced/utils.html#GenericConstraint):
224```py
225import type_enforced
226from type_enforced.utils import GenericConstraint
227
228CustomConstraint = GenericConstraint(
229    {
230        'in_rgb': lambda x: x in ['red', 'green', 'blue'],
231    }
232)
233
234@type_enforced.Enforcer()
235def rgb_test(value: [str, CustomConstraint]) -> bool:
236    return True
237
238rgb_test('red') # Passes
239rgb_test('yellow') # Fails
240```
241
242
243
244## Validate class instances and classes
245
246Type enforcer can enforce class instances and classes. There are a few caveats between the two.
247
248To enforce a class instance, simply pass the class itself as a type hint:
249```py
250import type_enforced
251
252class Foo():
253    def __init__(self) -> None:
254        pass
255
256@type_enforced.Enforcer
257class my_class():
258    def __init__(self, object: Foo) -> None:
259        self.object = object
260
261x=my_class(Foo()) # Works great!
262y=my_class(Foo) # Fails!
263```
264
265Notice how an initialized class instance `Foo()` must be passed for the enforcer to not raise an exception.
266
267To enforce an uninitialized class object use `typing.Type[classHere]` on the class to enforce inputs to be an uninitialized class:
268```py
269import type_enforced
270import typing
271
272class Foo():
273    def __init__(self) -> None:
274        pass
275
276@type_enforced.Enforcer
277class my_class():
278    def __init__(self, object_class: typing.Type[Foo]) -> None:
279        self.object = object_class()
280
281y=my_class(Foo) # Works great!
282x=my_class(Foo()) # Fails
283```
284
285## Validate classes with inheritance
286
287```py
288import type_enforced
289from type_enforced.utils import WithSubclasses
290
291class Foo:
292    pass
293
294class Bar(Foo):
295    pass
296
297class Baz:
298    pass
299
300@type_enforced.Enforcer
301def my_fn(custom_class: WithSubclasses(Foo)):
302    pass
303
304print(WithSubclasses.get_subclasses(Foo)) # Prints: [<class '__main__.Foo'>, <class '__main__.Bar'>]
305my_fn(Foo()) # Passes as expected
306my_fn(Bar()) # Passes as expected
307my_fn(Baz()) # Raises TypeError as expected
308```
309
310# Development
311## Running Tests
312### Debug and Test using Docker
313
314- Creates a docker container and runs all tests in the `test` folder.
315  - Alternately, you can comment out the `ENTRYPOINT` line in the `Dockerfile` and drop into a shell to run tests individually.
316- Runs the tests on the python version specified in the `Dockerfile`.
317    - Modify this as needed to ensure function across all supported python versions (3.9+)
318
319```bash
320./run_test.sh
321```
322"""
323from .enforcer import Enforcer, FunctionMethodEnforcer