Last active
May 22, 2023 17:19
-
-
Save juancarlospaco/94f98cff7f9accd8d618 to your computer and use it in GitHub Desktop.
Python3 Annotations as Static Typing, checks code blocks inputs and outputs enforcing Types via Decorator (change "log.critical" to "log.exception" to Force Fail).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
# -*- coding: utf-8 -*- | |
import functools | |
import logging as log | |
def typecheck(f): | |
"""Decorator for Python3 annotations to type-check inputs and outputs.""" | |
def __check_annotations(tipe): | |
_type, is_ok = None, isinstance(tipe, (type, tuple, type(None))) | |
if is_ok: # Annotations can be Type or Tuple or None | |
_type = tipe if isinstance(tipe, tuple) else tuple((tipe, )) | |
if None in _type: # if None on tuple replace with type(None) | |
_type = tuple([_ if _ is not None else type(_) for _ in _type]) | |
return _type, is_ok | |
@functools.wraps(f) # wrap a function or method to Type Check it. | |
def decorated(*args, **kwargs): | |
msg = "Type check error: {0} must be {1} but is {2} on function {3}()." | |
notations, f_name = tuple(f.__annotations__.keys()), f.__code__.co_name | |
for i, name in enumerate(f.__code__.co_varnames): | |
if name not in notations: | |
continue # this arg name has no annotation then skip it. | |
_type, is_ok = __check_annotations(f.__annotations__.get(name)) | |
if is_ok: # Force to tuple | |
if i < len(args) and not isinstance(args[i], _type): | |
log.critical(msg.format(repr(args[i])[:50], _type, | |
type(args[i]), f_name)) | |
elif name in kwargs and not isinstance(kwargs[name], _type): | |
log.critical(msg.format(repr(kwargs[name])[:50], _type, | |
type(kwargs[name]), f_name)) | |
out = f(*args, **kwargs) | |
_type, is_ok = __check_annotations(f.__annotations__.get("return")) | |
if is_ok and not isinstance(out, _type) and "return" in notations: | |
log.critical(msg.format(repr(out)[:50], _type, type(out), f_name)) | |
return out # The output result of function or method. | |
return decorated # The decorated function or method. | |
if __name__ in "__main__": | |
@typecheck | |
def test_pass(a: int, b: list, c: tuple=(1, 2, 3)) -> float: | |
return 42.00 | |
test_pass(1, [2, 3], (4, 5)) # this should pass ok. | |
@typecheck | |
def test_fail(a: int, b: list, x: "non-type", c: tuple=(1, 2, 3)) -> dict: | |
return "MUST Fail. Using Python3 Annotations as Static Typing Checks." | |
test_fail("This", "will", "fail", "but its ok") # this should fail. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
log.critical()
tolog.exception()
orassert
to Force Fail and TraceBack.type
ortuple
orNone
will be processed and checked.[ ]
and{ }
as annotations are ignored,*args
and**kwargs
without annotations are ignored.