-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathdeps.py
More file actions
114 lines (95 loc) · 4.09 KB
/
deps.py
File metadata and controls
114 lines (95 loc) · 4.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import importlib
import importlib.metadata
import os
import click
def _pip_command():
# Check OS to determine pip vs pip3
if os.name == "nt":
return "pip"
# Avoid potential issues with multiple python (2/3) versions on Unix/Linux systems
else:
return "pip3"
def requires(*requirements):
"""
Decorator that ensures required Python modules are installed and meet optional minimum version constraints.
Parameters:
*requirements: One or more dictionaries describing a module requirement.
Each dictionary may contain the following keys:
- module (str): The importable module name (e.g., "requests").
- package (str, optional): The name of the package to install via pip.
Use this if the pip install name differs from the import name
(e.g., module="cwms", package="cwms-python").
- version (str, optional): A minimum required version string (e.g., "2.30.0").
- desc (str, optional): A short description of what the module is or why it's needed.
Included in the error message to help users understand the dependency.
- link (str, optional): A URL pointing to documentation or the package's homepage.
Example:
@requires(
{
"module": "cwms",
"package": "cwms-python",
"version": "1.0.1",
"desc": "CWMS REST API Python client",
"link": "https://github.com/hydrologicengineeringcenter/cwms-python"
},
{
"module": "requests",
"version": "2.30.0",
"desc": "Required for HTTP API access"
}
)
"""
def decorator(func):
def wrapper(*args, **kwargs):
missing = []
version_issues = []
for req in requirements:
mod = req["module"]
pkg = req.get("package", mod)
min_version = req.get("version")
desc = req.get("desc")
link = req.get("link")
# Check if the provided requirement is already imported
try:
importlib.import_module(mod)
except ImportError:
msg = f"- `{mod}` (install: `{pkg}`)"
if desc:
msg += f" — {desc}"
if link:
msg += f" [docs]({link})"
missing.append((msg, pkg))
continue
# Confirm the minimum version is met
if min_version:
try:
actual_version = importlib.metadata.version(pkg)
if actual_version < min_version:
version_issues.append(
f"- `{pkg}` version `{actual_version}` found, "
f"but `{min_version}` or higher is required"
)
except importlib.metadata.PackageNotFoundError:
version_issues.append(
f"- `{pkg}` is installed but version could not be verified"
)
# Build out the error response
if missing or version_issues:
error_lines = []
if missing:
error_lines.append("Missing module(s):")
for msg, _ in missing:
error_lines.append(msg)
install_cmd = f"{_pip_command()} install " + " ".join(
pkg for _, pkg in missing
)
error_lines.append(
f"\nInstall missing packages:\n {install_cmd}"
)
if version_issues:
error_lines.append("\nVersion issues:")
error_lines.extend(version_issues)
raise click.ClickException("\n".join(error_lines))
return func(*args, **kwargs)
return wrapper
return decorator