diff --git a/lvsfunc/__init__.py b/lvsfunc/__init__.py index 474001d..a204f4e 100644 --- a/lvsfunc/__init__.py +++ b/lvsfunc/__init__.py @@ -13,6 +13,7 @@ presets, util) from .comparison import * from .deblock import * +from .dependency import * from .exceptions import * from .export import * from .grain import * diff --git a/lvsfunc/dependency/__init__.py b/lvsfunc/dependency/__init__.py new file mode 100644 index 0000000..dd7ff6d --- /dev/null +++ b/lvsfunc/dependency/__init__.py @@ -0,0 +1,7 @@ +# flake8: noqa + +from .exceptions import * +from .function import * +from .packages import * +from .plugin import * +from .types import * diff --git a/lvsfunc/dependency/exceptions.py b/lvsfunc/dependency/exceptions.py new file mode 100644 index 0000000..441e348 --- /dev/null +++ b/lvsfunc/dependency/exceptions.py @@ -0,0 +1,77 @@ +from typing import Any + +from vstools import CustomError, FuncExceptT, SupportsString + +__all__: list[str] = [ + 'CustomDependencyError', + 'MissingPluginsError', 'MissingPluginFunctionsError', + 'MissingPackagesError' +] + + +class CustomDependencyError(CustomError, ImportError): + """Raised when there's a general dependency error.""" + + def __init__( + self, func: FuncExceptT, deps: str | list[str] | ImportError, + message: SupportsString = "Missing dependencies: '{deps}'!", + **kwargs: Any + ) -> None: + """ + :param func: Function this error was raised from. + :param deps: Either the raised error or the names of the missing package. + :param message: Custom error message. + """ + + super().__init__(message, func, deps=deps, **kwargs) + + +class MissingPluginsError(CustomDependencyError): + """Raised when there's missing plugins.""" + + def __init__( + self, func: FuncExceptT, plugins: str | list[str] | ImportError, + message: SupportsString = "Missing plugins '{deps}'!", + **kwargs: Any + ) -> None: + if isinstance(plugins, list) and len(plugins) == 1: + if isinstance(message, str): + message = message.replace("plugins", "plugin") + + plugins = plugins[0] + + super().__init__(func, plugins, message, **kwargs) + + +class MissingPluginFunctionsError(CustomDependencyError): + """Raised when a plugin is missing functions.""" + + def __init__( + self, func: FuncExceptT, plugin: str, functions: str | list[str], + message: SupportsString = "'{plugin}' plugin is missing functions '{deps}'!", + **kwargs: Any + ) -> None: + if isinstance(functions, list) and len(functions) == 1: + if isinstance(message, str): + message = message.replace("functions", "function") + + functions = functions[0] + + super().__init__(func, functions, message, plugin=plugin, **kwargs) + + +class MissingPackagesError(CustomDependencyError): + """Raised when there's missing packages.""" + + def __init__( + self, func: FuncExceptT, packages: str | list[str] | ImportError, + message: SupportsString = "Missing packages '{deps}'!", + **kwargs: Any + ) -> None: + if isinstance(packages, list) and len(packages) == 1: + if isinstance(message, str): + message = message.replace("packages", "package") + + packages = packages[0] + + super().__init__(func, packages, message, **kwargs) diff --git a/lvsfunc/dependency/function.py b/lvsfunc/dependency/function.py new file mode 100644 index 0000000..e9acdb3 --- /dev/null +++ b/lvsfunc/dependency/function.py @@ -0,0 +1,104 @@ +from functools import wraps +from typing import Any, Callable + +from vstools import FuncExceptT, core + +from .plugin import check_installed_plugins +from .exceptions import MissingPluginFunctionsError +from .types import F + +__all__: list[str] = [ + 'check_installed_plugin_functions', + 'required_plugin_functions' +] + + +def check_installed_plugin_functions( + plugin: str, + functions: str | list[str] = [], + strict: bool = True, + func_except: FuncExceptT | None = None +) -> list[str]: + """ + Check if the given plugins are installed. + + Example usage: + + .. code-block:: python + + >>> check_installed_plugin_functions('descale', ['Bicubic', 'Debicubic']) + + >>> if check_installed_plugins('descale', ['Bicubic', 'Debicubic'], strict=False): + ... print('Missing functions for plugin! Please update!') + + :param plugin: The plugin to check. + :param plugins: A list of functions to check for. + :param strict: If True, raises an error if any of the plugins are missing. + Default: True. + :param func_except: Function returned for custom error handling. + This should only be set by VS package developers. + + :return: A list of all missing functions if strict=False, else raises an error. + """ + + if not functions: + return list[str]() + + func = func_except or check_installed_plugin_functions + + check_installed_plugins(plugin, True, func) + + if isinstance(functions, str): + functions = [functions] + + plg = getattr(core, plugin) # type:ignore + + missing = [ + plugin_func for plugin_func in functions if not hasattr(plg, plugin_func) + ] + + if not missing or not strict: + return missing + + raise MissingPluginFunctionsError(func, plugin, missing, reason=f"{strict=}") + + +def required_plugin_functions( + plugin: str, functions: list[str] = [], + func_except: FuncExceptT | None = None +) -> Callable[[F], F]: + """ + Decorator to ensure that the specified plugin has specific functions. + + The plugin and list of functions will be stored in the function's `required_plugin_functions` attribute. + + Example usage: + + .. code-block:: python + + >>> @required_plugin_functions('descale', ['Bicubic', 'Debicubic']) + >>> def func(clip: vs.VideoNode) -> vs.VideoNode: + ... return clip + + >>> print(func.required_plugin_functions) + ... ('descale', ['Bicubic', 'Debicubic']) + + For more information, see :py:func:`check_installed_plugin_functions`. + + :param plugin: The plugin to check. + :param functions: A list of functions to check for. + :param func_except: Function returned for custom error handling. + This should only be set by VS package developers. + """ + + def decorator(func: F) -> F: + @wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> Any: + check_installed_plugin_functions(plugin, functions, True, func_except or func) + func.required_plugin_functions = (plugin, functions) # type:ignore + + return func(*args, **kwargs) + + return wrapper # type:ignore + + return decorator diff --git a/lvsfunc/dependency/packages.py b/lvsfunc/dependency/packages.py new file mode 100644 index 0000000..d8affc9 --- /dev/null +++ b/lvsfunc/dependency/packages.py @@ -0,0 +1,106 @@ +from functools import wraps +from typing import Any, Callable + +from vstools import FuncExceptT + +from .exceptions import MissingPackagesError +from .types import DEP_URL, F + +__all__: list[str] = [ + 'check_installed_packages', + 'required_packages' +] + + +def check_installed_packages( + packages: str | list[str] | dict[str, DEP_URL] = [], + strict: bool = True, + func_except: FuncExceptT | None = None +) -> list[str]: + """ + Check if the given packages are installed. + + Example usage: + + .. code-block:: python + + >>> check_installed_packages(['lvsfunc', 'vstools']) + + >>> check_installed_packages({'lvsfunc': 'pip install lvsfunc'}) + + >>> if check_installed_packages(['lvsfunc', 'vstools'], strict=False): + ... print('Missing packages!') + + :param packages: A list of packages to check for. If a dict is passed, + the values are treated as either a URL or a pip command to download the package. + :param strict: If True, raises an error if any of the packages are missing. + Default: True. + :param func_except: Function returned for custom error handling. + This should only be set by VS package developers. + + :return: A list of all missing packages if strict=False, else raises an error. + """ + + if not packages: + return list[str]() + + if isinstance(packages, str): + packages = [packages] + + missing = list[str]() + + for pkg in (packages.keys() if isinstance(packages, dict) else packages): + try: + __import__(pkg) + except ImportError: + missing.append(f"{pkg} ({packages[pkg]})" if isinstance(packages, dict) else pkg) + + if not missing or not strict: + return missing + + raise MissingPackagesError(func_except or check_installed_packages, missing, reason=f"{strict=}") + + +def required_packages( + packages: list[str] | dict[str, DEP_URL] = [], + func_except: FuncExceptT | None = None +) -> Callable[[F], F]: + """ + Decorator to ensure that specified packages are installed. + + The list of packages will be stored in the function's `required_packages` attribute. + + Example usage: + + .. code-block:: python + + >>> @required_packages(['lvsfunc', 'vstools']) + >>> def func(clip: vs.VideoNode) -> vs.VideoNode: + ... return clip + + >>> print(func.required_packages) + ... ['lvsfunc', 'vstools'] + + >>> @required_packages({'lvsfunc': 'pip install lvsfunc'}) + >>> def func(clip: vs.VideoNode) -> vs.VideoNode: + ... return clip + + For more information, see :py:func:`check_installed_packages`. + + :param packages: A list of packages to check for. If a dict is passed, + the values are treated as either a URL or a pip command to download the package. + :param func_except: Function returned for custom error handling. + This should only be set by VS package developers. + """ + + def decorator(func: F) -> F: + @wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> Any: + check_installed_packages(packages, True, func_except or func) + func.required_packages = packages # type:ignore + + return func(*args, **kwargs) + + return wrapper # type:ignore + + return decorator diff --git a/lvsfunc/dependency/plugin.py b/lvsfunc/dependency/plugin.py new file mode 100644 index 0000000..4ac2ba9 --- /dev/null +++ b/lvsfunc/dependency/plugin.py @@ -0,0 +1,104 @@ +from functools import wraps +from typing import Any, Callable + +from vstools import FuncExceptT, core + +from .exceptions import MissingPluginsError +from .types import DEP_URL, F + +__all__: list[str] = [ + 'check_installed_plugins', + 'required_plugins' +] + + +def check_installed_plugins( + plugins: str | list[str] | dict[str, DEP_URL] = [], + strict: bool = True, + func_except: FuncExceptT | None = None +) -> list[str]: + """ + Check if the given plugins are installed. + + Example usage: + + .. code-block:: python + + >>> check_installed_plugins(['resize', 'descale']) + + >>> check_installed_plugins({'descale': 'https://github.com/Jaded-Encoding-Thaumaturgy/vapoursynth-descale'}) + + >>> if check_installed_plugins(['resize', 'descale'], strict=False): + ... print('Missing plugins!') + + :param plugins: A list of plugins to check for. If a dict is passed, + the values are treated as URLs to download the plugin. + :param strict: If True, raises an error if any of the plugins are missing. + Default: True. + :param func_except: Function returned for custom error handling. + This should only be set by VS package developers. + + :return: A list of all missing plugins if strict=False, else raises an error. + """ + + if not plugins: + return list[str]() + + if isinstance(plugins, str): + plugins = [plugins] + + missing = [ + f"{plugin} ({plugins[plugin]})" if isinstance(plugins, dict) else plugin + for plugin in (plugins.keys() if isinstance(plugins, dict) else plugins) + if not hasattr(core, plugin) + ] + + if not missing or not strict: + return missing + + raise MissingPluginsError(func_except or check_installed_plugins, missing, reason=f"{strict=}") + + +def required_plugins( + plugins: list[str] | dict[str, DEP_URL] = [], + func_except: FuncExceptT | None = None +) -> Callable[[F], F]: + """ + Decorator to ensure that specified plugins are installed. + + The list of plugins will be stored in the function's `required_plugins` attribute. + + Example usage: + + .. code-block:: python + + >>> @required_plugins(['resize', 'descale']) + >>> def func(clip: vs.VideoNode) -> vs.VideoNode: + ... return clip + + >>> print(func.required_plugins) + ... ['resize', 'descale'] + + >>> @required_plugins({'descale': 'https://github.com/Jaded-Encoding-Thaumaturgy/vapoursynth-descale'}) + >>> def func(clip: vs.VideoNode) -> vs.VideoNode: + ... return clip + + For more information, see :py:func:`check_installed_plugins`. + + :param plugins: A list of plugins to check for. If a dict is passed, + the values are treated as URLs to download the plugin. + :param func_except: Function returned for custom error handling. + This should only be set by VS package developers. + """ + + def decorator(func: F) -> F: + @wraps(func) + def wrapper(*args: Any, **kwargs: Any) -> Any: + check_installed_plugins(plugins, True, func_except or func) + func.required_plugins = plugins # type:ignore + + return func(*args, **kwargs) + + return wrapper # type:ignore + + return decorator diff --git a/lvsfunc/dependency/types.py b/lvsfunc/dependency/types.py new file mode 100644 index 0000000..db6bb0a --- /dev/null +++ b/lvsfunc/dependency/types.py @@ -0,0 +1,13 @@ +from typing import Any, Callable, TypeVar + +__all__ = [ + 'DEP_URL', + 'F', +] + + +DEP_URL = str +"""A string representing a URL to download a dependency from.""" + +F = TypeVar('F', bound=Callable[..., Any]) +"""Generic type variable for the function""" diff --git a/setup.py b/setup.py index 57dd9a4..aec48a2 100755 --- a/setup.py +++ b/setup.py @@ -24,6 +24,7 @@ packages=[ package_name, f"{package_name}.packets", + f"{package_name}.dependency", ], package_data={ package_name: ['py.typed'],