Skip to content

Commit

Permalink
Add dependency check plugins/plugin functions/decorators (#168)
Browse files Browse the repository at this point in the history
* Add dependency check plugins/decorators

* Fix dep exceptions str() failed

* Add reason to exceptions

* Ensure decorators always receive strict=True

* Ensure always return a list if strict=False

This makes it easier for the results to be used for if/else statements

* Export submodule properly

* Add str input support

* Replace exception func name to passed func

* Minor docstring changes

* plugins -> packages

* Add required_packages/plugins attribute to function

* Simplify package check logic

* Add plugin function check

* Remove unused import

* Docstring updates
  • Loading branch information
LightArrowsEXE committed Aug 21, 2024
1 parent b0d1ad2 commit 9a45c77
Show file tree
Hide file tree
Showing 8 changed files with 413 additions and 0 deletions.
1 change: 1 addition & 0 deletions lvsfunc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
presets, util)
from .comparison import *
from .deblock import *
from .dependency import *
from .exceptions import *
from .export import *
from .grain import *
Expand Down
7 changes: 7 additions & 0 deletions lvsfunc/dependency/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# flake8: noqa

from .exceptions import *
from .function import *
from .packages import *
from .plugin import *
from .types import *
77 changes: 77 additions & 0 deletions lvsfunc/dependency/exceptions.py
Original file line number Diff line number Diff line change
@@ -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)
104 changes: 104 additions & 0 deletions lvsfunc/dependency/function.py
Original file line number Diff line number Diff line change
@@ -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
106 changes: 106 additions & 0 deletions lvsfunc/dependency/packages.py
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 9a45c77

Please sign in to comment.