-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add dependency check plugins/plugin functions/decorators (#168)
* 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
1 parent
b0d1ad2
commit 9a45c77
Showing
8 changed files
with
413 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 * |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.