the1conf 1.0.0__py3-none-any.whl → 1.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- the1conf/__init__.py +12 -2
- the1conf/app_config.py +276 -408
- the1conf/app_config_meta.py +202 -0
- the1conf/attr_dict.py +38 -25
- the1conf/auto_prolog.py +177 -0
- the1conf/click_option.py +62 -25
- the1conf/config_var.py +312 -396
- the1conf/flags.py +143 -0
- the1conf/solver.py +346 -0
- the1conf/utils.py +110 -0
- the1conf/var_substitution.py +59 -0
- the1conf-1.3.0.dist-info/METADATA +794 -0
- the1conf-1.3.0.dist-info/RECORD +16 -0
- the1conf-1.0.0.dist-info/METADATA +0 -516
- the1conf-1.0.0.dist-info/RECORD +0 -10
- {the1conf-1.0.0.dist-info → the1conf-1.3.0.dist-info}/WHEEL +0 -0
- {the1conf-1.0.0.dist-info → the1conf-1.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
from collections import OrderedDict
|
|
5
|
+
from inspect import getsource
|
|
6
|
+
from typing import Any, Iterable, cast
|
|
7
|
+
from .config_var import ConfigVarDef
|
|
8
|
+
from .utils import get_attribute_docstrings, NameSpace
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ConfigVarCollector:
|
|
12
|
+
"""
|
|
13
|
+
Collector class to gather ConfigVarDef from AppConfig classes and their nested NameSpace classes.
|
|
14
|
+
|
|
15
|
+
This class helps in aggregating configuration variables from various sources during the class creation process.
|
|
16
|
+
It supports two main collection strategies:
|
|
17
|
+
1. Parsing the class definition directly (useful for mixins or the root class).
|
|
18
|
+
2. Inheriting already collected variables from a parent `AppConfig` class (optimization).
|
|
19
|
+
|
|
20
|
+
This class can be used as a context manager to ensure proper resource management.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
_collected_vars: OrderedDict[str, ConfigVarDef]
|
|
24
|
+
_auto_prolog_var_names: list[str]
|
|
25
|
+
|
|
26
|
+
def __enter__(self):
|
|
27
|
+
"""Return self to be used in the with statement"""
|
|
28
|
+
return self
|
|
29
|
+
|
|
30
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
31
|
+
"""Cleanup resources on exit"""
|
|
32
|
+
self.close()
|
|
33
|
+
|
|
34
|
+
def __init__(self) -> None:
|
|
35
|
+
"""Create a new ConfigVarCollector instance."""
|
|
36
|
+
self._collected_vars = OrderedDict()
|
|
37
|
+
self._auto_prolog_var_names = []
|
|
38
|
+
|
|
39
|
+
def close(self) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Clean up resources if needed.
|
|
42
|
+
"""
|
|
43
|
+
self._collected_vars.clear()
|
|
44
|
+
self._auto_prolog_var_names.clear()
|
|
45
|
+
|
|
46
|
+
def _collect_config_vars(
|
|
47
|
+
self, owner: type, prefix: str = ""
|
|
48
|
+
) -> Iterable[ConfigVarDef]:
|
|
49
|
+
"""
|
|
50
|
+
Traverses ConfigVarDef defined in the owner class and its nested NameSpace classes.
|
|
51
|
+
Search for ConfigVarDef in the owner class __dict__ and yield them.
|
|
52
|
+
Attach the docstrings as help if help is not already defined and a docstring exists
|
|
53
|
+
for the attribute.
|
|
54
|
+
change its name by adding the namespace it is defined in as a prefix.
|
|
55
|
+
"""
|
|
56
|
+
# extract docstrings from the class source code
|
|
57
|
+
docstrings = get_attribute_docstrings(owner)
|
|
58
|
+
|
|
59
|
+
# Inspect the class __dict__ to find ConfigVarDef defined in this class
|
|
60
|
+
for attr_name, value in owner.__dict__.items():
|
|
61
|
+
if isinstance(value, ConfigVarDef):
|
|
62
|
+
# Clone to update name with prefix
|
|
63
|
+
new_var = value
|
|
64
|
+
|
|
65
|
+
# If help is missing, try to use the docstring
|
|
66
|
+
if not new_var.help and attr_name in docstrings:
|
|
67
|
+
new_var._help = docstrings[attr_name]
|
|
68
|
+
|
|
69
|
+
if prefix:
|
|
70
|
+
# We need to access _name directly because we don't want to trigger the
|
|
71
|
+
# descriptor __get__ method
|
|
72
|
+
original_name = getattr(value, "_name", attr_name)
|
|
73
|
+
new_var._name = f"{prefix}{original_name}"
|
|
74
|
+
|
|
75
|
+
yield new_var
|
|
76
|
+
|
|
77
|
+
elif (
|
|
78
|
+
isinstance(value, type)
|
|
79
|
+
and issubclass(value, NameSpace)
|
|
80
|
+
and value is not NameSpace
|
|
81
|
+
):
|
|
82
|
+
# Recurse into nested NameSpace classes
|
|
83
|
+
yield from self._collect_config_vars(
|
|
84
|
+
value, prefix=f"{prefix}{attr_name}."
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def collect_on_class(self, owner: type) -> None:
|
|
88
|
+
"""
|
|
89
|
+
Collect configuration variables by inspecting the class `__dict__` and its nested `NameSpace`s.
|
|
90
|
+
|
|
91
|
+
This method is used when the class does not have a `_config_var_defs` attribute yet,
|
|
92
|
+
typically for mixins (like AutoProlog) or during the initialization of the root `AppConfig` class.
|
|
93
|
+
|
|
94
|
+
It performs a deep scan:
|
|
95
|
+
- Iterates over the class attributes to find `ConfigVarDef` instances.
|
|
96
|
+
- Extracts docstrings to use as help text if needed.
|
|
97
|
+
- Recurses into nested `NameSpace` classes to collect variables defined within them.
|
|
98
|
+
|
|
99
|
+
Any variable found overrides an existing variable with the same name in the collection.
|
|
100
|
+
"""
|
|
101
|
+
for vardef in self._collect_config_vars(owner):
|
|
102
|
+
attr_name = getattr(vardef, "_name", None)
|
|
103
|
+
if attr_name:
|
|
104
|
+
if attr_name in self._collected_vars:
|
|
105
|
+
# Override previous definition
|
|
106
|
+
self._collected_vars.pop(attr_name)
|
|
107
|
+
self._collected_vars[attr_name] = vardef
|
|
108
|
+
if attr_name and vardef.auto_prolog:
|
|
109
|
+
if attr_name in self._auto_prolog_var_names:
|
|
110
|
+
# in this case we need to move the var_name to the end of the list to preserve the order
|
|
111
|
+
self._auto_prolog_var_names.remove(attr_name)
|
|
112
|
+
self._auto_prolog_var_names.append(attr_name)
|
|
113
|
+
|
|
114
|
+
def collect_in_config_var_defs(self, owner: type) -> None:
|
|
115
|
+
"""
|
|
116
|
+
Collect configuration variables from the `_config_var_defs` attribute of the owner class.
|
|
117
|
+
|
|
118
|
+
This method is used during inheritance when the parent class (`owner`) is already a subclass
|
|
119
|
+
of `AppConfig`. Since the parent has already computed its configuration variables (stored in
|
|
120
|
+
`_config_var_defs`), we can simply copy them instead of re-scanning the class.
|
|
121
|
+
|
|
122
|
+
This is much faster than `collect_on_class` and preserves the resolution order established
|
|
123
|
+
in the parent class.
|
|
124
|
+
|
|
125
|
+
Any variable found overrides an existing variable with the same name in the collection.
|
|
126
|
+
"""
|
|
127
|
+
base_config_var_defs = getattr(owner, "_config_var_defs", None)
|
|
128
|
+
if not base_config_var_defs:
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
assert isinstance(base_config_var_defs, OrderedDict)
|
|
132
|
+
|
|
133
|
+
for var_name, var_def in cast(
|
|
134
|
+
OrderedDict[str, ConfigVarDef], base_config_var_defs
|
|
135
|
+
).items():
|
|
136
|
+
# base_config_var_defs is the _config_var_defs dict. Loops on each of its elements and store them in the collected list
|
|
137
|
+
# For each one read its name. If the variable name is already in the collected list, it means that it has been
|
|
138
|
+
# defined in a parent class of the current inspected base class. So we remove the old definition and add the new one.
|
|
139
|
+
if var_name:
|
|
140
|
+
if var_name in self._collected_vars:
|
|
141
|
+
# Override previous definition
|
|
142
|
+
self._collected_vars.pop(var_name)
|
|
143
|
+
self._collected_vars[var_name] = var_def
|
|
144
|
+
if var_name and var_def.auto_prolog:
|
|
145
|
+
if var_name in self._auto_prolog_var_names:
|
|
146
|
+
# in this case we need to move the var_name to the end of the list to preserve the order
|
|
147
|
+
self._auto_prolog_var_names.remove(var_name)
|
|
148
|
+
self._auto_prolog_var_names.append(var_name)
|
|
149
|
+
|
|
150
|
+
def get_collected_vars(self) -> OrderedDict[str, ConfigVarDef]:
|
|
151
|
+
"""
|
|
152
|
+
Get the collected configuration variables.
|
|
153
|
+
"""
|
|
154
|
+
return self._collected_vars.copy()
|
|
155
|
+
|
|
156
|
+
def get_auto_prolog_var_names(self) -> list[str]:
|
|
157
|
+
"""
|
|
158
|
+
Get only the collected configuration variables that have auto_prolog enabled.
|
|
159
|
+
"""
|
|
160
|
+
return self._auto_prolog_var_names.copy()
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class AppConfigMeta(type):
|
|
164
|
+
"""
|
|
165
|
+
Metaclass for AppConfig to handle initialization of the root class itself,
|
|
166
|
+
specifically to collect configuration variables from mixins that are not AppConfig subclasses.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
def __init__(cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any]) -> None:
|
|
170
|
+
"""
|
|
171
|
+
Initialize the class object.
|
|
172
|
+
|
|
173
|
+
This method is part of the Python metaclass protocol. It is invoked after the class object `cls`
|
|
174
|
+
has been created (usually via `type.__new__`). It effectively acts as a constructor for the class
|
|
175
|
+
object itself.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
name (str): The name of the class being created.
|
|
179
|
+
bases (tuple[type, ...]): A tuple containing the base classes of the class being created.
|
|
180
|
+
attrs (dict[str, Any]): A dictionary containing the class namespace (attributes and methods)
|
|
181
|
+
populated during the execution of the class body.
|
|
182
|
+
Keys are the names of the attributes (strings) and values are the
|
|
183
|
+
attribute values (methods, class variables, properties, etc.).
|
|
184
|
+
"""
|
|
185
|
+
super().__init__(name, bases, attrs)
|
|
186
|
+
|
|
187
|
+
# Only perform root initialization for the AppConfig class itself.
|
|
188
|
+
# Subclasses use the standard __init_subclass__ mechanism.
|
|
189
|
+
if name == "AppConfig":
|
|
190
|
+
with ConfigVarCollector() as collector:
|
|
191
|
+
for base in reversed(cls.__mro__[1:-1]):
|
|
192
|
+
# For the root class initialization we want to scan all parents.
|
|
193
|
+
# We want to include variables from mixins like AutoProlog.
|
|
194
|
+
# Since AppConfig is the root for this mechanism, its parents don't have _config_var_defs.
|
|
195
|
+
# In the mro list AppConfig is always at index 0, so we can skip it safely and the last item is always object which we can also skip.
|
|
196
|
+
|
|
197
|
+
collector.collect_on_class(base)
|
|
198
|
+
# Add own vars
|
|
199
|
+
collector.collect_on_class(cls)
|
|
200
|
+
|
|
201
|
+
cls._config_var_defs = collector.get_collected_vars()
|
|
202
|
+
cls._autoprolog_var_names = collector.get_auto_prolog_var_names()
|
the1conf/attr_dict.py
CHANGED
|
@@ -1,24 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
from
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
def is_sequence(obj: Any) -> bool:
|
|
11
|
-
""" test if obj is a sequence (list, tuple, etc...) but not a string in thte most general way"""
|
|
12
|
-
try:
|
|
13
|
-
len(obj)
|
|
14
|
-
obj[0:0]
|
|
15
|
-
return not isinstance(obj, str)
|
|
16
|
-
except KeyError:
|
|
17
|
-
return False
|
|
18
|
-
except TypeError:
|
|
19
|
-
return False # TypeError: object is not iterable
|
|
20
|
-
|
|
21
|
-
class AttrDict(MutableMapping):
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import Iterator, Mapping
|
|
5
|
+
from typing import Any, Optional
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AttrDict:
|
|
22
9
|
"""
|
|
23
10
|
class which properties can be accessed like a dict or like a propertie (.x) and vis/versa:
|
|
24
11
|
if used like a Dict to set a value , if the key contains dots (.) it creates a hierarchy of AttrDict object.
|
|
@@ -39,9 +26,9 @@ class AttrDict(MutableMapping):
|
|
|
39
26
|
|
|
40
27
|
|
|
41
28
|
class Test(AttrDict):
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
29
|
+
def __init__(self):
|
|
30
|
+
self.var1 = "var1"
|
|
31
|
+
self.__dict__["vardict"] = "vardict"
|
|
45
32
|
t = Test()
|
|
46
33
|
print (f"t['vardict'] = {t['vardict']}") # t['vardict'] = vardict
|
|
47
34
|
print (f"t.vardict = {t.vardict}") # t.vardict = vardict
|
|
@@ -61,6 +48,8 @@ class AttrDict(MutableMapping):
|
|
|
61
48
|
print (f"t.a1 = {t.a1}") # t.a1 = {'a2': 'complexval'}
|
|
62
49
|
"""
|
|
63
50
|
|
|
51
|
+
_logger = logging.getLogger(__name__)
|
|
52
|
+
|
|
64
53
|
def __init__(self, init: Optional[Mapping[Any, Any]] = None) -> None:
|
|
65
54
|
if init is not None:
|
|
66
55
|
self.__dict__.update(init)
|
|
@@ -127,15 +116,39 @@ class AttrDict(MutableMapping):
|
|
|
127
116
|
"""This method is called when an iterator is required for a container."""
|
|
128
117
|
return self.__dict__.__iter__()
|
|
129
118
|
|
|
130
|
-
def update(
|
|
131
|
-
self, other: Mapping[Any, Any], override: bool = False
|
|
132
|
-
) -> None:
|
|
119
|
+
def update(self, other: Mapping[Any, Any], override: bool = False) -> None:
|
|
133
120
|
if override:
|
|
134
121
|
res = self.__dict__ | other # type: ignore
|
|
135
122
|
else:
|
|
136
123
|
res = other | self.__dict__ # type: ignore
|
|
137
124
|
self.__dict__ = res
|
|
138
125
|
|
|
126
|
+
def to_dict(self) -> dict[Any, Any]:
|
|
127
|
+
"""
|
|
128
|
+
Return a copy of the AttrDict as a dictionary.
|
|
129
|
+
This is a recursive copy: nested AttrDict are also converted to dict.
|
|
130
|
+
"""
|
|
131
|
+
d = {}
|
|
132
|
+
for k, v in self.__dict__.items():
|
|
133
|
+
if issubclass(type(v), AttrDict):
|
|
134
|
+
d[k] = v.to_dict()
|
|
135
|
+
else:
|
|
136
|
+
d[k] = v
|
|
137
|
+
return d
|
|
138
|
+
|
|
139
|
+
def has_value(self, key: str | None) -> bool:
|
|
140
|
+
"""
|
|
141
|
+
Check if the AttrDict has a value for the given key.
|
|
142
|
+
The key can be a dotted path to access nested AttrDict.
|
|
143
|
+
"""
|
|
144
|
+
if key is None:
|
|
145
|
+
return False
|
|
146
|
+
try:
|
|
147
|
+
_ = self[key]
|
|
148
|
+
return True
|
|
149
|
+
except Exception:
|
|
150
|
+
return False
|
|
151
|
+
|
|
139
152
|
def _repr_with_ident(self, indent: int) -> list[str]:
|
|
140
153
|
res = []
|
|
141
154
|
indent_str = "\t" * indent
|
the1conf/auto_prolog.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Literal, Optional, Any
|
|
6
|
+
|
|
7
|
+
from .config_var import configvar
|
|
8
|
+
from .utils import PathType
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _get_os_type() -> str:
|
|
12
|
+
if sys.platform.startswith("win"):
|
|
13
|
+
return "windows"
|
|
14
|
+
elif sys.platform.startswith("linux"):
|
|
15
|
+
return "linux"
|
|
16
|
+
elif sys.platform.startswith("darwin"):
|
|
17
|
+
return "macos"
|
|
18
|
+
return "unknown"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AutoProlog:
|
|
22
|
+
"""
|
|
23
|
+
Automatic configuration for standard paths and environment variables.
|
|
24
|
+
This class defines configuration variables that will be accessible in any
|
|
25
|
+
subclass of AppConfig, providing sensible defaults based directories, Os type
|
|
26
|
+
and execution stage.
|
|
27
|
+
|
|
28
|
+
Variables defined:
|
|
29
|
+
- os_type: The operating system type (windows, linux, macos)
|
|
30
|
+
- user_home: The user's home directory, as returned by Path.home()
|
|
31
|
+
Must be used instead of '~' to ensure cross platform compatibility.
|
|
32
|
+
- xdg_data_home: Base directory for user specific data files (XDG standard)
|
|
33
|
+
- xdg_config_home: Base directory for user specific configuration files (XDG standard)
|
|
34
|
+
- xdg_cache_home: Base directory for user specific non-essential data files (XDG standard)
|
|
35
|
+
- xdg_state_home: Base directory for user specific state files (XDG standard)
|
|
36
|
+
- app_data: Application Data directory on Windows
|
|
37
|
+
- app_config: Application Config directory on Windows
|
|
38
|
+
- app_cache: Application Cache directory on Windows
|
|
39
|
+
- data_home: OS independent application data directory
|
|
40
|
+
- config_home: OS independent application configuration directory
|
|
41
|
+
- cache_home: OS independent application cache directory
|
|
42
|
+
- exec_stage: The execution stage, default to the string "dev", no restriction on its value
|
|
43
|
+
to let the user define its own stages.
|
|
44
|
+
|
|
45
|
+
Any of this variable can be overridden by a redefinition on a subclasse of AppConfig.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
os_type: Literal["windows", "linux", "macos", "unknown"] = configvar(
|
|
49
|
+
default=lambda _, __, ___: _get_os_type(),
|
|
50
|
+
no_search=True,
|
|
51
|
+
help="The operating system type (windows, linux, macos)",
|
|
52
|
+
auto_prolog=True,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
exec_stage: Any = configvar(
|
|
56
|
+
default=None,
|
|
57
|
+
type_info=str,
|
|
58
|
+
env_name=["EXEC_STAGE", "STAGE", "ENV"],
|
|
59
|
+
value_key=["exec_stage", "stage", "env"],
|
|
60
|
+
help="The execution stage (dev, prod, test)",
|
|
61
|
+
click_key_conversion=True,
|
|
62
|
+
allow_override=True,
|
|
63
|
+
auto_prolog=True,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
user_home: Path = configvar(
|
|
67
|
+
default=lambda _, __, ___: Path.home(),
|
|
68
|
+
no_search=True,
|
|
69
|
+
help="The user's home directory",
|
|
70
|
+
auto_prolog=True,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# XDG Standards
|
|
74
|
+
xdg_data_home: Optional[Path] = configvar(
|
|
75
|
+
default=lambda _, c, __: (
|
|
76
|
+
c.user_home / ".local" / "share" if c.os_type != "windows" else None
|
|
77
|
+
),
|
|
78
|
+
env_name="XDG_DATA_HOME",
|
|
79
|
+
help="Base directory for user specific data files",
|
|
80
|
+
no_value_search=True,
|
|
81
|
+
no_conffile_search=True,
|
|
82
|
+
auto_prolog=True,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
xdg_config_home: Optional[Path] = configvar(
|
|
86
|
+
default=lambda _, c, __: (
|
|
87
|
+
c.user_home / ".config" if c.os_type != "windows" else None
|
|
88
|
+
),
|
|
89
|
+
env_name="XDG_CONFIG_HOME",
|
|
90
|
+
help="Base directory for user specific configuration files",
|
|
91
|
+
no_value_search=True,
|
|
92
|
+
no_conffile_search=True,
|
|
93
|
+
auto_prolog=True,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
xdg_cache_home: Optional[Path] = configvar(
|
|
97
|
+
default=lambda _, c, __: (
|
|
98
|
+
c.user_home / ".cache" if c.os_type != "windows" else None
|
|
99
|
+
),
|
|
100
|
+
env_name="XDG_CACHE_HOME",
|
|
101
|
+
help="Base directory for user specific non-essential data files",
|
|
102
|
+
no_value_search=True,
|
|
103
|
+
no_conffile_search=True,
|
|
104
|
+
auto_prolog=True,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
xdg_state_home: Optional[Path] = configvar(
|
|
108
|
+
default=lambda _, c, __: (
|
|
109
|
+
c.user_home / ".local" / "state" if c.os_type != "windows" else None
|
|
110
|
+
),
|
|
111
|
+
env_name="XDG_STATE_HOME",
|
|
112
|
+
help="Base directory for user specific state files",
|
|
113
|
+
no_value_search=True,
|
|
114
|
+
no_conffile_search=True,
|
|
115
|
+
auto_prolog=True,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Windows specific variables
|
|
119
|
+
app_data: Optional[Path] = configvar(
|
|
120
|
+
default=lambda _, c, __: (
|
|
121
|
+
c.user_home / "AppData" if c.os_type == "windows" else None
|
|
122
|
+
),
|
|
123
|
+
env_name="APP_DATA",
|
|
124
|
+
help="Application Data directory on Windows",
|
|
125
|
+
no_value_search=True,
|
|
126
|
+
no_conffile_search=True,
|
|
127
|
+
auto_prolog=True,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
app_config: Optional[Path] = configvar(
|
|
131
|
+
default=lambda _, c, __: c.app_data / "Config" if c.app_data else None,
|
|
132
|
+
env_name="APP_CONFIG",
|
|
133
|
+
help="Application Config directory on Windows",
|
|
134
|
+
no_value_search=True,
|
|
135
|
+
no_conffile_search=True,
|
|
136
|
+
auto_prolog=True,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
app_cache: Optional[Path] = configvar(
|
|
140
|
+
default=lambda _, c, __: c.app_data / "Cache" if c.app_data else None,
|
|
141
|
+
env_name="APP_CACHE",
|
|
142
|
+
help="Application Cache directory on Windows",
|
|
143
|
+
no_value_search=True,
|
|
144
|
+
no_conffile_search=True,
|
|
145
|
+
auto_prolog=True,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Unified variables
|
|
149
|
+
data_home: Path = configvar(
|
|
150
|
+
default=lambda _, c, __: (
|
|
151
|
+
c.app_data if c.os_type == "windows" else c.xdg_data_home
|
|
152
|
+
),
|
|
153
|
+
click_key_conversion=True,
|
|
154
|
+
help="OS independent application data directory",
|
|
155
|
+
auto_prolog=True,
|
|
156
|
+
allow_override=True,
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
config_home: Path = configvar(
|
|
160
|
+
default=lambda _, c, __: (
|
|
161
|
+
c.app_config if c.os_type == "windows" else c.xdg_config_home
|
|
162
|
+
),
|
|
163
|
+
click_key_conversion=True,
|
|
164
|
+
help="OS independent application configuration directory",
|
|
165
|
+
auto_prolog=True,
|
|
166
|
+
allow_override=True,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
cache_home: Path = configvar(
|
|
170
|
+
default=lambda _, c, __: (
|
|
171
|
+
c.app_cache if c.os_type == "windows" else c.xdg_cache_home
|
|
172
|
+
),
|
|
173
|
+
click_key_conversion=True,
|
|
174
|
+
help="OS independent application cache directory",
|
|
175
|
+
auto_prolog=True,
|
|
176
|
+
allow_override=True,
|
|
177
|
+
)
|
the1conf/click_option.py
CHANGED
|
@@ -1,35 +1,72 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
+
|
|
2
3
|
from typing import Any, Callable
|
|
3
4
|
|
|
4
5
|
import click
|
|
5
6
|
|
|
6
|
-
from .app_config import ConfigVarDef
|
|
7
|
+
from .app_config import ConfigVarDef
|
|
8
|
+
from .utils import Undefined, _undef
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class StringOrUndefined(click.ParamType):
|
|
12
|
+
name = "string"
|
|
13
|
+
|
|
14
|
+
def convert(self, value: Any, param: Any, ctx: Any) -> Any:
|
|
15
|
+
if value is _undef:
|
|
16
|
+
return value
|
|
17
|
+
return click.STRING.convert(value, param, ctx)
|
|
18
|
+
|
|
7
19
|
|
|
8
20
|
def click_option(config_var: Any, **kwargs: Any) -> Callable[[Any], Any]:
|
|
9
|
-
"""
|
|
21
|
+
"""Wrapper for click.option with the metadata from ConfigVarDef."""
|
|
10
22
|
if not isinstance(config_var, ConfigVarDef):
|
|
11
23
|
raise TypeError(f"click_option expects a ConfigVarDef, got {type(config_var)}")
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
|
|
25
|
+
# 1. Flag name (ex: my_var -> --my-var)
|
|
26
|
+
param_decls = []
|
|
27
|
+
|
|
28
|
+
keys = []
|
|
29
|
+
if config_var.value_key is None:
|
|
30
|
+
keys = [config_var.name]
|
|
31
|
+
elif isinstance(config_var.value_key, str):
|
|
32
|
+
keys = [config_var.value_key]
|
|
33
|
+
else:
|
|
34
|
+
keys = config_var.value_key
|
|
35
|
+
|
|
36
|
+
for key in keys:
|
|
37
|
+
if len(key) == 1:
|
|
38
|
+
param_decls.append(f"-{key}")
|
|
39
|
+
else:
|
|
40
|
+
param_decls.append(f"--{key.replace('_', '-').lower()}")
|
|
41
|
+
|
|
42
|
+
# Used as destination in the dict returned by click
|
|
43
|
+
param_name = keys[0]
|
|
44
|
+
|
|
18
45
|
# 2. Documentation
|
|
19
|
-
if "help" not in kwargs and config_var.
|
|
20
|
-
kwargs["help"] = config_var.
|
|
21
|
-
|
|
22
|
-
# 3.
|
|
23
|
-
#
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
if "help" not in kwargs and config_var.help:
|
|
47
|
+
kwargs["help"] = config_var.help
|
|
48
|
+
|
|
49
|
+
# 3. Strict constraint: Always strings
|
|
50
|
+
# We overwrite any passed type to ensure that resolve_vars receives a string.
|
|
51
|
+
# We use a custom type to handle the `_undef` default value without converting it to string.
|
|
52
|
+
kwargs["type"] = StringOrUndefined()
|
|
53
|
+
|
|
54
|
+
# We set default to `_undef` to distinguish between "option not provided" (_undef)
|
|
55
|
+
# and "option provided with no value" (which shouldn't happen with string) or None.
|
|
56
|
+
# AppConfig handles `_undef` as "missing value" and will look for other sources (env, file, default).
|
|
57
|
+
kwargs["default"] = _undef
|
|
58
|
+
|
|
59
|
+
# 4. Display default without applying it
|
|
60
|
+
if (
|
|
61
|
+
"show_default" not in kwargs
|
|
62
|
+
and config_var.default is not Undefined
|
|
63
|
+
and not callable(config_var.default)
|
|
64
|
+
):
|
|
65
|
+
# We must convert to string otherwise click interprets bool/int as flags (True/False)
|
|
66
|
+
# which tell it "show the default" (which is None here), so it displays nothing.
|
|
67
|
+
kwargs["show_default"] = str(config_var.default)
|
|
68
|
+
|
|
69
|
+
# Note: We do NOT pass 'envvar' nor 'default'
|
|
70
|
+
|
|
71
|
+
# We force the destination name to match the key expected by the1conf
|
|
72
|
+
return click.option(*param_decls, param_name, **kwargs)
|