# # SPDX-FileCopyrightText: Copyright (c) 1993-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # import inspect import sys import warnings from textwrap import dedent import polygraphy from polygraphy import config from polygraphy.logger import G_LOGGER from polygraphy.mod.util import version def _add_to_all(symbol, module): if hasattr(module, "__all__"): module.__all__.append(symbol) else: module.__all__ = [symbol] def _define_in_module(name, symbol, module): assert name not in vars(module), "This symbol is already defined!" vars(module)[name] = symbol _add_to_all(name, module) def export(funcify=False, func_name=None): """ Decorator that exports a symbol into the ``__all__`` attribute of the caller's module. This makes the symbol visible in a ``*`` import (e.g. ``from module import *``) and hides other symbols unless they are also present in ``__all__``. Args: funcify (bool): Whether to create and export a function that will call a decorated Polygraphy loader. The decorated type *must* be a subclass of ``BaseLoader`` if ``funcify=True``. This is useful to provide convenient short-hands to immediately evaluate loaders. For example: :: @mod.export(funcify=True) class SuperCoolModelFromPath(BaseLoader): def __init__(self, init_params): ... def call_impl(self, call_params): ... # We can now magically access an immediately evaluated functional # variant of the loader: model = super_cool_model_from_path(init_params, call_params) # Which is equivalent to: load_model = SuperCoolModelFromPath(init_params) model = load_model(call_params) The signature of the generated function is a combination of the signatures of ``__init__`` and ``call_impl``. Specifically, parameters without defaults will precede those with defaults, and ``__init__`` parameters will precede ``call_impl`` parameters. Special parameters like ``*args`` and ``**kwargs`` will always be the last parameters in the generated signature if they are present in the loader method signatures. The return value(s) will always come from ``call_impl``. For example: :: # With __init__ signature: def __init__(a, b=0) -> None: # And call_impl signature: def call_impl(c, d=0) -> z: # The generated function will have a signature: def generated(a, c, b=0, d=0) -> z: func_name (str): If funcify is True, this controls the name of the generated function. By default, the exported function will use the same name as the loader, but ``snake_case`` instead of ``PascalCase``. """ module = inspect.getmodule(sys._getframe(1)) # Find a method by wallking the inheritance hierarchy of a type: def find_method(symbol, method): hierarchy = inspect.getmro(symbol) for ancestor in hierarchy: if method in vars(ancestor): return vars(ancestor)[method] assert ( False ), f"Could not find method: {method} in the inheritance hierarcy of: {symbol}" def export_impl(func_or_cls): _add_to_all(func_or_cls.__name__, module) if funcify: # We only support funcify-ing BaseLoaders, and only if __init__ and call_impl # have no overlapping parameters. from polygraphy.backend.base import BaseLoader assert inspect.isclass( func_or_cls ), "Decorated type must be a loader to use funcify=True" assert BaseLoader in inspect.getmro( func_or_cls ), "Decorated type must derive from BaseLoader to use funcify=True" def get_params(method): return list( inspect.signature( find_method(func_or_cls, method) ).parameters.values() )[1:] def is_variadic(param): return param.kind in [param.VAR_POSITIONAL, param.VAR_KEYWORD] def has_default(param): return param.default != param.empty def get_param_name(p): # For variadic arguments, p.name will drop the *, ** return str(p) if is_variadic(p) else p.name def param_names(params): return [get_param_name(p) for p in params] loader = func_or_cls init_params = get_params("__init__") call_impl_params = get_params("call_impl") assert ( set(param_names(call_impl_params)) - set(param_names(init_params)) ) == set( param_names(call_impl_params) ), "Cannot funcify a type where call_impl and __init__ have the same argument names!" # Dynamically generate a function with the right signature. # To generate the signature, we use the init and call_impl arguments, # but move required arguments (i.e. without default values) to the front. def build_arg_list(should_include): def str_from_param(p): return get_param_name(p) + ( f"={p.default}" if has_default(p) else "" ) arg_list = [str_from_param(p) for p in init_params if should_include(p)] arg_list += [ str_from_param(p) for p in call_impl_params if should_include(p) ] return arg_list non_default_args = build_arg_list( should_include=lambda p: not is_variadic(p) and not has_default(p) ) default_args = build_arg_list( should_include=lambda p: not is_variadic(p) and has_default(p) ) special_args = build_arg_list(should_include=is_variadic) signature = ", ".join(non_default_args + default_args + special_args) init_args = ", ".join(param_names(init_params)) call_impl_args = ", ".join(param_names(call_impl_params)) def pascal_to_snake(name): return "".join( f"_{c.lower()}" if c.isupper() else c for c in name ).lstrip("_") nonlocal func_name func_name = func_name or pascal_to_snake(loader.__name__) func_code = dedent( f""" def {func_name}({signature}): return loader_binding({init_args})({call_impl_args}) func_var = {func_name} """ ) new_locals = {} exec( func_code, # Need to bind the loader this way, or it won't be accesible from func_code. {"loader_binding": loader}, new_locals, ) func = new_locals["func_var"] # Next we setup the docstring so that it is a combination of the __init__ # and call_impl docstrings. func.__doc__ = f"Immediately evaluated functional variant of :class:`{loader.__name__}` .\n" def try_add_method_doc(method): call_impl = find_method(loader, method) if call_impl.__doc__: func.__doc__ += dedent(call_impl.__doc__) try_add_method_doc("__init__") try_add_method_doc("call_impl") # Now that the function has been defined, we just need to add it into the module's # __dict__ so it is accessible like a normal symbol. _define_in_module(func_name, func, module) # We don't actually want to modify the decorated object. return func_or_cls return export_impl def warn_deprecated( name, use_instead, remove_in, module_name=None, always_show_warning=False ): if version(polygraphy.__version__) >= version(remove_in): G_LOGGER.internal_error( f"{name} should have been removed in version: {remove_in}" ) full_obj_name = f"{module_name}.{name}" if module_name else name msg = ( f"{full_obj_name} is deprecated and will be removed in Polygraphy {remove_in}." ) if use_instead is not None: msg += f" Use {use_instead} instead." warnings.warn(msg, DeprecationWarning, stacklevel=3) if always_show_warning: G_LOGGER.warning(msg) def deprecate(remove_in, use_instead, module_name=None, name=None): """ Decorator that marks a function or class as deprecated. When the function or class is used, a warning will be issued. Args: remove_in (str): The version in which the decorated type will be removed. use_instead (str): The function or class to use instead. module_name (str): The name of the containing module. This will be used to generate more informative warnings. Defaults to None. name (str): The name of the object being deprecated. If not provided, this is automatically determined based on the decorated type. Defaults to None. """ def deprecate_impl(obj): if config.INTERNAL_CORRECTNESS_CHECKS and version( polygraphy.__version__ ) >= version(remove_in): G_LOGGER.internal_error( f"{obj} should have been removed in version: {remove_in}" ) nonlocal name name = name or obj.__name__ if inspect.ismodule(obj): class DeprecatedModule: def __getattr__(self, attr_name): warn_deprecated(name, use_instead, remove_in, module_name) self = obj return getattr(self, attr_name) def __setattr__(self, attr_name, value): warn_deprecated(name, use_instead, remove_in, module_name) self = obj return setattr(self, attr_name, value) DeprecatedModule.__doc__ = f"Deprecated: Use {use_instead} instead" return DeprecatedModule() elif inspect.isclass(obj): class Deprecated(obj): def __init__(self, *args, **kwargs): warn_deprecated(name, use_instead, remove_in, module_name) super().__init__(*args, **kwargs) Deprecated.__doc__ = f"Deprecated: Use {use_instead} instead" return Deprecated elif inspect.isfunction(obj): def wrapped(*args, **kwargs): warn_deprecated(name, use_instead, remove_in, module_name) return obj(*args, **kwargs) wrapped.__doc__ = f"Deprecated: Use {use_instead} instead" return wrapped else: G_LOGGER.internal_error(f"deprecate is not implemented for: {obj}") return deprecate_impl def export_deprecated_alias(name, remove_in, use_instead=None): """ Decorator that creates and exports a deprecated alias for the decorated class or function. The alias will behave like the decorated type, except it will issue a deprecation warning when used. To create a deprecated alias for an entire module, invoke the function manually within the module like so: :: mod.export_deprecated_alias("old_mod_name", remove_in="0.0.0")(sys.modules[__name__]) Args: name (str): The name of the deprecated alias. remove_in (str): The version, as a string, in which the deprecated alias will be removed. use_instead (str): The name of the function, class, or module to use instead. If this is ``None``, the new name will be automatically determined. Defaults to None. """ module = inspect.getmodule(sys._getframe(1)) def export_deprecated_alias_impl(obj): new_obj = deprecate( remove_in, use_instead=use_instead or obj.__name__, module_name=module.__name__, name=name, )(obj) _define_in_module(name, new_obj, module) return obj return export_deprecated_alias_impl