type_enforced.enforcer

  1from types import (
  2    FunctionType,
  3    MethodType,
  4    GeneratorType,
  5    BuiltinFunctionType,
  6    BuiltinMethodType,
  7    UnionType,
  8)
  9from typing import Type, Union, Sized, Literal, Callable, get_type_hints, Any
 10from functools import update_wrapper, wraps
 11from type_enforced.utils import (
 12    Partial,
 13    GenericConstraint,
 14    DeepMerge,
 15    iterable_types,
 16)
 17
 18
 19class FunctionMethodEnforcer:
 20    def __init__(self, __fn__):
 21        """
 22        Initialize a FunctionMethodEnforcer class object as a wrapper for a passed function `__fn__`.
 23
 24        Requires:
 25
 26            - `__fn__`:
 27                - What: The function to enforce
 28                - Type: function | method | class
 29        """
 30        update_wrapper(self, __fn__)
 31        self.__fn__ = __fn__
 32        self.__outer_self__ = None
 33        # Validate that the passed function or method is a method or function
 34        self.__check_method_function__()
 35        # Get input defaults for the function or method
 36        self.__get_defaults__()
 37
 38    def __get_defaults__(self):
 39        """
 40        Get the default values of the passed function or method and store them in `self.__fn_defaults__`.
 41        """
 42        self.__fn_defaults__ = {}
 43        if self.__fn__.__defaults__ is not None:
 44            # Get the names of all provided default values for args
 45            default_varnames = list(self.__fn__.__code__.co_varnames)[
 46                : self.__fn__.__code__.co_argcount
 47            ][-len(self.__fn__.__defaults__) :]
 48            # Update the output dictionary with the default values
 49            self.__fn_defaults__.update(
 50                dict(zip(default_varnames, self.__fn__.__defaults__))
 51            )
 52        if self.__fn__.__kwdefaults__ is not None:
 53            # Update the output dictionary with the keyword default values
 54            self.__fn_defaults__.update(self.__fn__.__kwdefaults__)
 55
 56    def __get_checkable_types__(self):
 57        """
 58        Creates two class attributes:
 59
 60        - `self.__checkable_types__`:
 61            - What: A dictionary of all annotations as checkable types
 62            - Type: dict
 63
 64        - `self.__return_type__`:
 65            - What: The return type of the function or method
 66            - Type: dict | None
 67        """
 68        if not hasattr(self, "__checkable_types__"):
 69            self.__checkable_types__ = {
 70                key: self.__get_checkable_type__(value)
 71                for key, value in get_type_hints(self.__fn__).items()
 72            }
 73            self.__return_type__ = self.__checkable_types__.pop("return", None)
 74
 75    def __get_checkable_type__(self, annotation):
 76        """
 77        Parses a type annotation and returns a nested dict structure
 78        representing the checkable type(s) for validation.
 79        """
 80
 81        if annotation is None:
 82            return {type(None): None}
 83
 84        # Handle `int | str` syntax (Python 3.10+) and Unions
 85        if (
 86            isinstance(annotation, UnionType)
 87            or getattr(annotation, "__origin__", None) == Union
 88        ):
 89            combined_types = {}
 90            for sub_type in annotation.__args__:
 91                combined_types = DeepMerge(
 92                    combined_types, self.__get_checkable_type__(sub_type)
 93                )
 94            return combined_types
 95
 96        # Handle typing.Literal
 97        if getattr(annotation, "__origin__", None) == Literal:
 98            return {"__extra__": {"__literal__": list(annotation.__args__)}}
 99
100        # Handle generic collections
101        origin = getattr(annotation, "__origin__", None)
102        args = getattr(annotation, "__args__", ())
103
104        if origin == list:
105            if len(args) != 1:
106                raise TypeError(
107                    f"List must have a single type argument, got: {args}"
108                )
109            return {list: self.__get_checkable_type__(args[0])}
110
111        if origin == dict:
112            if len(args) != 2:
113                raise TypeError(
114                    f"Dict must have two type arguments, got: {args}"
115                )
116            key_type = self.__get_checkable_type__(args[0])
117            value_type = self.__get_checkable_type__(args[1])
118            return {dict: (key_type, value_type)}
119
120        if origin == tuple:
121            if len(args) > 2 or len(args) == 1:
122                if Ellipsis in args:
123                    raise TypeError(
124                        "Tuple with Ellipsis must have exactly two type arguments and the second must be Ellipsis."
125                    )
126            if len(args) == 2:
127                if args[0] is Ellipsis:
128                    raise TypeError(
129                        "Tuple with Ellipsis must have exactly two type arguments and the first must not be Ellipsis."
130                    )
131                if args[1] is Ellipsis:
132                    return {tuple: (self.__get_checkable_type__(args[0]), True)}
133            return {
134                tuple: (
135                    tuple(self.__get_checkable_type__(arg) for arg in args),
136                    False,
137                )
138            }
139
140        if origin == set:
141            if len(args) != 1:
142                raise TypeError(
143                    f"Set must have a single type argument, got: {args}"
144                )
145            return {set: self.__get_checkable_type__(args[0])}
146
147        # Handle Sized types
148        if annotation == Sized:
149            return {
150                list: None,
151                tuple: None,
152                dict: None,
153                set: None,
154                str: None,
155                bytes: None,
156                bytearray: None,
157                memoryview: None,
158                range: None,
159            }
160
161        # Handle Callable types
162        if annotation == Callable:
163            return {
164                staticmethod: None,
165                classmethod: None,
166                FunctionType: None,
167                BuiltinFunctionType: None,
168                MethodType: None,
169                BuiltinMethodType: None,
170                GeneratorType: None,
171            }
172
173        if annotation == Any:
174            return {
175                object: None,
176            }
177
178        # Handle Constraints
179        if isinstance(annotation, GenericConstraint):
180            return {"__extra__": {"__constraints__": [annotation]}}
181
182        # Handle standard types
183        if isinstance(annotation, type):
184            return {annotation: None}
185
186        # Hanldle typing.Type (for uninitialized classes)
187        if origin is type and len(args) == 1:
188            return {annotation: None}
189
190        raise TypeError(f"Unsupported type hint: {annotation}")
191
192    def __exception__(self, message):
193        """
194        Usage:
195
196        - Creates a class based exception message
197
198        Requires:
199
200        - `message`:
201            - Type: str
202            - What: The message to warn users with
203        """
204        raise TypeError(
205            f"TypeEnforced Exception ({self.__fn__.__qualname__}): {message}"
206        )
207
208    def __get__(self, obj, objtype):
209        """
210        Overwrite standard __get__ method to return __call__ instead for wrapped class methods.
211
212        Also stores the calling (__get__) `obj` to be passed as an initial argument for `__call__` such that methods can pass `self` correctly.
213        """
214
215        @wraps(self.__fn__)
216        def __get_fn__(*args, **kwargs):
217            return self.__call__(*args, **kwargs)
218
219        self.__outer_self__ = obj
220        return __get_fn__
221
222    def __check_method_function__(self):
223        """
224        Validate that `self.__fn__` is a method or function
225        """
226        if not isinstance(self.__fn__, (MethodType, FunctionType)):
227            raise Exception(
228                f"A non function/method was passed to Enforcer. See the stack trace above for more information."
229            )
230
231    def __call__(self, *args, **kwargs):
232        """
233        This method is used to validate the passed inputs and return the output of the wrapped function or method.
234        """
235        # Special code to pass self as an initial argument
236        # for validation purposes in methods
237        # See: self.__get__
238        if self.__outer_self__ is not None:
239            args = (self.__outer_self__, *args)
240        # Get a dictionary of all annotations as checkable types
241        # Note: This is only done once at first call to avoid redundant calculations
242        self.__get_checkable_types__()
243        # Create a compreshensive dictionary of assigned variables (order matters)
244        assigned_vars = {
245            **self.__fn_defaults__,
246            **dict(zip(self.__fn__.__code__.co_varnames[: len(args)], args)),
247            **kwargs,
248        }
249        # Validate all listed annotations vs the assigned_vars dictionary
250        for key, value in self.__checkable_types__.items():
251            self.__check_type__(assigned_vars.get(key), value, key)
252        # Execute the function callable
253        return_value = self.__fn__(*args, **kwargs)
254        # If a return type was passed, validate the returned object
255        if self.__return_type__ is not None:
256            self.__check_type__(return_value, self.__return_type__, "return")
257        return return_value
258
259    def __check_type__(self, obj, expected, key):
260        """
261        Raises an exception the type of a passed `obj` (parameter) is not in the list of supplied `acceptable_types` for the argument.
262        """
263        # Special case for None
264        if obj is None and type(None) in expected:
265            return
266        extra = expected.get("__extra__", {})
267        expected = {k: v for k, v in expected.items() if k != "__extra__"}
268
269        if isinstance(obj, type):
270            # An uninitialized class is passed, we need to check if the type is in the expected types using Type[obj]
271            obj_type = Type[obj]
272            is_present = obj_type in expected
273        else:
274            obj_type = type(obj)
275            is_present = isinstance(obj, tuple(expected.keys()))
276
277        if not is_present:
278            # Allow for literals to be used to bypass type checks if present
279            literal = extra.get("__literal__", ())
280            if literal:
281                if obj not in literal:
282                    self.__exception__(
283                        f"Type mismatch for typed variable `{key}`. Expected one of the following `{list(expected.keys())}` or a literal value in `{literal}` but got type `{obj_type}` with value `{obj}` instead."
284                    )
285            # Raise an exception if the type is not in the expected types
286            else:
287                self.__exception__(
288                    f"Type mismatch for typed variable `{key}`. Expected one of the following `{list(expected.keys())}` but got `{obj_type}` with value `{obj}` instead."
289                )
290        # If the object_type is in the expected types, we can proceed with validation
291        elif obj_type in iterable_types:
292            subtype = expected.get(obj_type, None)
293            if subtype is None:
294                pass
295            # Recursive validation
296            elif obj_type == list:
297                for idx, item in enumerate(obj):
298                    self.__check_type__(item, subtype, f"{key}[{idx}]")
299            elif obj_type == dict:
300                key_type, val_type = subtype
301                for k, v in obj.items():
302                    self.__check_type__(k, key_type, f"{key}.key[{repr(k)}]")
303                    self.__check_type__(v, val_type, f"{key}[{repr(k)}]")
304            elif obj_type == tuple:
305                expected_args, is_ellipsis = subtype
306                if is_ellipsis:
307                    for idx, item in enumerate(obj):
308                        self.__check_type__(
309                            item, expected_args, f"{key}[{idx}]"
310                        )
311                else:
312                    if len(obj) != len(expected_args):
313                        self.__exception__(
314                            f"Tuple length mismatch for `{key}`. Expected length {len(expected_args)}, got {len(obj)}"
315                        )
316                    for idx, (item, ex) in enumerate(zip(obj, expected_args)):
317                        self.__check_type__(item, ex, f"{key}[{idx}]")
318            elif obj_type == set:
319                for item in obj:
320                    self.__check_type__(item, subtype, f"{key}[{repr(item)}]")
321
322        # Validate constraints if any are present
323        constraints = extra.get("__constraints__", [])
324        for constraint in constraints:
325            constraint_validation_output = constraint.__validate__(key, obj)
326            if constraint_validation_output is not True:
327                self.__exception__(
328                    f"Constraint validation error for variable `{key}` with value `{obj}`. {constraint_validation_output}"
329                )
330
331    def __repr__(self):
332        return f"<type_enforced {self.__fn__.__module__}.{self.__fn__.__qualname__} object at {hex(id(self))}>"
333
334
335def Enforcer(clsFnMethod, enabled):
336    """
337    A wrapper to enforce types within a function or method given argument annotations.
338
339    Each wrapped item is converted into a special `FunctionMethodEnforcer` class object that validates the passed parameters for the function or method when it is called. If a function or method that is passed does not have any annotations, it is not converted into a `FunctionMethodEnforcer` class as no validation is possible.
340
341    If wrapping a class, all methods in the class that meet any of the following criteria will be wrapped individually:
342
343    - Methods with `__call__`
344    - Methods wrapped with `staticmethod` (if python >= 3.10)
345    - Methods wrapped with `classmethod` (if python >= 3.10)
346
347    Requires:
348
349    - `clsFnMethod`:
350        - What: The class, function or method that should have input types enforced
351        - Type: function | method | class
352
353    Optional:
354
355    - `enabled`:
356        - What: A boolean to enable or disable the enforcer
357        - Type: bool
358        - Default: True
359
360    Example Use:
361    ```
362    >>> import type_enforced
363    >>> @type_enforced.Enforcer
364    ... def my_fn(a: int , b: [int, str] =2, c: int =3) -> None:
365    ...     pass
366    ...
367    >>> my_fn(a=1, b=2, c=3)
368    >>> my_fn(a=1, b='2', c=3)
369    >>> my_fn(a='a', b=2, c=3)
370    Traceback (most recent call last):
371      File "<stdin>", line 1, in <module>
372      File "/home/conmak/development/personal/type_enforced/type_enforced/enforcer.py", line 85, in __call__
373        self.__check_type__(assigned_vars.get(key), value, key)
374      File "/home/conmak/development/personal/type_enforced/type_enforced/enforcer.py", line 107, in __check_type__
375        self.__exception__(
376      File "/home/conmak/development/personal/type_enforced/type_enforced/enforcer.py", line 34, in __exception__
377        raise Exception(f"({self.__fn__.__qualname__}): {message}")
378    Exception: (my_fn): Type mismatch for typed variable `a`. Expected one of the following `[<class 'int'>]` but got `<class 'str'>` instead.
379    ```
380    """
381    if not hasattr(clsFnMethod, "__type_enforced_enabled__"):
382        # Special try except clause to handle cases when the object is immutable
383        try:
384            clsFnMethod.__type_enforced_enabled__ = enabled
385        except:
386            return clsFnMethod
387    if not clsFnMethod.__type_enforced_enabled__:
388        return clsFnMethod
389    if isinstance(
390        clsFnMethod, (staticmethod, classmethod, FunctionType, MethodType)
391    ):
392        # Only apply the enforcer if type_hints are present
393        if get_type_hints(clsFnMethod) == {}:
394            return clsFnMethod
395        elif isinstance(clsFnMethod, staticmethod):
396            return staticmethod(FunctionMethodEnforcer(clsFnMethod.__func__))
397        elif isinstance(clsFnMethod, classmethod):
398            return classmethod(FunctionMethodEnforcer(clsFnMethod.__func__))
399        else:
400            return FunctionMethodEnforcer(clsFnMethod)
401    elif hasattr(clsFnMethod, "__dict__"):
402        for key, value in clsFnMethod.__dict__.items():
403            # Skip the __annotate__ method if present in __dict__ as it deletes itself upon invocation
404            # Skip any previously wrapped methods if they are already a FunctionMethodEnforcer
405            if key == "__annotate__" or isinstance(
406                value, FunctionMethodEnforcer
407            ):
408                continue
409            if hasattr(value, "__call__") or isinstance(
410                value, (classmethod, staticmethod)
411            ):
412                setattr(clsFnMethod, key, Enforcer(value, enabled=enabled))
413        return clsFnMethod
414    else:
415        raise Exception(
416            "Enforcer can only be used on classes, methods, or functions."
417        )
418
419
420Enforcer = Partial(Enforcer, enabled=True)
class FunctionMethodEnforcer:
 20class FunctionMethodEnforcer:
 21    def __init__(self, __fn__):
 22        """
 23        Initialize a FunctionMethodEnforcer class object as a wrapper for a passed function `__fn__`.
 24
 25        Requires:
 26
 27            - `__fn__`:
 28                - What: The function to enforce
 29                - Type: function | method | class
 30        """
 31        update_wrapper(self, __fn__)
 32        self.__fn__ = __fn__
 33        self.__outer_self__ = None
 34        # Validate that the passed function or method is a method or function
 35        self.__check_method_function__()
 36        # Get input defaults for the function or method
 37        self.__get_defaults__()
 38
 39    def __get_defaults__(self):
 40        """
 41        Get the default values of the passed function or method and store them in `self.__fn_defaults__`.
 42        """
 43        self.__fn_defaults__ = {}
 44        if self.__fn__.__defaults__ is not None:
 45            # Get the names of all provided default values for args
 46            default_varnames = list(self.__fn__.__code__.co_varnames)[
 47                : self.__fn__.__code__.co_argcount
 48            ][-len(self.__fn__.__defaults__) :]
 49            # Update the output dictionary with the default values
 50            self.__fn_defaults__.update(
 51                dict(zip(default_varnames, self.__fn__.__defaults__))
 52            )
 53        if self.__fn__.__kwdefaults__ is not None:
 54            # Update the output dictionary with the keyword default values
 55            self.__fn_defaults__.update(self.__fn__.__kwdefaults__)
 56
 57    def __get_checkable_types__(self):
 58        """
 59        Creates two class attributes:
 60
 61        - `self.__checkable_types__`:
 62            - What: A dictionary of all annotations as checkable types
 63            - Type: dict
 64
 65        - `self.__return_type__`:
 66            - What: The return type of the function or method
 67            - Type: dict | None
 68        """
 69        if not hasattr(self, "__checkable_types__"):
 70            self.__checkable_types__ = {
 71                key: self.__get_checkable_type__(value)
 72                for key, value in get_type_hints(self.__fn__).items()
 73            }
 74            self.__return_type__ = self.__checkable_types__.pop("return", None)
 75
 76    def __get_checkable_type__(self, annotation):
 77        """
 78        Parses a type annotation and returns a nested dict structure
 79        representing the checkable type(s) for validation.
 80        """
 81
 82        if annotation is None:
 83            return {type(None): None}
 84
 85        # Handle `int | str` syntax (Python 3.10+) and Unions
 86        if (
 87            isinstance(annotation, UnionType)
 88            or getattr(annotation, "__origin__", None) == Union
 89        ):
 90            combined_types = {}
 91            for sub_type in annotation.__args__:
 92                combined_types = DeepMerge(
 93                    combined_types, self.__get_checkable_type__(sub_type)
 94                )
 95            return combined_types
 96
 97        # Handle typing.Literal
 98        if getattr(annotation, "__origin__", None) == Literal:
 99            return {"__extra__": {"__literal__": list(annotation.__args__)}}
100
101        # Handle generic collections
102        origin = getattr(annotation, "__origin__", None)
103        args = getattr(annotation, "__args__", ())
104
105        if origin == list:
106            if len(args) != 1:
107                raise TypeError(
108                    f"List must have a single type argument, got: {args}"
109                )
110            return {list: self.__get_checkable_type__(args[0])}
111
112        if origin == dict:
113            if len(args) != 2:
114                raise TypeError(
115                    f"Dict must have two type arguments, got: {args}"
116                )
117            key_type = self.__get_checkable_type__(args[0])
118            value_type = self.__get_checkable_type__(args[1])
119            return {dict: (key_type, value_type)}
120
121        if origin == tuple:
122            if len(args) > 2 or len(args) == 1:
123                if Ellipsis in args:
124                    raise TypeError(
125                        "Tuple with Ellipsis must have exactly two type arguments and the second must be Ellipsis."
126                    )
127            if len(args) == 2:
128                if args[0] is Ellipsis:
129                    raise TypeError(
130                        "Tuple with Ellipsis must have exactly two type arguments and the first must not be Ellipsis."
131                    )
132                if args[1] is Ellipsis:
133                    return {tuple: (self.__get_checkable_type__(args[0]), True)}
134            return {
135                tuple: (
136                    tuple(self.__get_checkable_type__(arg) for arg in args),
137                    False,
138                )
139            }
140
141        if origin == set:
142            if len(args) != 1:
143                raise TypeError(
144                    f"Set must have a single type argument, got: {args}"
145                )
146            return {set: self.__get_checkable_type__(args[0])}
147
148        # Handle Sized types
149        if annotation == Sized:
150            return {
151                list: None,
152                tuple: None,
153                dict: None,
154                set: None,
155                str: None,
156                bytes: None,
157                bytearray: None,
158                memoryview: None,
159                range: None,
160            }
161
162        # Handle Callable types
163        if annotation == Callable:
164            return {
165                staticmethod: None,
166                classmethod: None,
167                FunctionType: None,
168                BuiltinFunctionType: None,
169                MethodType: None,
170                BuiltinMethodType: None,
171                GeneratorType: None,
172            }
173
174        if annotation == Any:
175            return {
176                object: None,
177            }
178
179        # Handle Constraints
180        if isinstance(annotation, GenericConstraint):
181            return {"__extra__": {"__constraints__": [annotation]}}
182
183        # Handle standard types
184        if isinstance(annotation, type):
185            return {annotation: None}
186
187        # Hanldle typing.Type (for uninitialized classes)
188        if origin is type and len(args) == 1:
189            return {annotation: None}
190
191        raise TypeError(f"Unsupported type hint: {annotation}")
192
193    def __exception__(self, message):
194        """
195        Usage:
196
197        - Creates a class based exception message
198
199        Requires:
200
201        - `message`:
202            - Type: str
203            - What: The message to warn users with
204        """
205        raise TypeError(
206            f"TypeEnforced Exception ({self.__fn__.__qualname__}): {message}"
207        )
208
209    def __get__(self, obj, objtype):
210        """
211        Overwrite standard __get__ method to return __call__ instead for wrapped class methods.
212
213        Also stores the calling (__get__) `obj` to be passed as an initial argument for `__call__` such that methods can pass `self` correctly.
214        """
215
216        @wraps(self.__fn__)
217        def __get_fn__(*args, **kwargs):
218            return self.__call__(*args, **kwargs)
219
220        self.__outer_self__ = obj
221        return __get_fn__
222
223    def __check_method_function__(self):
224        """
225        Validate that `self.__fn__` is a method or function
226        """
227        if not isinstance(self.__fn__, (MethodType, FunctionType)):
228            raise Exception(
229                f"A non function/method was passed to Enforcer. See the stack trace above for more information."
230            )
231
232    def __call__(self, *args, **kwargs):
233        """
234        This method is used to validate the passed inputs and return the output of the wrapped function or method.
235        """
236        # Special code to pass self as an initial argument
237        # for validation purposes in methods
238        # See: self.__get__
239        if self.__outer_self__ is not None:
240            args = (self.__outer_self__, *args)
241        # Get a dictionary of all annotations as checkable types
242        # Note: This is only done once at first call to avoid redundant calculations
243        self.__get_checkable_types__()
244        # Create a compreshensive dictionary of assigned variables (order matters)
245        assigned_vars = {
246            **self.__fn_defaults__,
247            **dict(zip(self.__fn__.__code__.co_varnames[: len(args)], args)),
248            **kwargs,
249        }
250        # Validate all listed annotations vs the assigned_vars dictionary
251        for key, value in self.__checkable_types__.items():
252            self.__check_type__(assigned_vars.get(key), value, key)
253        # Execute the function callable
254        return_value = self.__fn__(*args, **kwargs)
255        # If a return type was passed, validate the returned object
256        if self.__return_type__ is not None:
257            self.__check_type__(return_value, self.__return_type__, "return")
258        return return_value
259
260    def __check_type__(self, obj, expected, key):
261        """
262        Raises an exception the type of a passed `obj` (parameter) is not in the list of supplied `acceptable_types` for the argument.
263        """
264        # Special case for None
265        if obj is None and type(None) in expected:
266            return
267        extra = expected.get("__extra__", {})
268        expected = {k: v for k, v in expected.items() if k != "__extra__"}
269
270        if isinstance(obj, type):
271            # An uninitialized class is passed, we need to check if the type is in the expected types using Type[obj]
272            obj_type = Type[obj]
273            is_present = obj_type in expected
274        else:
275            obj_type = type(obj)
276            is_present = isinstance(obj, tuple(expected.keys()))
277
278        if not is_present:
279            # Allow for literals to be used to bypass type checks if present
280            literal = extra.get("__literal__", ())
281            if literal:
282                if obj not in literal:
283                    self.__exception__(
284                        f"Type mismatch for typed variable `{key}`. Expected one of the following `{list(expected.keys())}` or a literal value in `{literal}` but got type `{obj_type}` with value `{obj}` instead."
285                    )
286            # Raise an exception if the type is not in the expected types
287            else:
288                self.__exception__(
289                    f"Type mismatch for typed variable `{key}`. Expected one of the following `{list(expected.keys())}` but got `{obj_type}` with value `{obj}` instead."
290                )
291        # If the object_type is in the expected types, we can proceed with validation
292        elif obj_type in iterable_types:
293            subtype = expected.get(obj_type, None)
294            if subtype is None:
295                pass
296            # Recursive validation
297            elif obj_type == list:
298                for idx, item in enumerate(obj):
299                    self.__check_type__(item, subtype, f"{key}[{idx}]")
300            elif obj_type == dict:
301                key_type, val_type = subtype
302                for k, v in obj.items():
303                    self.__check_type__(k, key_type, f"{key}.key[{repr(k)}]")
304                    self.__check_type__(v, val_type, f"{key}[{repr(k)}]")
305            elif obj_type == tuple:
306                expected_args, is_ellipsis = subtype
307                if is_ellipsis:
308                    for idx, item in enumerate(obj):
309                        self.__check_type__(
310                            item, expected_args, f"{key}[{idx}]"
311                        )
312                else:
313                    if len(obj) != len(expected_args):
314                        self.__exception__(
315                            f"Tuple length mismatch for `{key}`. Expected length {len(expected_args)}, got {len(obj)}"
316                        )
317                    for idx, (item, ex) in enumerate(zip(obj, expected_args)):
318                        self.__check_type__(item, ex, f"{key}[{idx}]")
319            elif obj_type == set:
320                for item in obj:
321                    self.__check_type__(item, subtype, f"{key}[{repr(item)}]")
322
323        # Validate constraints if any are present
324        constraints = extra.get("__constraints__", [])
325        for constraint in constraints:
326            constraint_validation_output = constraint.__validate__(key, obj)
327            if constraint_validation_output is not True:
328                self.__exception__(
329                    f"Constraint validation error for variable `{key}` with value `{obj}`. {constraint_validation_output}"
330                )
331
332    def __repr__(self):
333        return f"<type_enforced {self.__fn__.__module__}.{self.__fn__.__qualname__} object at {hex(id(self))}>"
FunctionMethodEnforcer(__fn__)
21    def __init__(self, __fn__):
22        """
23        Initialize a FunctionMethodEnforcer class object as a wrapper for a passed function `__fn__`.
24
25        Requires:
26
27            - `__fn__`:
28                - What: The function to enforce
29                - Type: function | method | class
30        """
31        update_wrapper(self, __fn__)
32        self.__fn__ = __fn__
33        self.__outer_self__ = None
34        # Validate that the passed function or method is a method or function
35        self.__check_method_function__()
36        # Get input defaults for the function or method
37        self.__get_defaults__()

Initialize a FunctionMethodEnforcer class object as a wrapper for a passed function __fn__.

Requires:

- `__fn__`:
    - What: The function to enforce
    - Type: function | method | class
def Enforcer(clsFnMethod, enabled):
336def Enforcer(clsFnMethod, enabled):
337    """
338    A wrapper to enforce types within a function or method given argument annotations.
339
340    Each wrapped item is converted into a special `FunctionMethodEnforcer` class object that validates the passed parameters for the function or method when it is called. If a function or method that is passed does not have any annotations, it is not converted into a `FunctionMethodEnforcer` class as no validation is possible.
341
342    If wrapping a class, all methods in the class that meet any of the following criteria will be wrapped individually:
343
344    - Methods with `__call__`
345    - Methods wrapped with `staticmethod` (if python >= 3.10)
346    - Methods wrapped with `classmethod` (if python >= 3.10)
347
348    Requires:
349
350    - `clsFnMethod`:
351        - What: The class, function or method that should have input types enforced
352        - Type: function | method | class
353
354    Optional:
355
356    - `enabled`:
357        - What: A boolean to enable or disable the enforcer
358        - Type: bool
359        - Default: True
360
361    Example Use:
362    ```
363    >>> import type_enforced
364    >>> @type_enforced.Enforcer
365    ... def my_fn(a: int , b: [int, str] =2, c: int =3) -> None:
366    ...     pass
367    ...
368    >>> my_fn(a=1, b=2, c=3)
369    >>> my_fn(a=1, b='2', c=3)
370    >>> my_fn(a='a', b=2, c=3)
371    Traceback (most recent call last):
372      File "<stdin>", line 1, in <module>
373      File "/home/conmak/development/personal/type_enforced/type_enforced/enforcer.py", line 85, in __call__
374        self.__check_type__(assigned_vars.get(key), value, key)
375      File "/home/conmak/development/personal/type_enforced/type_enforced/enforcer.py", line 107, in __check_type__
376        self.__exception__(
377      File "/home/conmak/development/personal/type_enforced/type_enforced/enforcer.py", line 34, in __exception__
378        raise Exception(f"({self.__fn__.__qualname__}): {message}")
379    Exception: (my_fn): Type mismatch for typed variable `a`. Expected one of the following `[<class 'int'>]` but got `<class 'str'>` instead.
380    ```
381    """
382    if not hasattr(clsFnMethod, "__type_enforced_enabled__"):
383        # Special try except clause to handle cases when the object is immutable
384        try:
385            clsFnMethod.__type_enforced_enabled__ = enabled
386        except:
387            return clsFnMethod
388    if not clsFnMethod.__type_enforced_enabled__:
389        return clsFnMethod
390    if isinstance(
391        clsFnMethod, (staticmethod, classmethod, FunctionType, MethodType)
392    ):
393        # Only apply the enforcer if type_hints are present
394        if get_type_hints(clsFnMethod) == {}:
395            return clsFnMethod
396        elif isinstance(clsFnMethod, staticmethod):
397            return staticmethod(FunctionMethodEnforcer(clsFnMethod.__func__))
398        elif isinstance(clsFnMethod, classmethod):
399            return classmethod(FunctionMethodEnforcer(clsFnMethod.__func__))
400        else:
401            return FunctionMethodEnforcer(clsFnMethod)
402    elif hasattr(clsFnMethod, "__dict__"):
403        for key, value in clsFnMethod.__dict__.items():
404            # Skip the __annotate__ method if present in __dict__ as it deletes itself upon invocation
405            # Skip any previously wrapped methods if they are already a FunctionMethodEnforcer
406            if key == "__annotate__" or isinstance(
407                value, FunctionMethodEnforcer
408            ):
409                continue
410            if hasattr(value, "__call__") or isinstance(
411                value, (classmethod, staticmethod)
412            ):
413                setattr(clsFnMethod, key, Enforcer(value, enabled=enabled))
414        return clsFnMethod
415    else:
416        raise Exception(
417            "Enforcer can only be used on classes, methods, or functions."
418        )

A wrapper to enforce types within a function or method given argument annotations.

Each wrapped item is converted into a special FunctionMethodEnforcer class object that validates the passed parameters for the function or method when it is called. If a function or method that is passed does not have any annotations, it is not converted into a FunctionMethodEnforcer class as no validation is possible.

If wrapping a class, all methods in the class that meet any of the following criteria will be wrapped individually:

  • Methods with __call__
  • Methods wrapped with staticmethod (if python >= 3.10)
  • Methods wrapped with classmethod (if python >= 3.10)

Requires:

  • clsFnMethod:
    • What: The class, function or method that should have input types enforced
    • Type: function | method | class

Optional:

  • enabled:
    • What: A boolean to enable or disable the enforcer
    • Type: bool
    • Default: True

Example Use:

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