| """ |
| 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): |
| |
| |
| return decor(args[0]) |
| else: |
| |
| 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] |
|
|
| |
| @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) |
|
|
|
|
| |
| |
| |
|
|
|
|
| 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] |
| 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) |
|
|
| |
| |
| |
| |
| cls = type(self) |
| try: |
| |
| with cls(): |
| response = gen.send(None) |
|
|
| while True: |
| try: |
| |
| request = yield response |
|
|
| except GeneratorExit: |
| |
| with cls(): |
| gen.close() |
| raise |
|
|
| except BaseException: |
| |
| with cls(): |
| response = gen.throw(*sys.exc_info()) |
|
|
| else: |
| |
| with cls(): |
| response = gen.send(request) |
|
|
| |
| |
| except StopIteration as e: |
| |
| |
| |
| 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 |
|
|