""" Inspect, meta, etc. """ from __future__ import annotations import functools import inspect import pprint import sys import types from typing import Any, Dict, Literal import warnings from ..data_structure.tree_utils import is_mapping, is_sequence def state_dict_class(keys: list[str]): """ Just like pytorch nn.Module Add the following methods to the class: state_dict() -> dict of attribute keys load_state_dict(sdict) restore states """ def _wrap_class(cls): assert inspect.isclass(cls) def state_dict(self): return {k: getattr(self, k) for k in keys} def load_state_dict(self, states: Dict[str, Any]): if not set(keys).issubset(set(states.keys())): raise ValueError(f"states does not have all the required keys: {keys}") for k in keys: setattr(self, k, states[k]) @property def state_keys(self): return keys cls.state_dict = state_dict cls.load_state_dict = load_state_dict cls.state_keys = state_keys return cls return _wrap_class def implements_method(object, method: str): """ Returns: True if object implements a method """ return hasattr(object, method) and callable(getattr(object, method)) def assert_implements_method(object, method: str | list[str]): if isinstance(method, str): method = [method] for m in method: assert implements_method(object, m), ( f"object {object.__class__} does not " f"implement method {m}()" ) def meta_decorator(decor): """ a decorator, allowing the wrapped decorator to be used as: @decorator(*args, **kwargs) def callable() -- or -- @decorator # without parenthesis, args and kwargs will use default def callable() Args: decor: a decorator whose first argument is a callable (function or class to be decorated), and the rest of the arguments can be omitted as default. decor(f, ... the other arguments must have default values) Warning: decor can NOT be a function that receives a single, callable argument. See stackoverflow: http://goo.gl/UEYbDB """ import functools def single_callable(args, kwargs): return len(args) == 1 and len(kwargs) == 0 and callable(args[0]) @functools.wraps(decor) def new_decor(*args, **kwargs): if single_callable(args, kwargs): # this is the double-decorated f. # It should not run on a single callable. return decor(args[0]) else: # decorator arguments return lambda real_f: decor(real_f, *args, **kwargs) return new_decor @meta_decorator def make_recursive_func(fn, *, with_path=False): """ Decorator that turns a function that works on a single array/tensor to working on arbitrary nested structures. """ import functools import tree @functools.wraps(fn) def _wrapper(tensor_struct, *args, **kwargs): if with_path: return tree.map_structure_with_path( lambda paths, x: fn(paths, x, *args, **kwargs), tensor_struct ) else: return tree.map_structure(lambda x: fn(x, *args, **kwargs), tensor_struct) return _wrapper @meta_decorator def deprecated(func, msg="", action="warning", type=""): """ Function/class decorator: designate deprecation. Args: msg: string message. action: string mode - 'warning': (default) prints `msg` to stderr - 'noop': do nothing, just for source code annotation purposes - 'raise': raise DeprecatedError(`msg`) """ action = action.lower() type = type.lower() ALL_ACTIONS = ["warn", "warning", "noop", "raise"] if action not in ALL_ACTIONS: raise ValueError(f"Unknown action {action}. Choose from {ALL_ACTIONS}.") ALL_TYPES = { "": DeprecationWarning, "pending": PendingDeprecationWarning, "future": FutureWarning, } if type not in ALL_TYPES: raise ValueError(f"Unknown type {type}. Choose from {ALL_TYPES.keys()}.") if not msg: msg = "This is a deprecated feature." WarningExceptionCls = ALL_TYPES[type] # only does the deprecation when being called @functools.wraps(func) def _deprecated(*args, **kwargs): if action in ["warning", "warn"]: warnings.warn(msg, WarningExceptionCls) elif action == "raise": raise WarningExceptionCls(msg) return func(*args, **kwargs) return _deprecated @meta_decorator def call_once(func, on_second_call: Literal["noop", "raise", "warn"] = "noop"): """ Decorator to ensure that a function is only called once. Args: on_second_call (str): what happens when the function is called a second time. """ assert on_second_call in [ "noop", "raise", "warn", ], "mode must be one of 'noop', 'raise', 'warn'" @functools.wraps(func) def wrapper(*args, **kwargs): if wrapper._called: if on_second_call == "raise": raise RuntimeError(f"{func.__name__} has already been called. Can only call once.") elif on_second_call == "warn": warnings.warn(f"{func.__name__} has already been called. Should only call once.") else: wrapper._called = True return func(*args, **kwargs) wrapper._called = False return wrapper class NoopObject: """ Object that does nothing when called any method """ def __init__(self, *args, **kwargs): self.init_args = args self.init_kwargs = kwargs def __getattr__(self, name): def _func(*args, **kwargs): pass return _func class NoopContext: """ Placeholder context manager that does nothing. We could have written simply as: @contextmanager def noop_context(*args, **kwargs): yield but the returned context manager cannot be called twice, i.e. my_noop = NoopContext() with my_noop: do1() with my_noop: # trigger generator error do2() """ def __init__(self, *args, **kwargs): self.args = args self.kwargs = kwargs def __enter__(self): return self def __exit__(self, exc_type, exc_val, exc_tb): pass def make_registry_metaclass(class_name): """ Usage: TrainerRegistry = make_registry_metaclass('TrainerRegistry') class BaseTrainer(metaclass=TrainerRegistry): pass class MyTrainer(BaseTrainer): pass TrainerRegistry['MyTrainer'] -> MyTrainer class # syntax enabled by metaclass TrainerRegistry.get_class('MyTrainer') # same as above TrainerRegistry.registry -> full dict of {name: trainer_class} Templated definition: class TrainerRegistry(type): registry = {} def __new__(cls, name, bases, attr): new_cls = super().__new__(cls, name, bases, attr) TrainerRegistry.registry[name] = new_cls return new_cls def get_class(cls, name): if name not in cls.registry: raise KeyError( f"Trainer class {name} not found in registry: " f"{pprint.pformat(cls.registry)}" ) return cls.registry[name]""" def new__(cls, name, bases, attr): """ Change the attr dict to dynamically add methods and attributes """ new_cls = type.__new__(cls, name, bases, attr) cls.registry[name] = new_cls return new_cls def get_class(cls, name): if name not in cls.registry: existing_cls = list(cls.registry.keys()) raise KeyError(f"{class_name} class '{name}' not found in registry: {existing_cls}") return cls.registry[name] def instantiate(cls_, cls, **kwargs): Cls = cls_.get_class(cls) return Cls(**kwargs) class _BracketOperator(type): def __getitem__(cls, name): return get_class(cls, name) return types.new_class( class_name, bases=(type,), kwds={"metaclass": _BracketOperator}, exec_body=lambda ns: ns.update( { "registry": {}, "__new__": new__, "get_class": classmethod(get_class), "instantiate": classmethod(instantiate), } ), ) class ClassRegistry: """ May be a preferred way over make_registry_metaclass if your code does not support metaclass well, e.g. pickle or Ray Use in conjunction with `__init_subclass__` hook in your base class class BaseClass: registry = ClassRegistry() def __init_subclass__(cls, **kwargs): cls.registry.add(cls) super().__init_subclass__(**kwargs) print(registry) """ def __init__(self, base_class_name: str = None): self.registry = {} self._base_class_name = base_class_name def add(self, cls): self.registry[cls.__name__] = cls def get(self, name): if name not in self.registry: existing_cls = list(self.registry.keys()) base_name = self._base_class_name + " " if self._base_class_name else "" raise KeyError(f"{base_name} subclass '{name}' not found in registry: {existing_cls}") return self.registry[name] def __str__(self): return pprint.pformat(self.registry) def __getitem__(self, name): return self.get(name) def instantiate(self, cls, **kwargs): return self.get(cls)(**kwargs) # ======================================================== # =================== Inspect utils ==================== # ======================================================== def func_parameters(func): return inspect.signature(func).parameters def func_has_arg(func, arg_name): return arg_name in func_parameters(func) def pack_varargs(args): """ Pack *args or a single list arg as list def f(*args): arg_list = pack_varargs(args) # arg_list is now packed as a list """ assert isinstance(args, tuple), "please input the tuple `args` as in *args" if len(args) == 1 and is_sequence(args[0]): return args[0] else: return args def enable_list_arg(func): """ Function decorator. If a function only accepts varargs (*args), make it support a single list arg as well """ @functools.wraps(func) def wrapper(*args, **kwargs): args = pack_varargs(args) return func(*args, **kwargs) return wrapper def enable_varargs(func): """ Function decorator. If a function only accepts a list arg, make it support varargs as well """ @functools.wraps(func) def wrapper(*args, **kwargs): args = pack_varargs(args) return func(args, **kwargs) return wrapper def pack_kwargs(args, kwargs): """ Pack **kwargs or a single dict arg as dict def f(*args, **kwargs): kwdict = pack_kwargs(args, kwargs) # kwdict is now packed as a dict """ if len(args) == 1 and is_mapping(args[0]): assert not kwargs, "cannot have both **kwargs and a dict arg" return args[0] # single-dict else: assert not args, "cannot have positional args if **kwargs exist" return kwargs def merge_kwargs(args, kwargs) -> Dict: """ Merge all dicts in `args` and keywords in kwargs. E.g. merge_kwargs({"a.b": 1, "a.c": 2}, foo=6, bar=8) -> {"a.b": 1, "a.c": 2, "foo": 6, "bar": 8} """ kw_all = {} for arg in args: assert is_mapping(arg), f"{arg} is not a dict." kw_all.update(arg) kw_all.update(kwargs) return kw_all def enable_dict_arg(func): """ Function decorator. If a function only accepts varargs (*args), make it support a single list arg as well """ @functools.wraps(func) def wrapper(*args, **kwargs): kwargs = pack_kwargs(args, kwargs) return func(**kwargs) return wrapper def enable_kwargs(func): """ Function decorator. If a function only accepts a dict arg, make it support kwargs as well """ @functools.wraps(func) def wrapper(*args, **kwargs): kwargs = pack_kwargs(args, kwargs) return func(kwargs) return wrapper def has_keys(D, keys: list): assert is_mapping(D) return all(key in D for key in keys) def assert_has_keys(D, keys: list): assert is_mapping(D), "Input is not a dict" for key in keys: if key not in D: raise KeyError(f'Required key "{key}" is missing in dict {D}') return True def method_decorator(decorator): """ Decorator of decorator: transform a decorator that only works on normal functions to a decorator that works on class methods From Django form: https://goo.gl/XLjxKK """ @functools.wraps(decorator) def wrapped_decorator(method): @functools.wraps(method) def wrapper(self, *args, **kwargs): def bound_func(*args2, **kwargs2): return method(self, *args2, **kwargs2) return decorator(bound_func)(*args, **kwargs) return wrapper return wrapped_decorator def accepts_varargs(func): """ If a function accepts *args """ params = inspect.signature(func).parameters return any(param.kind == inspect.Parameter.VAR_POSITIONAL for param in params.values()) def accepts_kwargs(func): """ If a function accepts **kwargs """ params = inspect.signature(func).parameters return any(param.kind == inspect.Parameter.VAR_KEYWORD for param in params.values()) def is_signature_compatible(func, *args, **kwargs): sig = inspect.signature(func) try: sig.bind(*args, **kwargs) return True except TypeError: return False def make_list(x): """ Turns a singleton object to a list. If already a list, no change. """ if is_sequence(x): return x else: return [x] def make_tuple(elem, repeats): """ E.g. expand a singleton x into (x, x, x) useful for things like image_size or kernal, which can be a single int/float or a tuple of fixed size """ if is_sequence(elem): assert len(elem) == repeats, f"length of input must be {repeats}: {elem}" return elem else: return (elem,) * repeats def accumulate(iterable, fn=lambda x, y: x + y): """ Return running totals # _accumulate([1,2,3,4,5]) --> 1 3 6 10 15 # _accumulate([1,2,3,4,5], operator.mul) --> 1 2 6 24 120 """ it = iter(iterable) try: total = next(it) except StopIteration: return yield total for element in it: total = fn(total, element) yield total class DecoratorContextManager: """ Allow a context manager to be used as a decorator From torch.auto_grad.grad_mode """ def __call__(self, func): if inspect.isgeneratorfunction(func): return self._wrap_generator(func) @functools.wraps(func) def decorate_context(*args, **kwargs): with self.__class__(): return func(*args, **kwargs) return decorate_context def _wrap_generator(self, func): """Wrap each generator invocation with the context manager""" @functools.wraps(func) def generator_context(*args, **kwargs): gen = func(*args, **kwargs) # Generators are suspended and unsuspended at `yield`, hence we # make sure the grad mode is properly set every time the execution # flow returns into the wrapped generator and restored when it # returns through our `yield` to our caller (see PR #49017). cls = type(self) try: # Issuing `None` to a generator fires it up with cls(): response = gen.send(None) while True: try: # Forward the response to our caller and get its next request request = yield response except GeneratorExit: # Inform the still active generator about its imminent closure with cls(): gen.close() raise except BaseException: # Propagate the exception thrown at us by the caller with cls(): response = gen.throw(*sys.exc_info()) else: # Pass the last request to the generator and get its response with cls(): response = gen.send(request) # We let the exceptions raised above by the generator's `.throw` or # `.send` methods bubble up to our caller, except for StopIteration except StopIteration as e: # The generator informed us that it is done: take whatever its # returned value (if any) was and indicate that we're done too # by returning it (see docs for python's return-statement). return e.value return generator_context def __enter__(self) -> None: raise NotImplementedError def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: raise NotImplementedError