139 lines
4.2 KiB
Python
139 lines
4.2 KiB
Python
import sys
|
|
|
|
from packaging import version as _version
|
|
|
|
|
|
def _check_version(actver, version, cmp_op):
|
|
"""
|
|
Check version string of an active module against a required version.
|
|
|
|
If dev/prerelease tags result in TypeError for string-number comparison,
|
|
it is assumed that the dependency is satisfied.
|
|
Users on dev branches are responsible for keeping their own packages up to
|
|
date.
|
|
"""
|
|
try:
|
|
if cmp_op == '>':
|
|
return _version.parse(actver) > _version.parse(version)
|
|
elif cmp_op == '>=':
|
|
return _version.parse(actver) >= _version.parse(version)
|
|
elif cmp_op == '=':
|
|
return _version.parse(actver) == _version.parse(version)
|
|
elif cmp_op == '<':
|
|
return _version.parse(actver) < _version.parse(version)
|
|
else:
|
|
return False
|
|
except TypeError:
|
|
return True
|
|
|
|
|
|
def get_module_version(module_name):
|
|
"""Return module version or None if version can't be retrieved."""
|
|
mod = __import__(module_name, fromlist=[module_name.rpartition('.')[-1]])
|
|
return getattr(mod, '__version__', getattr(mod, 'VERSION', None))
|
|
|
|
|
|
def is_installed(name, version=None):
|
|
"""Test if *name* is installed.
|
|
|
|
Parameters
|
|
----------
|
|
name : str
|
|
Name of module or "python"
|
|
version : str, optional
|
|
Version string to test against.
|
|
If version is not None, checking version
|
|
(must have an attribute named '__version__' or 'VERSION')
|
|
Version may start with =, >=, > or < to specify the exact requirement
|
|
|
|
Returns
|
|
-------
|
|
out : bool
|
|
True if `name` is installed matching the optional version.
|
|
"""
|
|
if name.lower() == 'python':
|
|
actver = sys.version[:6]
|
|
else:
|
|
try:
|
|
actver = get_module_version(name)
|
|
except ImportError:
|
|
return False
|
|
if version is None:
|
|
return True
|
|
else:
|
|
# since version_requirements is in the critical import path,
|
|
# we lazy import re
|
|
import re
|
|
|
|
match = re.search('[0-9]', version)
|
|
assert match is not None, "Invalid version number"
|
|
symb = version[: match.start()]
|
|
if not symb:
|
|
symb = '='
|
|
assert symb in ('>=', '>', '=', '<'), f"Invalid version condition '{symb}'"
|
|
version = version[match.start() :]
|
|
return _check_version(actver, version, symb)
|
|
|
|
|
|
def require(name, version=None):
|
|
"""Return decorator that forces a requirement for a function or class.
|
|
|
|
Parameters
|
|
----------
|
|
name : str
|
|
Name of module or "python".
|
|
version : str, optional
|
|
Version string to test against.
|
|
If version is not None, checking version
|
|
(must have an attribute named '__version__' or 'VERSION')
|
|
Version may start with =, >=, > or < to specify the exact requirement
|
|
|
|
Returns
|
|
-------
|
|
func : function
|
|
A decorator that raises an ImportError if a function is run
|
|
in the absence of the input dependency.
|
|
"""
|
|
# since version_requirements is in the critical import path, we lazy import
|
|
# functools
|
|
import functools
|
|
|
|
def decorator(obj):
|
|
@functools.wraps(obj)
|
|
def func_wrapped(*args, **kwargs):
|
|
if is_installed(name, version):
|
|
return obj(*args, **kwargs)
|
|
else:
|
|
msg = f'"{obj}" in "{obj.__module__}" requires "{name}'
|
|
if version is not None:
|
|
msg += f" {version}"
|
|
raise ImportError(msg + '"')
|
|
|
|
return func_wrapped
|
|
|
|
return decorator
|
|
|
|
|
|
def get_module(module_name, version=None):
|
|
"""Return a module object of name *module_name* if installed.
|
|
|
|
Parameters
|
|
----------
|
|
module_name : str
|
|
Name of module.
|
|
version : str, optional
|
|
Version string to test against.
|
|
If version is not None, checking version
|
|
(must have an attribute named '__version__' or 'VERSION')
|
|
Version may start with =, >=, > or < to specify the exact requirement
|
|
|
|
Returns
|
|
-------
|
|
mod : module or None
|
|
Module if *module_name* is installed matching the optional version
|
|
or None otherwise.
|
|
"""
|
|
if not is_installed(module_name, version):
|
|
return None
|
|
return __import__(module_name, fromlist=[module_name.rpartition('.')[-1]])
|