the1conf 1.0.0__py3-none-any.whl → 1.2.3__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.
@@ -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
- from collections.abc import Mapping, MutableMapping,Sequence, Iterator
4
- from typing import (Any, Callable, Optional,
5
- Protocol, Union, get_origin)
6
-
7
- import numpy as np
8
- import pandas as pd
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
- def __init__(self):
43
- self.var1 = "var1"
44
- self.__dict__["vardict"] = "vardict"
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( # type: ignore
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
@@ -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, Undefined
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
- """Wrappe click.option avec les métadonnées de ConfigVarDef."""
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
- # 1. Nom du flag (ex: my_var -> --my-var)
15
- param_name = config_var.Name
16
- flag_name = f"--{param_name.replace('_', '-').lower()}"
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.Help:
20
- kwargs["help"] = config_var.Help
21
-
22
- # 3. Contrainte stricte : Toujours des strings
23
- # On écrase tout type passé pour garantir que resolve_vars reçoive du string.
24
- kwargs["type"] = click.STRING
25
-
26
- # 4. Affichage du défaut sans l'appliquer
27
- if "show_default" not in kwargs and config_var.Default is not Undefined and not callable(config_var.Default):
28
- # On doit convertir en string sinon click interprete les bool/int comme des flags (True/False)
29
- # qui lui disent "affiche le default" (qui est None ici), du coup il n'affiche rien.
30
- kwargs["show_default"] = str(config_var.Default)
31
-
32
- # Note: On ne passe PAS 'envvar' ni 'default'
33
-
34
- # On force le nom de destination pour qu'il matche la clé attendue par the1conf
35
- return click.option(flag_name, param_name, **kwargs)
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)