-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmetadata.py
More file actions
214 lines (156 loc) · 6.74 KB
/
metadata.py
File metadata and controls
214 lines (156 loc) · 6.74 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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
"""Metadata and derived configuration for the CLI."""
from __future__ import annotations
import re
import sys
from collections.abc import Mapping
from importlib.metadata import PackageNotFoundError, packages_distributions, version
from pathlib import Path
import click
import tomllib
PREFERRED_TOOL_METADATA_SECTION = "cli"
METADATA_TOOL_FIELDS = frozenset({"name", "cli_name", "env_prefix"})
DEFAULT_PACKAGE_NAME = "cli"
DEFAULT_APP_NAME = "CLI"
DEFAULT_COMMAND_NAME = "cli"
DEFAULT_ENV_PREFIX = "CLI_"
ENV_PREFIX_SUFFIX = "_"
CONSOLE_SCRIPT_TARGET = "cli.main:main"
PyprojectTable = Mapping[str, object]
def _pyproject_path() -> Path:
return Path(__file__).resolve().parents[3] / "pyproject.toml"
def _normalized_string(value: object) -> str | None:
if not isinstance(value, str):
return None
normalized = value.strip()
return normalized or None
def _load_pyproject(pyproject_path: Path | None = None) -> dict[str, object]:
path = pyproject_path or _pyproject_path()
try:
data = tomllib.loads(path.read_text(encoding="utf-8"))
except (OSError, tomllib.TOMLDecodeError):
return {}
if isinstance(data, dict) and all(isinstance(key, str) for key in data):
return data
return {}
PYPROJECT = _load_pyproject()
def project_table(pyproject: PyprojectTable = PYPROJECT) -> PyprojectTable:
project = pyproject.get("project")
if not isinstance(project, dict):
return {}
return project
def tool_tables(pyproject: PyprojectTable = PYPROJECT) -> dict[str, PyprojectTable]:
tool = pyproject.get("tool")
if not isinstance(tool, dict):
return {}
tables: dict[str, PyprojectTable] = {}
for key, value in tool.items():
if isinstance(key, str) and isinstance(value, dict):
tables[key] = value
return tables
def tool_metadata_section_name(pyproject: PyprojectTable = PYPROJECT) -> str | None:
tables = tool_tables(pyproject)
if PREFERRED_TOOL_METADATA_SECTION in tables:
return PREFERRED_TOOL_METADATA_SECTION
for section_name, table in tables.items():
if any(field in table for field in METADATA_TOOL_FIELDS):
return section_name
return None
def tool_metadata_table(pyproject: PyprojectTable = PYPROJECT) -> PyprojectTable:
section_name = tool_metadata_section_name(pyproject)
if section_name is None:
return {}
return tool_tables(pyproject).get(section_name, {})
def _configured_tool_metadata_value(key: str, pyproject: PyprojectTable = PYPROJECT) -> str | None:
return _normalized_string(tool_metadata_table(pyproject).get(key))
def script_name_from_pyproject(pyproject: PyprojectTable = PYPROJECT) -> str | None:
scripts = project_table(pyproject).get("scripts")
if not isinstance(scripts, dict) or not scripts:
return None
matching = [
name
for name, target in scripts.items()
if isinstance(name, str) and isinstance(target, str) and target.strip() == CONSOLE_SCRIPT_TARGET
]
if len(matching) == 1:
return _normalized_string(matching[0])
if len(scripts) == 1:
return _normalized_string(next(iter(scripts)))
return None
def package_name_from_pyproject(pyproject: PyprojectTable = PYPROJECT) -> str:
return (
_normalized_string(project_table(pyproject).get("name"))
or inferred_package_name_from_installed_distribution()
or DEFAULT_PACKAGE_NAME
)
def inferred_package_name_from_installed_distribution(module_package: str | None = __package__) -> str | None:
if not module_package:
return None
top_level_package = module_package.split(".")[0].strip()
if not top_level_package:
return None
candidates = packages_distributions().get(top_level_package, [])
for candidate in candidates:
normalized = _normalized_string(candidate)
if normalized:
return normalized
return None
def app_name_from_pyproject(pyproject: PyprojectTable = PYPROJECT) -> str:
return (
_configured_tool_metadata_value("name", pyproject) or package_name_from_pyproject(pyproject) or DEFAULT_APP_NAME
)
def command_name_from_pyproject(pyproject: PyprojectTable = PYPROJECT) -> str:
return (
_configured_tool_metadata_value("cli_name", pyproject)
or script_name_from_pyproject(pyproject)
or package_name_from_pyproject(pyproject)
or DEFAULT_COMMAND_NAME
)
def _normalize_env_prefix(prefix: str) -> str:
normalized = prefix.strip().upper()
if not normalized:
return DEFAULT_ENV_PREFIX
if not normalized.endswith(ENV_PREFIX_SUFFIX):
normalized = f"{normalized}{ENV_PREFIX_SUFFIX}"
return normalized
def env_prefix_from_command_name(command_name: str) -> str:
normalized = re.sub(r"[^A-Za-z0-9]+", "_", command_name.strip()).strip("_").upper()
if not normalized:
return DEFAULT_ENV_PREFIX
return f"{normalized}{ENV_PREFIX_SUFFIX}"
def env_prefix_from_pyproject(pyproject: PyprojectTable = PYPROJECT) -> str:
env_prefix = _configured_tool_metadata_value("env_prefix", pyproject)
if env_prefix is not None:
return _normalize_env_prefix(env_prefix)
return env_prefix_from_command_name(command_name_from_pyproject(pyproject))
def user_config_dir(package_name: str) -> Path:
normalized = package_name.strip() or DEFAULT_PACKAGE_NAME
return Path.home() / f".{normalized}"
class Metadata:
"""Centralized metadata and constants for the application."""
PACKAGE_NAME = package_name_from_pyproject()
APP_NAME = app_name_from_pyproject()
COMMAND_NAME = command_name_from_pyproject()
ENV_PREFIX = env_prefix_from_pyproject()
TOOL_METADATA_SECTION = tool_metadata_section_name() or PREFERRED_TOOL_METADATA_SECTION
try:
VERSION = version(PACKAGE_NAME)
except PackageNotFoundError:
raise click.ClickException(f"Package '{PACKAGE_NAME}' not found. Ensure it is installed correctly.") from None
PACKAGE_ROOT_DIR = Path(__file__).resolve().parent.parent
COMMANDS_DIR = PACKAGE_ROOT_DIR / "commands"
USER_CONFIG_DIR = user_config_dir(PACKAGE_NAME)
USER_COMMANDS_DIR = USER_CONFIG_DIR / "commands"
PYTHON_VERSION = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
IS_TTY = sys.stdout.isatty()
@classmethod
def banner(cls) -> str:
"""Generate application banner."""
return f"{cls.APP_NAME} v{cls.VERSION}"
@classmethod
def full_version(cls) -> str:
"""Detailed version information."""
return f"{cls.APP_NAME} {cls.VERSION}\nPython {cls.PYTHON_VERSION}\nPackage: {cls.PACKAGE_NAME}"
@classmethod
def env_var(cls, name: str) -> str:
"""Build an environment variable name using the configured prefix."""
return f"{cls.ENV_PREFIX}{name}"