type_enforced

Type Enforced

PyPI version License: MIT DOI PyPI Downloads

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 | 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, strict=True, clean_traceback=True, iterable_sample_pct=100)
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.
  • 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.
  • 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.
  • Note: iterable_sample_pct=100 by default if not specified. You can set this to a value between 0 and 100 to only check a sample of items in typed iterables (list, dict, set, variable-length tuple). Lower values improve performance for large iterables at the cost of reduced type checking coverage.

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. If True, type checking will be enforced. If False, type checking will be disabled.
  • 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.
  • clean_traceback (True): A boolean to enable or disable cleaning of tracebacks. If True, modifies the excepthook temporarily such that only the relevant stack (not in the type_enforced package) is shown.
  • iterable_sample_pct (100): An integer percentage (0-100) to control how many items in iterables are checked during type enforcement. If 100, all items are checked. If less than 100, a random sample is checked. If 0, only the first item is checked.
    • Note: Lower values improve performance for large iterables but reduce type checking coverage.

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_enforced is a pure python type enforcer that does not require any special compiler or preprocessor to work.
  • type_enforced uses 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_enforced is designed to be lightweight and easy to use, making it a great choice for both small and large projects.
  • type_enforced supports complex (nested) typing hints, union types, and many of the standard python typing functions.
  • type_enforced is designed to be fast and efficient, with minimal overhead.
  • type_enforced offers 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] 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.
    • 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 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 "<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=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

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.

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 Dockerfile to test different python versions.

Virtual Environment

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