type_enforced
Type Enforced
A pure python runtime type enforcer for type annotations. Enforce types in python functions and methods.
Setup
Make sure you have Python 3.11.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 annotationstypechecking are not supported) - For 3.10: use type_enforced<=1.10.2 (
from __future__ import annotationsmay cause errors (EG: when using staticmethods and classmethods))
Installation
pip install type_enforced
Basic Usage
import type_enforced
@type_enforced.Enforcer(enabled=True, strict=True, clean_traceback=True)
def my_fn(a: int , b: int | str =2, c: int =3) -> None:
pass
- Note:
enabled=Trueby default if not specified. You can setenabled=Falseto 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. - Note:
strict=Trueby default if not specified. You can setstrict=Falseto disable exceptions being raised when type checking fails. Instead, a warning will be printed to the console. - Note:
clean_traceback=Trueby default if not specified. This modifies the excepthook temporarily when a type exception is raised such that only the relevant stack (stack items not from type_enforced) is shown.
Getting Started
type_enforcer contains a basic Enforcer wrapper that can be used to enforce many basic python typing hints. Technical Docs Here.
Enforcer can be used as a decorator for functions, methods, and classes. It will enforce the type hints on the function or method inputs and outputs. It takes in the following optional arguments:
enabled(True): A boolean to enable or disable type checking. IfTrue, type checking will be enforced. IfFalse, type checking will be disabled.strict(True): A boolean to enable or disable type mismatch exceptions. IfTrueexceptions will be raised when type checking fails. IfFalse, exceptions will not be raised but instead a warning will be printed to the console.
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 or typing.Union[int, float].
Nesting is allowed as long as the nested items are iterables (e.g. typing.List, dict, ...). For example, 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.
Why use Type Enforced?
type_enforcedis a pure python type enforcer that does not require any special compiler or preprocessor to work.type_enforceduses the standard python typing hints and enforces them at runtime.- This means that you can use it in any python environment (3.11+) without any special setup.
type_enforcedis designed to be lightweight and easy to use, making it a great choice for both small and large projects.type_enforcedsupports complex (nested) typing hints, union types, and many of the standard python typing functions.type_enforcedis designed to be fast and efficient, with minimal overhead.type_enforcedoffers the fastest performance for enforcing large objects of complex types- Note: See the benchmarks for more information on the performance of each type checker.
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 items (e.g.int | float)
- Nested types (e.g.
dict[str, int]orlist[int|float])- Note: Each parent level must be an iterable
- Specifically a variant of
list,set,tupleordict
- Specifically a variant of
- Note:
dictrequires two types to be specified (unions count as a single type)- The first type is the key type and the second type is the value type
- e.g.
dict[str, int|float]ordict[int, float]
- Note:
listandsetrequire a single type to be specified (unions count as a single type)- e.g.
list[int],set[str],list[float|str]
- e.g.
- Note:
tupleAllows forNtypes to be specified- Each item refers to the positional type of each item in the tuple
- Support for ellipsis (
...) is supported if you only specify two types and the second is the ellipsis type- e.g.
tuple[int, ...]ortuple[int|str, ...]
- e.g.
- Note: Unions between two tuples are not supported
- e.g.
tuple[int, str] | tuple[str, int]will not work
- e.g.
- 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:
ListSetDictTuple
UnionOptionalAnySized- 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 typesabove
- 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
Literalas acceptable values.- e.g. Literal['a', 'b', 1, 2] will require any passed values that are equal (
==) to'a','b',1or2.
- e.g. Literal['a', 'b', 1, 2] will require any passed values that are equal (
- Note: If type is a
str | Literal['a', 'b']- The check will validate that the type is a string or the value is equal to
'a'or'b'. - This means that an input of
'c'will pass the check since it matches the string type, but an input of1will fail.
- The check will validate that the type is a string or the value is equal to
- Note: If type is a
int | Literal['a', 'b']- The check will validate that the type is an int or the value is equal to
'a'or'b'. - This means that an input of
'c'will fail the check, but an input of1will pass.
- The check will validate that the type is an int or the value is equal to
- Note: Literals stack when used with unions.
- e.g.
Literal['a', 'b'] | Literal[1, 2]will require any passed values that are equal (==) to'a','b',1or2.
- e.g.
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:
Constraintvalidation.- This is a special type of validation that allows passed input to be validated.
- Standard and custom constraints are supported.
- Constraints are not actually types. They are type_enforced specific validators and may cause issues with other runtime or static type checkers like
mypy. - This is useful for validating that a passed input is within a certain range or meets a certain criteria.
- Note: Constraints stack when used with unions.
- e.g.
int | Constraint(ge=0) | Constraint(le=5)will require any passed values to be integers that are greater than or equal to0and less than or equal to5.
- e.g.
- Note: The constraint is checked after type checking occurs and operates independently of the type checking.
- This operates differently than other checks (like
Literal) and is evaluated post type checking. - For example, if you have an annotation of
str | Constraint(ge=0), this will always raise an exception since if you pass a string, it will raise on the constraint check and if you pass an integer, it will raise on the type check.
- This operates differently than other checks (like
- 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>
TypeError: TypeEnforced Exception (my_fn): Type mismatch for typed variable `a`. Expected one of the following `[<class 'int'>]` but got `<class 'str'>` with value `a` instead.
Nested Examples
import type_enforced
import typing
@type_enforced.Enforcer
def my_fn(
a: dict[str,dict[str, int|float]], # Note: For dicts, the key is the first type and the value is the second type
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: TypeEnforced Exception (my_fn): Type mismatch for typed variable `a['i']['j']`. Expected one of the following `[<class 'int'>, <class 'float'>]` but got `<class 'str'>` with value `k` 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=Falsefor an entire class or simply disable a specific method in a larger wrapped class. - Note: Method level wrapper
enabledvalues 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 validated 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
By default, type_enforced will check for subclasses of a class when validating types. This means that if you pass a subclass of the expected class, it will pass the type check.
Note: Uninitialized class objects that are passed are not checked for subclasses.
import type_enforced
class Foo:
pass
class Bar(Foo):
pass
class Baz:
pass
@type_enforced.Enforcer
def my_fn(custom_class: Foo):
pass
my_fn(Foo()) # Passes as expected
my_fn(Bar()) # Passes as expected
my_fn(Baz()) # Raises TypeError as expected
What changed in 2.0.0?
The main changes in version 2.0.0 revolve around migrating towards the standard python typing hint process and away from the original type_enfoced type hints (as type enforced was originally created before the | operator was added to python).
- Support for python3.10 has been dropped.
- List based union types are no longer supported.
- For example
[int, float]is no longer a supported type hint. - Use
int|floatortyping.Union[int, float]instead.
- For example
- Dict types now require two types to be specified.
- The first type is the key type and the second type is the value type.
- For example,
dict[str, int|float]ordict[int, float]are valid types.
- Tuple types now allow for
Ntypes to be specified.- Each item refers to the positional type of each item in the tuple.
- Support for ellipsis (
...) is supported if you only specify two types and the second is the ellipsis type.- For example,
tuple[int, ...]ortuple[int|str, ...]are valid types.
- For example,
- Note: Unions between two tuples are not supported.
- For example,
tuple[int, str] | tuple[str, int]will not work.
- For example,
- Constraints and Literals can now be stacked with unions.
- For example,
int | Constraint(ge=0) | Constraint(le=5)will require any passed values to be integers that are greater than or equal to0and less than or equal to5. - For example,
Literal['a', 'b'] | Literal[1, 2]will require any passed values that are equal (==) to'a','b',1or2.
- For example,
- Literals now evaluate during the same time as type checking and operate as OR checks.
- For example,
int | Literal['a', 'b']will validate that the type is an int or the value is equal to'a'or'b'.
- For example,
- Constraints are still are evaluated after type checking and operate independently of the type checking.
Support
Bug Reports and Feature Requests
If you find a bug or are looking for a new feature, please open an issue on GitHub.
Need Help?
If you need help, please open an issue on GitHub.
Contributing
Contributions are welcome! Please open an issue or submit a pull request.
Development
To avoid extra development overhead, we expect all developers to use a unix based environment (Linux or Mac). If you use Windows, please use WSL2.
For development, we test using Docker so we can lock system deps and swap out python versions easily. However, you can also use a virtual environment if you prefer. We provide a test script and a prettify script to help with development.
Making Changes
1) Fork the repo and clone it locally. 2) Make your modifications. 3) Use Docker or a virtual environment to run tests and make sure they pass. 4) Prettify your code. 5) DO NOT GENERATE DOCS. - We will generate the docs and update the version number when we are ready to release a new version. 6) Only commit relevant changes and add clear commit messages. - Atomic commits are preferred. 7) Submit a pull request.
Docker
Make sure Docker is installed and running.
- Create a docker container and drop into a shell
./run.sh
- Run all tests (see ./utils/test.sh)
./run.sh test
Prettify the code (see ./utils/prettify.sh)
./run.sh prettify
Note: You can and should modify the
Dockerfileto test different python versions.
Virtual Environment
- Create a virtual environment
python3.XX -m venv venv- Replace
3.XXwith your python version (3.11 or higher)
- Replace
- Activate the virtual environment
source venv/bin/activate
- Install the development requirements
pip install -r requirements/dev.txt
- Run Tests
./utils/test.sh
- Prettify Code
./utils/prettify.sh
1""" 2# Type Enforced 3[](https://badge.fury.io/py/type_enforced) 4[](https://opensource.org/licenses/MIT) 5[](https://pypi.org/project/type_enforced/) 6[](https://doi.org/10.21105/joss.08832) 7<!-- [](https://pypi.org/project/type_enforced/) --> 8 9A pure python runtime type enforcer for type annotations. Enforce types in python functions and methods. 10 11# Setup 12 13Make sure you have Python 3.11.x (or higher) installed on your system. You can download it [here](https://www.python.org/downloads/). 14 15- Unsupported python versions can be used, however newer features will not be available. 16 - For 3.7: use type_enforced==0.0.16 (only very basic type checking is supported) 17 - For 3.8: use type_enforced==0.0.16 (only very basic type checking is supported) 18 - For 3.9: use type_enforced<=1.9.0 (`staticmethod`, union with `|` and `from __future__ import annotations` typechecking are not supported) 19 - For 3.10: use type_enforced<=1.10.2 (`from __future__ import annotations` may cause errors (EG: when using staticmethods and classmethods)) 20 21## Installation 22 23``` 24pip install type_enforced 25``` 26 27## Basic Usage 28```py 29import type_enforced 30 31@type_enforced.Enforcer(enabled=True, strict=True, clean_traceback=True) 32def my_fn(a: int , b: int | str =2, c: int =3) -> None: 33 pass 34``` 35- 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. 36- Note: `strict=True` by default if not specified. You can set `strict=False` to disable exceptions being raised when type checking fails. Instead, a warning will be printed to the console. 37- Note: `clean_traceback=True` by default if not specified. This modifies the excepthook temporarily when a type exception is raised such that only the relevant stack (stack items not from type_enforced) is shown. 38 39## Getting Started 40 41`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). 42 43`Enforcer` can be used as a decorator for functions, methods, and classes. It will enforce the type hints on the function or method inputs and outputs. It takes in the following optional arguments: 44 45- `enabled` (True): A boolean to enable or disable type checking. If `True`, type checking will be enforced. If `False`, type checking will be disabled. 46- `strict` (True): A boolean to enable or disable type mismatch exceptions. If `True` exceptions will be raised when type checking fails. If `False`, exceptions will not be raised but instead a warning will be printed to the console. 47 48`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. 49 50You 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` or `typing.Union[int, float]`. 51 52Nesting is allowed as long as the nested items are iterables (e.g. `typing.List`, `dict`, ...). For example, you could validate that a list is a vector with `list[int]` or possibly `typing.List[int]`. 53 54Variables without an annotation for type are not enforced. 55 56## Why use Type Enforced? 57 58- `type_enforced` is a pure python type enforcer that does not require any special compiler or preprocessor to work. 59- `type_enforced` uses the standard python typing hints and enforces them at runtime. 60 - This means that you can use it in any python environment (3.11+) without any special setup. 61- `type_enforced` is designed to be lightweight and easy to use, making it a great choice for both small and large projects. 62- `type_enforced` supports complex (nested) typing hints, union types, and many of the standard python typing functions. 63- `type_enforced` is designed to be fast and efficient, with minimal overhead. 64- `type_enforced` offers the fastest performance for enforcing large objects of complex types 65 - Note: See the [benchmarks](https://github.com/connor-makowski/type_enforced/blob/main/benchmark.md) for more information on the performance of each type checker. 66 67## Supported Type Checking Features: 68 69- Function/Method Input Typing 70- Function/Method Return Typing 71- Dataclass Typing 72- All standard python types (`str`, `list`, `int`, `dict`, ...) 73- Union types 74 - typing.Union 75 - `|` separated items (e.g. `int | float`) 76- Nested types (e.g. `dict[str, int]` or `list[int|float]`) 77 - Note: Each parent level must be an iterable 78 - Specifically a variant of `list`, `set`, `tuple` or `dict` 79 - Note: `dict` requires two types to be specified (unions count as a single type) 80 - The first type is the key type and the second type is the value type 81 - e.g. `dict[str, int|float]` or `dict[int, float]` 82 - Note: `list` and `set` require a single type to be specified (unions count as a single type) 83 - e.g. `list[int]`, `set[str]`, `list[float|str]` 84 - Note: `tuple` Allows for `N` types to be specified 85 - Each item refers to the positional type of each item in the tuple 86 - Support for ellipsis (`...`) is supported if you only specify two types and the second is the ellipsis type 87 - e.g. `tuple[int, ...]` or `tuple[int|str, ...]` 88 - Note: Unions between two tuples are not supported 89 - e.g. `tuple[int, str] | tuple[str, int]` will not work 90 - Deeply nested types are supported too: 91 - `dict[dict[int]]` 92 - `list[set[str]]` 93- Many of the `typing` (package) functions and methods including: 94 - Standard typing functions: 95 - `List` 96 - `Set` 97 - `Dict` 98 - `Tuple` 99 - `Union` 100 - `Optional` 101 - `Any` 102 - `Sized` 103 - Essentially creates a union of: 104 - `list`, `tuple`, `dict`, `set`, `str`, `bytes`, `bytearray`, `memoryview`, `range` 105 - Note: Can not have a nested type 106 - Because this does not always meet the criteria for `Nested types` above 107 - `Literal` 108 - Only allow certain values to be passed. Operates slightly differently than other checks. 109 - e.g. `Literal['a', 'b']` will require any passed values that are equal (`==`) to `'a'` or `'b'`. 110 - This compares the value of the passed input and not the type of the passed input. 111 - Note: Multiple types can be passed in the same `Literal` as acceptable values. 112 - e.g. Literal['a', 'b', 1, 2] will require any passed values that are equal (`==`) to `'a'`, `'b'`, `1` or `2`. 113 - Note: If type is a `str | Literal['a', 'b']` 114 - The check will validate that the type is a string or the value is equal to `'a'` or `'b'`. 115 - This means that an input of `'c'` will pass the check since it matches the string type, but an input of `1` will fail. 116 - Note: If type is a `int | Literal['a', 'b']` 117 - The check will validate that the type is an int or the value is equal to `'a'` or `'b'`. 118 - This means that an input of `'c'` will fail the check, but an input of `1` will pass. 119 - Note: Literals stack when used with unions. 120 - e.g. `Literal['a', 'b'] | Literal[1, 2]` will require any passed values that are equal (`==`) to `'a'`, `'b'`, `1` or `2`. 121 - `Callable` 122 - Essentially creates a union of: 123 - `staticmethod`, `classmethod`, `types.FunctionType`, `types.BuiltinFunctionType`, `types.MethodType`, `types.BuiltinMethodType`, `types.GeneratorType` 124 - Note: Other functions might have support, but there are not currently tests to validate them 125 - Feel free to create an issue (or better yet a PR) if you want to add tests/support 126- `Constraint` validation. 127 - This is a special type of validation that allows passed input to be validated. 128 - Standard and custom constraints are supported. 129 - Constraints are not actually types. They are type_enforced specific validators and may cause issues with other runtime or static type checkers like `mypy`. 130 - This is useful for validating that a passed input is within a certain range or meets a certain criteria. 131 - Note: Constraints stack when used with unions. 132 - e.g. `int | Constraint(ge=0) | Constraint(le=5)` will require any passed values to be integers that are greater than or equal to `0` and less than or equal to `5`. 133 - Note: The constraint is checked after type checking occurs and operates independently of the type checking. 134 - This operates differently than other checks (like `Literal`) and is evaluated post type checking. 135 - For example, if you have an annotation of `str | Constraint(ge=0)`, this will always raise an exception since if you pass a string, it will raise on the constraint check and if you pass an integer, it will raise on the type check. 136 - 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. 137 138## Interactive Example 139 140```py 141>>> import type_enforced 142>>> @type_enforced.Enforcer 143... def my_fn(a: int , b: int|str =2, c: int =3) -> None: 144... pass 145... 146>>> my_fn(a=1, b=2, c=3) 147>>> my_fn(a=1, b='2', c=3) 148>>> my_fn(a='a', b=2, c=3) 149Traceback (most recent call last): 150 File "<stdin>", line 1, in <module> 151TypeError: TypeEnforced Exception (my_fn): Type mismatch for typed variable `a`. Expected one of the following `[<class 'int'>]` but got `<class 'str'>` with value `a` instead. 152 153``` 154 155## Nested Examples 156```py 157import type_enforced 158import typing 159 160@type_enforced.Enforcer 161def my_fn( 162 a: dict[str,dict[str, int|float]], # Note: For dicts, the key is the first type and the value is the second type 163 b: list[typing.Set[str]] # Could also just use set 164) -> None: 165 return None 166 167my_fn(a={'i':{'j':1}}, b=[{'x'}]) # Success 168 169my_fn(a={'i':{'j':'k'}}, b=[{'x'}]) # Error => 170# TypeError: TypeEnforced Exception (my_fn): Type mismatch for typed variable `a['i']['j']`. Expected one of the following `[<class 'int'>, <class 'float'>]` but got `<class 'str'>` with value `k` instead. 171``` 172 173## Class and Method Use 174 175Type enforcer can be applied to methods individually: 176 177```py 178import type_enforced 179 180class my_class: 181 @type_enforced.Enforcer 182 def my_fn(self, b:int): 183 pass 184``` 185 186You can also enforce all typing for all methods in a class by decorating the class itself. 187 188```py 189import type_enforced 190 191@type_enforced.Enforcer 192class my_class: 193 def my_fn(self, b:int): 194 pass 195 196 def my_other_fn(self, a: int, b: int | str): 197 pass 198``` 199 200You 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. 201 202```py 203import type_enforced 204 205@type_enforced.Enforcer 206class my_class: 207 @classmethod 208 def my_fn(self, b:int): 209 pass 210 211 @staticmethod 212 def my_other_fn(a: int, b: int | str): 213 pass 214``` 215 216Dataclasses are suported too. 217 218```py 219import type_enforced 220from dataclasses import dataclass 221 222@type_enforced.Enforcer 223@dataclass 224class my_class: 225 foo: int 226 bar: str 227``` 228 229You can skip enforcement if you add the argument `enabled=False` in the `Enforcer` call. 230- This is useful for a production vs debugging environment. 231- This is also useful for undecorating a single method in a larger wrapped class. 232- Note: You can set `enabled=False` for an entire class or simply disable a specific method in a larger wrapped class. 233- Note: Method level wrapper `enabled` values take precedence over class level wrappers. 234```py 235import type_enforced 236@type_enforced.Enforcer 237class my_class: 238 def my_fn(self, a: int) -> None: 239 pass 240 241 @type_enforced.Enforcer(enabled=False) 242 def my_other_fn(self, a: int) -> None: 243 pass 244``` 245 246## Validate with Constraints 247Type enforcer can enforce constraints for passed variables. These constraints are validated after any type checks are made. 248 249To 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: 250```py 251import type_enforced 252from type_enforced.utils import Constraint 253 254@type_enforced.Enforcer() 255def positive_int_test(value: int |Constraint(ge=0)) -> bool: 256 return True 257 258positive_int_test(1) # Passes 259positive_int_test(-1) # Fails 260positive_int_test(1.0) # Fails 261``` 262 263To enforce a [GenericConstraint](https://connor-makowski.github.io/type_enforced/type_enforced/utils.html#GenericConstraint): 264```py 265import type_enforced 266from type_enforced.utils import GenericConstraint 267 268CustomConstraint = GenericConstraint( 269 { 270 'in_rgb': lambda x: x in ['red', 'green', 'blue'], 271 } 272) 273 274@type_enforced.Enforcer() 275def rgb_test(value: str | CustomConstraint) -> bool: 276 return True 277 278rgb_test('red') # Passes 279rgb_test('yellow') # Fails 280``` 281 282 283 284## Validate class instances and classes 285 286Type enforcer can enforce class instances and classes. There are a few caveats between the two. 287 288To enforce a class instance, simply pass the class itself as a type hint: 289```py 290import type_enforced 291 292class Foo(): 293 def __init__(self) -> None: 294 pass 295 296@type_enforced.Enforcer 297class my_class(): 298 def __init__(self, object: Foo) -> None: 299 self.object = object 300 301x=my_class(Foo()) # Works great! 302y=my_class(Foo) # Fails! 303``` 304 305Notice how an initialized class instance `Foo()` must be passed for the enforcer to not raise an exception. 306 307To enforce an uninitialized class object use `typing.Type[classHere]` on the class to enforce inputs to be an uninitialized class: 308```py 309import type_enforced 310import typing 311 312class Foo(): 313 def __init__(self) -> None: 314 pass 315 316@type_enforced.Enforcer 317class my_class(): 318 def __init__(self, object_class: typing.Type[Foo]) -> None: 319 self.object = object_class() 320 321y=my_class(Foo) # Works great! 322x=my_class(Foo()) # Fails 323``` 324 325By default, type_enforced will check for subclasses of a class when validating types. This means that if you pass a subclass of the expected class, it will pass the type check. 326 327Note: Uninitialized class objects that are passed are not checked for subclasses. 328 329```py 330import type_enforced 331 332class Foo: 333 pass 334 335class Bar(Foo): 336 pass 337 338class Baz: 339 pass 340 341@type_enforced.Enforcer 342def my_fn(custom_class: Foo): 343 pass 344 345my_fn(Foo()) # Passes as expected 346my_fn(Bar()) # Passes as expected 347my_fn(Baz()) # Raises TypeError as expected 348``` 349 350## What changed in 2.0.0? 351The main changes in version 2.0.0 revolve around migrating towards the standard python typing hint process and away from the original type_enfoced type hints (as type enforced was originally created before the `|` operator was added to python). 352- Support for python3.10 has been dropped. 353- List based union types are no longer supported. 354 - For example `[int, float]` is no longer a supported type hint. 355 - Use `int|float` or `typing.Union[int, float]` instead. 356- Dict types now require two types to be specified. 357 - The first type is the key type and the second type is the value type. 358 - For example, `dict[str, int|float]` or `dict[int, float]` are valid types. 359- Tuple types now allow for `N` types to be specified. 360 - Each item refers to the positional type of each item in the tuple. 361 - Support for ellipsis (`...`) is supported if you only specify two types and the second is the ellipsis type. 362 - For example, `tuple[int, ...]` or `tuple[int|str, ...]` are valid types. 363 - Note: Unions between two tuples are not supported. 364 - For example, `tuple[int, str] | tuple[str, int]` will not work. 365- Constraints and Literals can now be stacked with unions. 366 - For example, `int | Constraint(ge=0) | Constraint(le=5)` will require any passed values to be integers that are greater than or equal to `0` and less than or equal to `5`. 367 - For example, `Literal['a', 'b'] | Literal[1, 2]` will require any passed values that are equal (`==`) to `'a'`, `'b'`, `1` or `2`. 368- Literals now evaluate during the same time as type checking and operate as OR checks. 369 - For example, `int | Literal['a', 'b']` will validate that the type is an int or the value is equal to `'a'` or `'b'`. 370- Constraints are still are evaluated after type checking and operate independently of the type checking. 371 372# Support 373 374## Bug Reports and Feature Requests 375 376If you find a bug or are looking for a new feature, please open an issue on GitHub. 377 378## Need Help? 379 380If you need help, please open an issue on GitHub. 381 382# Contributing 383 384Contributions are welcome! Please open an issue or submit a pull request. 385 386## Development 387 388To avoid extra development overhead, we expect all developers to use a unix based environment (Linux or Mac). If you use Windows, please use WSL2. 389 390For development, we test using Docker so we can lock system deps and swap out python versions easily. However, you can also use a virtual environment if you prefer. We provide a test script and a prettify script to help with development. 391 392## Making Changes 393 3941) Fork the repo and clone it locally. 3952) Make your modifications. 3963) Use Docker or a virtual environment to run tests and make sure they pass. 3974) Prettify your code. 3985) **DO NOT GENERATE DOCS**. 399 - We will generate the docs and update the version number when we are ready to release a new version. 4006) Only commit relevant changes and add clear commit messages. 401 - Atomic commits are preferred. 4027) Submit a pull request. 403 404## Docker 405 406Make sure Docker is installed and running. 407 408- Create a docker container and drop into a shell 409 - `./run.sh` 410- Run all tests (see ./utils/test.sh) 411 - `./run.sh test` 412- Prettify the code (see ./utils/prettify.sh) 413 - `./run.sh prettify` 414 415- Note: You can and should modify the `Dockerfile` to test different python versions. 416 417## Virtual Environment 418 419- Create a virtual environment 420 - `python3.XX -m venv venv` 421 - Replace `3.XX` with your python version (3.11 or higher) 422- Activate the virtual environment 423 - `source venv/bin/activate` 424- Install the development requirements 425 - `pip install -r requirements/dev.txt` 426- Run Tests 427 - `./utils/test.sh` 428- Prettify Code 429 - `./utils/prettify.sh`""" 430from .enforcer import Enforcer, FunctionMethodEnforcer