<jupyter_start><jupyter_text>decorator: enforce types<jupyter_code>def typecheck(**types):
    def decorator(func):
        @wraps(func)
        def wrapped(*args, **kwargs):
            fargs = inspect.signature(wrapped).bind(*args, **kwargs).arguments
            for k, v in types.items():
                if k in fargs and not isinstance(fargs[k], v):
                    raise TypeError(
                        f'{k} must be {str(v)}, not {type(fargs[k])}.'
                    )
            return func(*args, **kwargs)
        return wrapped
    return decorator
class Bar:
    
    def __init__(self, a:int, b:str, c:list=[1,2,3,4], d:float=0.0):
        self.a = a
        self.b = b
        self.c = c
        self.d = d
dec = typecheck(a=int, b=float, c=str)
dec(Bar.__init__)(Bar, a=1, b=2.0, c='3')
class Foo:
    
    @typecheck(a=int, b=str, c=int, e=list)
    def __init__(self, a, b, c=3, d='d', e=[1, 2, 3]):
        self.a = a
        self.b = b
        self.c = c
        self.d = d
        self.e = e
        
    @typecheck(x=(int, float))
    def compute(self, x, label='abc'):
        print(label)
        return self.a * self.c / x
f = Foo(1, 'b', 6, e=[3, 4])
print(f.compute(14))
print(f.compute(3.0, label=4444))
print(f.compute('a'))
f = Foo(1, b='b', d=4)
f = Foo(1, 'b', [1,3])<jupyter_output><empty_output><jupyter_text>decorator: enforce type annotations<jupyter_code>def enforce_types(func):
    """Wrapper for @typecheck decorator that infers types based off of 
    annotations.
    
    Parameters
    ----------
    func: function
        The function to decorate.
        
    Examples
    --------
    In the example below, notice that type annotations can be a single type,
    a tuple of types, or excluded entirely. Annotations regarding the returned
    value are not checked.
    
    @enforce_types
    def process(x:float, y:(int, float), z, iters:int=5, verbose:bool=True):
        print(f'z = {z}')
        for i in range(iters):
            if verbose: print(f'Iteration {i}...')
            x *= y
        return x
        
    >>> process(3.1, 4.5, 0, 2.0)
    TypeError: iters must be <class 'int'>, not <class 'float'>.
    
    >>> process(3.1, 4, 'a', 1, False)
    z = a
    12.4
    """
    arg_types = {k: v.annotation 
                 for k, v in inspect.signature(func).parameters.items()
                 if not v.annotation == inspect._empty}
    return typecheck(**arg_types)(func)
@enforce_types
def process(x:float, y:(int, float), z, iters:int=5, verbose:bool=True):
    print(f'z = {z}')
    for i in range(iters):
        if verbose: print(f'Iteration {i}...')
        x *= y
    return x
process(3.1, 4, 'a', 2.0)
process(3.1, 4, 'a', 1, False)
@enforce_types
def foobar(a:int, b:str, c:list, d:float=0.0, e:bool=False):
    """Return tuple of arguments."""
    return a, b, c, d, e
foobar(1, 'b', ['c'], 0.0, True)
foobar(a=1, b='b', c=[1], d=1.0, e=True)
foobar(1, 'b', [1], d=1.0, e=True)
foobar(a=1, b='b', c=[1], d=False)
foobar(1, 2, ['c'], e=True)
foobar(1, '2', 'c', e=True)
class EnforceTypes:
    
    def __init__(self, func):
        print('ET init')
        self.func = func
        self.arg_types = {k: v.annotation for k, v
                          in inspect.signature(func).parameters.items()
                          if not v.annotation == inspect._empty}
        self.check = typecheck(**self.arg_types)
        
    def __call__(self, *args, **kwargs):
        print('ET call')
        return self.check(self.func)(*args, **kwargs)
@EnforceTypes
def foobar2(a:int, b:str, c:list, d:float=0.0, e:bool=False):
    """Return tuple of arguments."""
    return a, b, c, d, e
foobar2(1, 'b', [1, 3])
class Foo:
    
    @EnforceTypes
    def __init__(self, a, b, c=3, d='d', e=[1, 2, 3]):
        self.a = a
        self.b = b
        self.c = c
        self.d = d
        self.e = e
        
    @EnforceTypes
    def compute(self, x, label='abc'):
        print(label)
        return self.a * self.c / x
f = Foo(a=1, b=2)
%%race -n 100 -r 100
_ = foobar(a=1, b='b', c=[1], d=1.0, e=True)
_ = foobar2(a=1, b='b', c=[1], d=1.0, e=True)
%%race -n 100 -r 100
_ = foobar(1, 'b', c=[1], e=True)
_ = foobar2(1, 'b', c=[1], e=True)<jupyter_output>The slowest run took 4.89 times longer than the fastest. This could mean that an intermediate result is being cached.
31.2 µs ± 14 µs per loop (mean ± std. dev. of 100 runs, 100 loops each)
33.8 µs ± 7 µs per loop (mean ± std. dev. of 100 runs, 100 loops each)<jupyter_text>Try to combine into 1 decorator that can be called with or without parens<jupyter_code>def f(func=None, **kwargs):
    if func:
        print(func)
    else:
        print('no func')
f(print)
from functools import partial
def typecheck(func_=None, **types):
    """
    Parameters
    ----------
    func_: function
        The function to decorate. When using decorator with 
        manually-specified types, this is None. Underscore is used so that
        `func` can still be used as a valid keyword argument for the wrapped 
        function.
    types: type
        Optional way to specify variable types.
        
    Examples
    --------
    In the first example, we specify types directly in the decorator. Notice
    that they can be single types or tuples of types. You can choose to 
    specify types for all arguments or just a subset.
    
    @typecheck(x=float, y=(int, float), iters=int, verbose=bool)
    def process(x, y, z, iters=5, verbose=True):
        print(f'z = {z}')
        for i in range(iters):
            if verbose: print(f'Iteration {i}...')
            x *= y
        return x
    
    >>> process(3.1, 4.5, 0, 2.0)
    TypeError: iters must be <class 'int'>, not <class 'float'>.
    
    >>> process(3.1, 4, 'a', 1, False)
    z = a
    12.4
    
    Alternatively, you can let the decorator infer types using annotations
    in the function that is to be decorated. The example below behaves 
    equivalently to the explicit example shown above. Note that annotations
    regarding the returned value are ignored.
    
    @typecheck
    def process(x:float, y:(int, float), z, iters:int=5, verbose:bool=True):
        print(f'z = {z}')
        for i in range(iters):
            if verbose: print(f'Iteration {i}...')
            x *= y
        return x
        
    >>> process(3.1, 4.5, 0, 2.0)
    TypeError: iters must be <class 'int'>, not <class 'float'>.
    
    >>> process(3.1, 4, 'a', 1, False)
    z = a
    12.4
    """
    # Case 1: Pass keyword args to decorator specifying types.
    if not func_:
        return partial(typecheck, **types)
    # Case 2: Infer types from annotations. Skip if Case 1 already occurred.
    elif not types:
        types = {k: v.annotation 
                 for k, v in inspect.signature(func_).parameters.items()
                 if not v.annotation == inspect._empty}
    
    @wraps(func_)
    def wrapped(*args, **kwargs):
        fargs = inspect.signature(wrapped).bind(*args, **kwargs).arguments
        for k, v in types.items():
            if k in fargs and not isinstance(fargs[k], v):
                raise TypeError(
                    f'{k} must be {str(v)}, not {type(fargs[k])}.'
                )
        return func_(*args, **kwargs)
    return wrapped
@typecheck(x=float, y=(int, float), iters=int, verbose=bool)
def process(x, y, z, iters=5, verbose=True):
    """Process docstring."""
    print(f'z = {z}')
    for i in range(iters):
        if verbose: print(f'Iteration {i}...')
        x *= y
    return x
process.__doc__
process(3.1, 4, 'a', 1, False)
process(3.1, 4.5, 0, 2.0)
@typecheck(a=int, c=str, d=list)
def foo3(a, b=3, c='d', d=[1, 2, 3]):
    return d, c, b, a
foo3(2)
foo3(2.0)
foo3(1, 'a', 0)
foo3(1, 4.0, c='c', d=[9])
@typecheck(b=(int, float), c=str)
def foo4(a, b=3, c='d', d=[1, 2, 3]):
    return d, c, b, a
foo4('a')
foo4(3, 2.0)
@typecheck
def foo5(a:int, b=3, c:str='d', d:list=[1, 2, 3]):
    return d, c, b, a
foo5(1)
foo5('a')
@typecheck
def foo5(a:str, b=3, c:(int, float)=4.0, d=[1, 2, 3]):
    return d, c, b, a
foo5(a='a')
foo5('a', 33, c=9, d=0)
foo5('a', 'b', 'c', 'd')
class Foo:
    
    @typecheck
    def __init__(self, a:str, b:int, c:(int, float)=3, d='d', e:list=[1, 2, 3]):
        self.a = a
        self.b = b
        self.c = c
        self.d = d
        self.e = e
        
    @typecheck(x=int, label=str)
    def compute(self, x, label='abc'):
        print(label)
        return self.b * self.c / x
f = Foo('a', 3, 33)
f.compute(2, 'a')
f = Foo('a', 0, d=True, e=False)
# Just thinking through call process, basically pseudocode.

# Explicit types. First pass in types to create decorator, then call on func, then call wrapped func.
# dec = typecheck(a=int, b=str)  
# decorated = dec(func)  
# decorated(*args, **kwargs)  

# Inferred types. First pass func to typecheck, then call on args and kwargs. 
# decorated = typecheck(func)  
# decorated(*args, **kwargs)  
from typing import List
isinstance(['a', 'b'], List[str])
# Book example
class OrderedMeta(type):
    def __new__(cls, clsname, bases, clsdict):
        d = dict(clsdict)
        print(cls, clsname, bases, clsdict)
        order = []
        for name, value in cls.__init__.__code__.co_varnames:
            print(name, value)
#             if isinstance(value, (int, str, float, bool)): continue
#             value._name = name
#             order.append(name)
#         d['_order'] = order
        return type.__new__(cls, clsname, bases, d)
# Book example
class Structure(metaclass=OrderedMeta):
    def as_csv(self):
        print('IN AS_CSV')    
        return ','.join(str(getattr(self,name)) for name in self._order)
# Example use
class Stock(Structure):
    x = 3
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price
s = Stock('n', 's', 'p')
hdir(s)
s.as_csv()
s._order<jupyter_output><empty_output><jupyter_text>try to rebuild autoinit as decorator<jupyter_code>def autoinit(cls):
    @wraps(cls)
    def wrapped(*args, **kwargs):
        print(args, kwargs)
        obj = cls.__new__(cls)
        bound = inspect.signature(cls.__init__).bind(obj, *args, **kwargs)
        bound.apply_defaults()
        
        arg_str = cls.__name__ + '('
        for k, v in bound.arguments.items():
            if k == 'self': continue
            setattr(obj, k, v)
            arg_str += f'{k}={v}, '
            
        def __repr__(obj):
            arg_str = ', '.join(f'{k}={v}' 
                                for k, v in bound.arguments.items())
            return obj.__name__
        
        obj.__repr__ = __repr__
        return obj
    return wrapped
@autoinit
class Data:
    
    def __init__(self, a, b=3, c=True, **kwargs):
#         self.a = a
#         self.b = b
#         self.c = c
        pass
    
#     def __repr__(self):
#         return f'Data({self.a}, {self.b}, {self.c})'
Data.__new__()
d = Data(91, z=44)
d
d.__dict__
d.__repr__(d)
bound = inspect.signature(foo5).bind('a', c=6)
hdir(bound)
%%talk
bound.args
bound.kwargs
bound.arguments
bound.apply_defaults()
bound<jupyter_output><empty_output><jupyter_text>Alternate inheritance method - trying to avoid need to pass locals()<jupyter_code>from contextlib import contextmanager
import inspect
import sys

from htools import *
# Temp 2 to distinguish from current implementation.

# Note: likely because of frame hack, this version is ~8x slower. However, in
# the example below this still only took 30-40 microseconds 
# (i.e. 3e-5 seconds), so in practice this probably isn't a huge concern in 
# most scenarios.
class AutoInit2:
    def __init__(self):
        # Calculate how many frames to go back to get child class.
        frame_idx = type(self).__mro__.index(AutoInit2) # TODO: rm 2 if shipping
        attrs = {k: v for k, v in sys._getframe(frame_idx).f_locals.items()
                  if not k.startswith('__')}
        attrs.pop('self')
        bound = inspect.signature(self.__class__.__init__)\
                       .bind_partial(**attrs)
        
        # Flatten dict so kwargs are not listed as their own argument.
        bound.arguments.update(
            bound.arguments.pop('kwargs', {}).get('kwargs', {})
        )
        self._init_keys = set(bound.arguments.keys())
        for k, v in bound.arguments.items():
            setattr(self, k, v)

    def __repr__(self):
        fstrs = (f'{k}={repr(getattr(self, k))}' for k in self._init_keys)            
        return f'{self.__class__.__name__}({", ".join(fstrs)})'
class Niece(AutoInit2):
    def __init__(self):
        super().__init__()
    def niecemethod(self):
        print('niece call niecemethod')
class Mixin:
    def mix(self):
        print('Mixin call self.mix')
class Multi(Niece, Mixin):
    def __init__(self, a, b, c=3, **kwargs):
        super().__init__()
class Child(AutoInit2):
    def __init__(self, a, b=3, **kwargs):
        super().__init__()
    def childmethod(self):
        print('child call childmethod')
class ProblemChild(AutoInit):
    b = ReadOnly('b')
    def __init__(self, a, b=3, **kwargs):
        super().__init__(locals())
    def childmethod(self):
        print('child call childmethod')
    @cached_property
    def c(self):
        for i in range(10_000_000):
            pass
        return self.a*self.b
class ProblemChild2(AutoInit2):
    b = ReadOnly('b')
    def __init__(self, a, b=3, **kwargs):
        super().__init__()
    def childmethod(self):
        print('child call childmethod')
    @cached_property
    def c(self):
        for i in range(10_000_000):
            pass
        return self.a*self.b
@contextmanager
def assert_raises(error, verbose=True):
    try:
        yield
    except error as e:
        if verbose: print(f'As expected, got {error.__name__}({e}).')
    except Exception as e:
        raise AssertionError(f'Wrong error raised. Expected {error.__name__},'
                             f' got {type(e).__name__}({e}).') from None
    else:
        raise AssertionError(f'No error raised, expected {error.__name__}.')
n = Niece()
n
m = Multi(1, 2, d='d', e='e')
m
c = Child('a', d='d', e=[1,2,3])
c
p = ProblemChild(2, e=True, d=[56, 55, 44])
p
with assert_raises(ValueError) as a:
    p.b = 2
with assert_raises(PermissionError) as a:
    p.a = 2
with assert_raises(PermissionError) as a:
    p.b = 2
p.c
# Faster because result is cached now.
p.c
p2 = ProblemChild2(2, e=True, d=[56, 55, 44])
p2
with assert_raises(PermissionError) as ar:
    p2.b = 33
p2.c
p3 = ProblemChild2(0, b=44, z=['a', 'b'])
p3
p3.c
p3.c
with assert_raises(PermissionError) as ar:
    p3.b = 'b'
with assert_raises(PermissionError) as ar:
    del p3.b
p3.b
p3.__dict__['b'] = 99
p3
def f():
    return ProblemChild(1, e=True, d=[56, 55, 44])
    
def main():
    m = Multi(11, 111, e='e')
    p = f()
    return m, p

m, p = main()
m
p
%%race -n 100 -r 50
p = ProblemChild(2, e=True, d=[56, 55, 44])
p2 = ProblemChild2(2, e=True, d=[56, 55, 44])
%%race -n 100 -r 50
p = ProblemChild(2, e=True, d=[56, 55, 44], x=True, y=False, z=[1,2], m={})
p2 = ProblemChild2(2, e=True, d=[56, 55, 44], x=True, y=False, z=[1,2], m={})<jupyter_output>7.27 µs ± 1.49 µs per loop (mean ± std. dev. of 50 runs, 100 loops each)
The slowest run took 5.41 times longer than the fastest. This could mean that an intermediate result is being cached.
44 µs ± 23.5 µs per loop (mean ± std. dev. of 50 runs, 100 loops each)<jupyter_text>@debug - rm self from args if method<jupyter_code>from functools import wraps
import inspect

from htools import hdir
def debug(func=None, prefix='', arguments=True):
    """Decorator that prints information about a function call. Often, this
    will only be used temporarily when debugging. Note that a wrapped function
    that accepts *args will display a signature including an 'args' parameter
    even though it isn't a named parameter, because the goal here is to
    explicitly show which values are being passed to which parameters. This
    does mean that the printed string won't be executable code in this case,
    but that shouldn't be necessary anyway since it would contain the same call
    that just occurred.

    The decorator can be used with or without arguments.

    Parameters
    ----------
    func: function
        Function being decorated.
    prefix: str
        A short string to prepend the printed message with. Ex: '>>>'
    arguments: bool
        If True, the printed message will include the function arguments.
        If False, it will print the function name but not its arguments.

    Examples
    --------
    Occasionally, you might pass arguments to different parameters than you
    intended. Throwing a debug_call decorator on the function helps you check
    that the arguments are matching up as expected. For example, the parameter
    names in the function below have an unexpected order, so you could easily
    make the following call and expect to get 8. The debug decorator helps
    catch that the third argument is being passed in as the x parameter.

    @debug
    def f(a, b, x=0, y=None, z=4, c=2):
        return a + b + c

    >>> f(3, 4, 1)
    CALLING f(a=3, b=4, x=1, y=None, z=4, c=2)
    9

    @debug(prefix='***', arguments=False)
    def f(a, b, x=0, y=None, z=4, c=2):
        return a + b + c

    >>> f(3, 4, 1)
    *** CALLING f()
    9
    """
    if not func:
        if prefix: prefix += ' '
        return partial(debug, prefix=prefix, arguments=arguments)

    @wraps(func)
    def wrapper(*args, **kwargs):
        out_fmt = '\n{}CALLING {}({})'
        arg_strs = ''
        if arguments:
            sig = inspect.signature(wrapper).bind_partial(*args, **kwargs)
            sig.apply_defaults()
            sig.arguments.update(sig.arguments.pop('kwargs', {}))
            # Remove self/cls arg from methods.
            first_key = next(iter(sig.arguments))
            if first_key in ('self', 'cls'): 
                del sig.arguments[first_key]
            arg_strs = (f'{k}={repr(v)}' for k, v in sig.arguments.items())

        # Print call message and return output.
        print(out_fmt.format(prefix, func.__qualname__, ', '.join(arg_strs)))
        return func(*args, **kwargs)

    return wrapper
class Foo: 
    @debug
    def __init__(self, a=6, b:int=1, c:str='c', d=True): 
        self.a, self.b, self.c, self.d = a, b, c, d 
    def walk(self): 
        return 'walk' 
    def _run(self, r:(bool, int)): 
        return r 
    def jump(self, x:bool, y, z:list=[1,2]): 
        return x, y, z 
    def __private(self, p:int=0): 
        return 'private', p 
f = Foo()
@debug
def bar(x, y=True):
    return x
bar(3)<jupyter_output>CALLING bar(x=3, y=True)
{'fraction_non_alphanumeric': 0.14386565408065033, 'fraction_numerical': 0.023478446892715797, 'mean_word_length': 2.4899216125419934, 'pattern_counts': {'":': 0, '<': 28, '<?xml version=': 0, '>': 55, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '6', 'dir': 'jupyter-structured-clean-dedup', 'id': '157138', 'license': 'MIT', 'n_tokens_mistral': 6694, 'n_tokens_neox': 6380, 'n_words': 2018, 'path': 'notebooks/scratch_typecheck.ipynb', 'repo_name': 'hdmamin/htools', 'size': '18698'}