type_enforced
Type Enforced
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|
andfrom __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)
- For python 3.10:
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 setenabled=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]
orlist[int,float]
)- Note: Each parent level must be an iterable
- Specifically a variant of
list
,set
,tuple
ordict
- Specifically a variant of
- Note:
dict
keys are not validated, only values - Deeply nested types are supported too:
dict[dict[int]]
list[set[str]]
- Note: Each parent level must be an iterable
- 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
- Because this does not always meet the criteria for
- Essentially creates a union of:
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
- Essentially creates a union of:
- 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
- Standard typing functions:
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. ```
- This is a special type of validation that allows passed input to be validated.
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 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.
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 theDockerfile
and drop into a shell to run tests individually.
- Alternately, you can comment out the
- 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