type_enforced

Type Enforced

PyPI version License: MIT PyPI Downloads

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.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 | and from __future__ import annotations typechecking are not supported)
    • For 3.10: use type_enforced<=1.10.2 (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 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.

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|float or typing.Union[int, float] instead.
  • 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] or dict[int, float] are valid types.
  • Tuple types now allow for N types 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, ...] or tuple[int|str, ...] are valid types.
    • Note: Unions between two tuples are not supported.
      • For example, tuple[int, str] | tuple[str, int] will not work.
  • 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 to 0 and less than or equal to 5.
    • For example, Literal['a', 'b'] | Literal[1, 2] will require any passed values that are equal (==) to 'a', 'b', 1 or 2.
  • 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'.
  • Constraints are still are evaluated after type checking and operate independently of the type checking.

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] or list[int|float])
    • Note: Each parent level must be an iterable
      • Specifically a variant of list, set, tuple or dict
    • Note: dict requires 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] or dict[int, float]
    • Note: list and set require a single type to be specified (unions count as a single type)
      • e.g. list[int], set[str], list[float|str]
    • Note: tuple Allows for N types 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, ...] or tuple[int|str, ...]
      • Note: Unions between two tuples are not supported
        • e.g. tuple[int, str] | tuple[str, int] will not work
    • 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
    • Any
    • 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 as acceptable values.
        • e.g. Literal['a', 'b', 1, 2] will require any passed values that are equal (==) to 'a', 'b', 1 or 2.
      • 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 of 1 will fail.
      • 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 of 1 will pass.
      • 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', 1 or 2.
    • 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: 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 to 0 and less than or equal to 5.
    • 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.
    • 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 "<python-input-2>", line 1, in <module>
    my_fn(a='a', b=2, c=3)
    ~~~~~^^^^^^^^^^^^^^^^^
  File "/app/type_enforced/enforcer.py", line 233, in __call__
    self.__check_type__(assigned_vars.get(key), value, key)
    ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/app/type_enforced/enforcer.py", line 266, in __check_type__
    self.__exception__(
    ~~~~~~~~~~~~~~~~~~^
        f"Type mismatch for typed variable `{key}`. Expected one of the following `{list(expected.keys())}` but got `{obj_type}` with value `{obj}` instead."
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
  File "/app/type_enforced/enforcer.py", line 188, in __exception__
    raise TypeError(f"TypeEnforced Exception ({self.__fn__.__qualname__}): {message}")
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=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 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

Development

Running Tests, Prettifying Code, and Updating Docs

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
  • Update the docs (see ./utils/docs.sh)

    • ./run.sh docs
  • Note: You can and should modify the Dockerfile to test different python versions.

  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[![PyPI Downloads](https://img.shields.io/pypi/dm/type_enforced.svg?label=PyPI%20downloads)](https://pypi.org/project/type_enforced/)
  6
  7A pure python (no special compiler required) type enforcer for type annotations. Enforce types in python functions and methods.
  8
  9# Setup
 10
 11Make sure you have Python 3.11.x (or higher) installed on your system. You can download it [here](https://www.python.org/downloads/).
 12
 13- Unsupported python versions can be used, however newer features will not be available.
 14    - For 3.7: use type_enforced==0.0.16 (only very basic type checking is supported)
 15    - For 3.8: use type_enforced==0.0.16 (only very basic type checking is supported)
 16    - For 3.9: use type_enforced<=1.9.0 (`staticmethod`, union with `|` and `from __future__ import annotations` typechecking are not supported)
 17    - For 3.10: use type_enforced<=1.10.2 (`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` or `typing.Union[int, float]`.
 42
 43Nesting 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]`.
 44
 45Variables without an annotation for type are not enforced.
 46
 47## What changed in 2.0.0?
 48The 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).
 49- Support for python3.10 has been dropped.
 50- List based union types are no longer supported.
 51    - For example `[int, float]` is no longer a supported type hint.
 52    - Use `int|float` or `typing.Union[int, float]` instead.
 53- Dict types now require two types to be specified.
 54    - The first type is the key type and the second type is the value type.
 55    - For example, `dict[str, int|float]` or `dict[int, float]` are valid types.
 56- Tuple types now allow for `N` types to be specified.
 57    - Each item refers to the positional type of each item in the tuple.
 58    - Support for ellipsis (`...`) is supported if you only specify two types and the second is the ellipsis type.
 59        - For example, `tuple[int, ...]` or `tuple[int|str, ...]` are valid types.
 60    - Note: Unions between two tuples are not supported.
 61        - For example, `tuple[int, str] | tuple[str, int]` will not work.
 62- Constraints and Literals can now be stacked with unions.
 63    - 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`.
 64    - For example, `Literal['a', 'b'] | Literal[1, 2]` will require any passed values that are equal (`==`) to `'a'`, `'b'`, `1` or `2`.
 65- Literals now evaluate during the same time as type checking and operate as OR checks.
 66    - For example, `int | Literal['a', 'b']` will validate that the type is an int or the value is equal to `'a'` or `'b'`.
 67- Constraints are still are evaluated after type checking and operate independently of the type checking.
 68
 69## Supported Type Checking Features:
 70
 71- Function/Method Input Typing
 72- Function/Method Return Typing
 73- Dataclass Typing
 74- All standard python types (`str`, `list`, `int`, `dict`, ...)
 75- Union types
 76    - typing.Union
 77    - `|` separated items (e.g. `int | float`)
 78- Nested types (e.g. `dict[str, int]` or `list[int|float]`)
 79    - Note: Each parent level must be an iterable
 80        - Specifically a variant of `list`, `set`, `tuple` or `dict`
 81    - Note: `dict` requires two types to be specified (unions count as a single type)
 82        - The first type is the key type and the second type is the value type
 83        - e.g. `dict[str, int|float]` or `dict[int, float]`
 84    - Note: `list` and `set` require a single type to be specified (unions count as a single type)
 85        - e.g. `list[int]`, `set[str]`, `list[float|str]`
 86    - Note: `tuple` Allows for `N` types to be specified
 87        - Each item refers to the positional type of each item in the tuple
 88        - Support for ellipsis (`...`) is supported if you only specify two types and the second is the ellipsis type
 89            - e.g. `tuple[int, ...]` or `tuple[int|str, ...]`
 90        - Note: Unions between two tuples are not supported
 91            - e.g. `tuple[int, str] | tuple[str, int]` will not work
 92    - Deeply nested types are supported too:
 93        - `dict[dict[int]]`
 94        - `list[set[str]]`
 95- Many of the `typing` (package) functions and methods including:
 96    - Standard typing functions:
 97        - `List`
 98        - `Set`
 99        - `Dict`
100        - `Tuple`
101    - `Union`
102    - `Optional`
103    - `Any`
104    - `Sized`
105        - Essentially creates a union of:
106            - `list`, `tuple`, `dict`, `set`, `str`, `bytes`, `bytearray`, `memoryview`, `range`
107        - Note: Can not have a nested type
108            - Because this does not always meet the criteria for `Nested types` above
109    - `Literal`
110        - Only allow certain values to be passed. Operates slightly differently than other checks.
111        - e.g. `Literal['a', 'b']` will require any passed values that are equal (`==`) to `'a'` or `'b'`.
112            - This compares the value of the passed input and not the type of the passed input.
113        - Note: Multiple types can be passed in the same `Literal` as acceptable values.
114            - e.g. Literal['a', 'b', 1, 2] will require any passed values that are equal (`==`) to `'a'`, `'b'`, `1` or `2`.
115        - Note: If type is a `str | Literal['a', 'b']`
116            - The check will validate that the type is a string or the value is equal to `'a'` or `'b'`.
117            - This means that an input of `'c'` will pass the check since it matches the string type, but an input of `1` will fail.
118        - Note: If type is a `int | Literal['a', 'b']`
119            - The check will validate that the type is an int or the value is equal to `'a'` or `'b'`.
120            - This means that an input of `'c'` will fail the check, but an input of `1` will pass.
121        - Note: Literals stack when used with unions.
122            - e.g. `Literal['a', 'b'] | Literal[1, 2]` will require any passed values that are equal (`==`) to `'a'`, `'b'`, `1` or `2`.
123    - `Callable`
124        - Essentially creates a union of:
125            - `staticmethod`, `classmethod`, `types.FunctionType`, `types.BuiltinFunctionType`, `types.MethodType`, `types.BuiltinMethodType`, `types.GeneratorType`
126    - Note: Other functions might have support, but there are not currently tests to validate them
127        - Feel free to create an issue (or better yet a PR) if you want to add tests/support
128- `Constraint` validation.
129    - This is a special type of validation that allows passed input to be validated.
130        - Standard and custom constraints are supported.
131    - This is useful for validating that a passed input is within a certain range or meets a certain criteria.
132    - Note: Constraints stack when used with unions.
133        - 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`.
134    - Note: The constraint is checked after type checking occurs and operates independently of the type checking.
135        - This operates differently than other checks (like `Literal`) and is evaluated post type checking.
136        - 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.
137    - 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.
138    ```
139
140## Interactive Example
141
142```py
143>>> import type_enforced
144>>> @type_enforced.Enforcer
145... def my_fn(a: int , b: int|str =2, c: int =3) -> None:
146...     pass
147...
148>>> my_fn(a=1, b=2, c=3)
149>>> my_fn(a=1, b='2', c=3)
150>>> my_fn(a='a', b=2, c=3)
151Traceback (most recent call last):
152  File "<python-input-2>", line 1, in <module>
153    my_fn(a='a', b=2, c=3)
154    ~~~~~^^^^^^^^^^^^^^^^^
155  File "/app/type_enforced/enforcer.py", line 233, in __call__
156    self.__check_type__(assigned_vars.get(key), value, key)
157    ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
158  File "/app/type_enforced/enforcer.py", line 266, in __check_type__
159    self.__exception__(
160    ~~~~~~~~~~~~~~~~~~^
161        f"Type mismatch for typed variable `{key}`. Expected one of the following `{list(expected.keys())}` but got `{obj_type}` with value `{obj}` instead."
162        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
163    )
164    ^
165  File "/app/type_enforced/enforcer.py", line 188, in __exception__
166    raise TypeError(f"TypeEnforced Exception ({self.__fn__.__qualname__}): {message}")
167TypeError: 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.
168```
169
170## Nested Examples
171```py
172import type_enforced
173import typing
174
175@type_enforced.Enforcer
176def my_fn(
177    a: dict[str,dict[str, int|float]], # Note: For dicts, the key is the first type and the value is the second type
178    b: list[typing.Set[str]] # Could also just use set
179) -> None:
180    return None
181
182my_fn(a={'i':{'j':1}}, b=[{'x'}]) # Success
183
184my_fn(a={'i':{'j':'k'}}, b=[{'x'}]) # Error =>
185# 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.
186```
187
188## Class and Method Use
189
190Type enforcer can be applied to methods individually:
191
192```py
193import type_enforced
194
195class my_class:
196    @type_enforced.Enforcer
197    def my_fn(self, b:int):
198        pass
199```
200
201You can also enforce all typing for all methods in a class by decorating the class itself.
202
203```py
204import type_enforced
205
206@type_enforced.Enforcer
207class my_class:
208    def my_fn(self, b:int):
209        pass
210
211    def my_other_fn(self, a: int, b: int | str):
212      pass
213```
214
215You 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.
216
217```py
218import type_enforced
219
220@type_enforced.Enforcer
221class my_class:
222    @classmethod
223    def my_fn(self, b:int):
224        pass
225
226    @staticmethod
227    def my_other_fn(a: int, b: int | str):
228      pass
229```
230
231Dataclasses are suported too.
232
233```py
234import type_enforced
235from dataclasses import dataclass
236
237@type_enforced.Enforcer
238@dataclass
239class my_class:
240    foo: int
241    bar: str
242```
243
244You can skip enforcement if you add the argument `enabled=False` in the `Enforcer` call.
245- This is useful for a production vs debugging environment.
246- This is also useful for undecorating a single method in a larger wrapped class.
247- Note: You can set `enabled=False` for an entire class or simply disable a specific method in a larger wrapped class.
248- Note: Method level wrapper `enabled` values take precedence over class level wrappers.
249```py
250import type_enforced
251@type_enforced.Enforcer
252class my_class:
253    def my_fn(self, a: int) -> None:
254        pass
255
256    @type_enforced.Enforcer(enabled=False)
257    def my_other_fn(self, a: int) -> None:
258        pass
259```
260
261## Validate with Constraints
262Type enforcer can enforce constraints for passed variables. These constraints are validated after any type checks are made.
263
264To 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:
265```py
266import type_enforced
267from type_enforced.utils import Constraint
268
269@type_enforced.Enforcer()
270def positive_int_test(value: int |Constraint(ge=0)) -> bool:
271    return True
272
273positive_int_test(1) # Passes
274positive_int_test(-1) # Fails
275positive_int_test(1.0) # Fails
276```
277
278To enforce a [GenericConstraint](https://connor-makowski.github.io/type_enforced/type_enforced/utils.html#GenericConstraint):
279```py
280import type_enforced
281from type_enforced.utils import GenericConstraint
282
283CustomConstraint = GenericConstraint(
284    {
285        'in_rgb': lambda x: x in ['red', 'green', 'blue'],
286    }
287)
288
289@type_enforced.Enforcer()
290def rgb_test(value: str | CustomConstraint) -> bool:
291    return True
292
293rgb_test('red') # Passes
294rgb_test('yellow') # Fails
295```
296
297
298
299## Validate class instances and classes
300
301Type enforcer can enforce class instances and classes. There are a few caveats between the two.
302
303To enforce a class instance, simply pass the class itself as a type hint:
304```py
305import type_enforced
306
307class Foo():
308    def __init__(self) -> None:
309        pass
310
311@type_enforced.Enforcer
312class my_class():
313    def __init__(self, object: Foo) -> None:
314        self.object = object
315
316x=my_class(Foo()) # Works great!
317y=my_class(Foo) # Fails!
318```
319
320Notice how an initialized class instance `Foo()` must be passed for the enforcer to not raise an exception.
321
322To enforce an uninitialized class object use `typing.Type[classHere]` on the class to enforce inputs to be an uninitialized class:
323```py
324import type_enforced
325import typing
326
327class Foo():
328    def __init__(self) -> None:
329        pass
330
331@type_enforced.Enforcer
332class my_class():
333    def __init__(self, object_class: typing.Type[Foo]) -> None:
334        self.object = object_class()
335
336y=my_class(Foo) # Works great!
337x=my_class(Foo()) # Fails
338```
339
340By 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.
341
342Note: Uninitialized class objects that are passed are not checked for subclasses.
343
344```py
345import type_enforced
346
347class Foo:
348    pass
349
350class Bar(Foo):
351    pass
352
353class Baz:
354    pass
355
356@type_enforced.Enforcer
357def my_fn(custom_class: Foo):
358    pass
359
360my_fn(Foo()) # Passes as expected
361my_fn(Bar()) # Passes as expected
362my_fn(Baz()) # Raises TypeError as expected
363```
364
365# Development
366## Running Tests, Prettifying Code, and Updating Docs
367
368Make sure Docker is installed and running.
369
370- Create a docker container and drop into a shell
371    - `./run.sh`
372- Run all tests (see ./utils/test.sh)
373    - `./run.sh test`
374- Prettify the code (see ./utils/prettify.sh)
375    - `./run.sh prettify`
376- Update the docs (see ./utils/docs.sh)
377    - `./run.sh docs`
378
379- Note: You can and should modify the `Dockerfile` to test different python versions."""
380from .enforcer import Enforcer, FunctionMethodEnforcer