the1conf 1.0.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 ADDED
@@ -0,0 +1,4 @@
1
+ from .app_config import AppConfig, ConfigVarDef, NameSpace, configvar
2
+ from .click_option import click_option
3
+
4
+ __all__ = ["AppConfig", "ConfigVarDef", "NameSpace", "configvar", "click_option"]
the1conf/app_config.py ADDED
@@ -0,0 +1,643 @@
1
+ from __future__ import annotations
2
+
3
+ import dataclasses
4
+ import importlib
5
+ import json
6
+ import logging
7
+ import copy
8
+ import os
9
+ import ast
10
+ import inspect
11
+ import textwrap
12
+ from collections.abc import Iterable, Mapping, MutableMapping, Sequence
13
+ from dataclasses import dataclass
14
+ from enum import Enum, auto
15
+ from pathlib import Path
16
+ from typing import (
17
+ Annotated,
18
+ Any,
19
+ Callable,
20
+ cast,
21
+ Generic,
22
+ Optional,
23
+ TypeAlias,
24
+ TypeVar,
25
+ Union,
26
+ get_args,
27
+ get_origin,
28
+ get_type_hints,
29
+ dataclass_transform,
30
+ )
31
+
32
+ import yaml
33
+
34
+ from .attr_dict import AttrDict, is_sequence
35
+ from pydantic import TypeAdapter, ValidationError
36
+ from .config_var import ConfigVarDef, Flags, PathType, Undefined, _UndefinedSentinel, _undef, configvar
37
+
38
+
39
+ def _get_attribute_docstrings(cls: type) -> dict[str, str]:
40
+ """
41
+ Extracts docstrings for class attributes by parsing the source code.
42
+ This allows using docstrings to populate ConfigVarDef.Help.
43
+ """
44
+ docstrings = {}
45
+ try:
46
+ source = inspect.getsource(cls)
47
+ # Deduct to handle nested classes indentation
48
+ source = textwrap.dedent(source)
49
+ except (OSError, TypeError):
50
+ # Source code not available (e.g. dynamic class, REPL, compiled files)
51
+ return {}
52
+
53
+ try:
54
+ tree = ast.parse(source)
55
+ except SyntaxError:
56
+ return {}
57
+
58
+ # We look for the ClassDef in the parsed source.
59
+ # Since inspect.getsource(cls) returns the class definition itself,
60
+ # the first node in body should be the ClassDef.
61
+ for node in tree.body:
62
+ if isinstance(node, ast.ClassDef):
63
+ for i, item in enumerate(node.body):
64
+ if isinstance(item, (ast.Assign, ast.AnnAssign)):
65
+ # Check if the next node is an expression containing a string (docstring)
66
+ if i + 1 < len(node.body):
67
+ next_node = node.body[i + 1]
68
+ if (
69
+ isinstance(next_node, ast.Expr)
70
+ and isinstance(next_node.value, ast.Constant)
71
+ and isinstance(next_node.value.value, str)
72
+ ):
73
+ docstring = next_node.value.value.strip()
74
+
75
+ targets = []
76
+ if isinstance(item, ast.Assign):
77
+ targets = item.targets
78
+ elif isinstance(item, ast.AnnAssign):
79
+ targets = [item.target]
80
+
81
+ for target in targets:
82
+ if isinstance(target, ast.Name):
83
+ docstrings[target.id] = docstring
84
+ # We found the class def, no need to continue in top level
85
+ break
86
+
87
+ return docstrings
88
+
89
+
90
+ class AppConfigException(Exception):
91
+ def __init__(self, message: str) -> None:
92
+ super().__init__(message)
93
+ self.message = message
94
+
95
+
96
+ class NameSpace:
97
+ """Marker class for configuration namespaces.
98
+ Used to group configuration variables into nested namespaces within an AppConfig subclass.
99
+ See AppConfig documentation for more information.
100
+ """
101
+
102
+ pass
103
+
104
+
105
+ class AppConfig(AttrDict):
106
+ """ "
107
+ A class to manage application configuration. It can be passed from callable to callable.
108
+ It can search for variable values in command line arguments, environment variables, a yaml or json configuration file
109
+ or a default value or a computed value.
110
+
111
+ Configuration variable are defined with a list of ConfigVarDef object which defines how to look for the variable values and/or compute them.
112
+
113
+ The variable values are searched in these locations in the following order:
114
+ - a dict of values usually passed from the command line arguments
115
+ - environment variables
116
+ - a configuration file in yaml or json format
117
+ - a default value or a computed value
118
+ The first match found is used as the value for the variable.
119
+
120
+ When found the variable can be casted to a specific type and/or transformed with a callable.
121
+
122
+ The default value can also be computed with a callable that can use already defined variables in this AppConfig object.
123
+ One can combine a default value and a transformation callable to compute complex default values that can combine several already defined variables,
124
+ knowing that variables are resolved in the order they are declared in the AppConfig subclass.
125
+
126
+ See method 'resolve_vars()' and class 'ConfigVarDef' for more information.
127
+
128
+ Variable can also be set directly like in an object attribute or in a Dict key:
129
+
130
+ ctx = AppConfig()
131
+ ctx["var"] = 2
132
+ print(ct.var) # print 2
133
+
134
+ Accessing the variables can also be done like an object attribute or a Dict key:
135
+ ctx = AppConfig()
136
+ ctx.var = 2
137
+ print(ctx["var"]) # print 2
138
+
139
+ This can be useful to mix known variable name and variable dynamically defined in a file or passed from the command line.
140
+ def foo(ctx: AppConfig,attname:Any) -> Any:
141
+ return ctx[attname]
142
+
143
+ Variable can contain variable recursively, in this case they can be access with a dotted notation or with a dict like way:
144
+ ctx = AppConfig()
145
+ ctx["var1.var2"] = 2
146
+ ctx["var1.var3"] = 3
147
+ print(f"var1 = {ctx.var1}") # print a dict {"var2":2,"var3":3}
148
+
149
+ print(ctx.var1.var2) # print 2
150
+
151
+ Declarative Configuration:
152
+ --------------------------
153
+ Configuration is best defined declaratively by subclassing AppConfig:
154
+
155
+ class MyConfig(AppConfig):
156
+ host: str = configvar(Default="localhost")
157
+ port: int = configvar(Default=8080)
158
+
159
+ Nested Namespaces:
160
+ ------------------
161
+ Variables can be grouped into namespaces using nested classes inheriting from NameSpace.
162
+ This creates a structured configuration:
163
+
164
+ class MyConfig(AppConfig):
165
+ # Root level variable
166
+ debug: bool = configvar(Default=False)
167
+
168
+ # Nested namespace 'db'
169
+ class db(NameSpace):
170
+ host: str = configvar(Default="db.local")
171
+ port: int = configvar(Default=5432)
172
+
173
+ # Nested namespace 'api'
174
+ class api(NameSpace):
175
+ key: str = configvar(EnvName="API_KEY")
176
+
177
+ inst = MyConfig()
178
+ inst.resolve_vars()
179
+ print(inst.db.host) # Access nested variables
180
+
181
+ Decentralized Configuration (Mixins):
182
+ -------------------------------------
183
+ Configuration can be split across multiple classes (Mixins) and combined using inheritance.
184
+ This allows different modules to define their own configuration requirements.
185
+
186
+ class DatabaseConfig(AppConfig):
187
+ class db(NameSpace):
188
+ host: str = configvar(Default="localhost")
189
+
190
+ class ApiConfig(AppConfig):
191
+ class api(NameSpace):
192
+ timeout: int = configvar(Default=30)
193
+
194
+ # Combine into the final application config
195
+ class App(DatabaseConfig, ApiConfig):
196
+ pass
197
+
198
+ If multiple mixins define the same namespace (e.g. `class db(NameSpace)`), their variables are merged.
199
+ """
200
+
201
+ #: The logger for this class
202
+ _logger = logging.getLogger(__name__)
203
+ _solver_logger = logging.getLogger(f"{__name__}.solver")
204
+ _global_flags: Flags
205
+ _config_var_defs: list[ConfigVarDef]
206
+
207
+ def __init_subclass__(cls, **kwargs: Any) -> None:
208
+ """
209
+ Call when a subclass inherits from AppConfig.
210
+
211
+ Create the _config_var_defs attribute that contains the list of ConfigVarDef
212
+ defined in this subclass as well as the ones defined in parent classes.
213
+ """
214
+ super().__init_subclass__(**kwargs)
215
+
216
+ # First try to look if there is already a _config_var_defs attribute in one of the parent classes.
217
+ collected: list[ConfigVarDef] = []
218
+ seen_attr_names: set[str] = set()
219
+ for base in reversed(cls.__mro__[1:]):
220
+ # from the deepest parent class to the nearest parent class: search if there
221
+ # is a _config_var_defs attribute in the inspected class.
222
+ base_defs = getattr(base, "_config_var_defs", None)
223
+ if not base_defs:
224
+ continue
225
+ for var_def in base_defs:
226
+ # base_defs is the _config_var_defs list. Loops on each of its elements and store them in the collected list
227
+ # For each one read its name. If the variable name is already in the collected list, it means that it has been
228
+ # defined in a parent class of the current inspected base class. So we remove the old definition and add the new one.
229
+ attr_name = getattr(var_def, "_name", None)
230
+ if attr_name:
231
+ if attr_name in seen_attr_names:
232
+ collected = [
233
+ v for v in collected if getattr(v, "_name", None) != attr_name
234
+ ]
235
+ collected.append(var_def)
236
+ seen_attr_names.add(attr_name)
237
+
238
+ # Helper method for recursive collection
239
+ def collect_vars(owner: type, prefix: str = "") -> Iterable[ConfigVarDef]:
240
+ # extract docstrings from the class source code
241
+ docstrings = _get_attribute_docstrings(owner)
242
+
243
+ # Inspect the class __dict__ to find ConfigVarDef defined in this class
244
+ for attr_name, value in owner.__dict__.items():
245
+ if isinstance(value, ConfigVarDef):
246
+ # Clone to update name with prefix
247
+ # We use copy.copy because ConfigVarDef is mutable but generic.
248
+ new_var = copy.copy(value)
249
+
250
+ # If Help is missing, try to use the docstring
251
+ if not new_var.Help and attr_name in docstrings:
252
+ new_var._help = docstrings[attr_name]
253
+
254
+ if prefix:
255
+ # We need to access _name directly
256
+ original_name = getattr(value, "_name", attr_name)
257
+ new_var._name = f"{prefix}{original_name}"
258
+
259
+ yield new_var
260
+
261
+ elif (
262
+ isinstance(value, type)
263
+ and issubclass(value, NameSpace)
264
+ and value is not NameSpace
265
+ ):
266
+ # Recurse
267
+ yield from collect_vars(value, prefix=f"{prefix}{attr_name}.")
268
+
269
+ # Now inspect the class __dict__ to find ConfigVarDef defined in this class (and nested classes) and add them to the collected list.
270
+ for value in collect_vars(cls):
271
+ attr_name = getattr(value, "_name", None)
272
+ if attr_name:
273
+ if attr_name in seen_attr_names:
274
+ collected = [
275
+ v for v in collected if getattr(v, "_name", None) != attr_name
276
+ ]
277
+ collected.append(value)
278
+ seen_attr_names.add(attr_name)
279
+
280
+ cls._config_var_defs = collected
281
+
282
+ def __init__(
283
+ self,
284
+ *,
285
+ no_env_search: Optional[bool] = None,
286
+ no_key_search: Optional[bool] = None,
287
+ no_conffile_search: Optional[bool] = None,
288
+ no_search: Optional[bool] = None,
289
+ conffile_path: Optional[Path] = None,
290
+ allow_override: Optional[bool] = None,
291
+ click_key_conversion: Optional[bool] = None,
292
+ ):
293
+ """Init the config.
294
+
295
+ Arguments are called 'global flags', they specify the behavior of the variable resolution globaly. They can be overiden
296
+ when the resolve method is call. (this method can be call several time and thus on each call some flags can be changed) or
297
+ per variable when defining the ConfigVarDef object.
298
+
299
+ Global Flags:
300
+ no_env_search (bool, optional): don't search values in the environment. Defaults to False.
301
+ no_key_search (bool, optional): don't search values in the value dictionary. Defaults to False.
302
+ no_conffile_search (bool, optional): don't search values in the configuration file. Defaults to False.
303
+ no_search (bool, optional): don't search values in any location. Defaults to False.
304
+ conffile_path (Path, optional): the configuration file path. Defaults to None.
305
+ allow_override (bool, optional): allow overriding variable in the configuration when calling resolve_vars.
306
+ it happens either when resolve_vars() is call several times or the variable was set before its call.
307
+ click_key_conversion (bool, optional): use the variable name converted to lower case and with dashes converted to underscores as
308
+ the key to search in values. Defaults to False.
309
+ """
310
+ self._global_flags = Flags(
311
+ no_env_search=no_env_search,
312
+ no_value_search=no_key_search,
313
+ no_conffile_search=no_conffile_search,
314
+ no_search=no_search,
315
+ click_key_conversion=click_key_conversion,
316
+ allow_override=allow_override,
317
+ )
318
+ self.conffile_path = conffile_path
319
+ # Configuration variable definitions used by resolve_vars().
320
+ # This is intentionally an instance attribute so callers can set/replace it
321
+ # dynamically (major-version breaking change: resolve_vars no longer accepts
322
+ # a var_specs parameter).
323
+ # Default to the declarative definitions collected on the class.
324
+ self._config_var_defs = list(getattr(self.__class__, "_config_var_defs", []))
325
+ self._logger.debug("init AppCtx")
326
+
327
+ def clone(self) -> AppConfig:
328
+ """cloning the AppConfig object"""
329
+ clone = AppConfig()
330
+ clone.__dict__ = self.__dict__.copy()
331
+ return clone
332
+
333
+ @classmethod
334
+ def _find_var_in_dict(cls, where: Mapping[Any, Any], var_name: str) -> Any | Undefined:
335
+ """
336
+ Search for var_name in a dictionary.
337
+ var_name contains dict keys separated by a dot (.) ie key1.key2.key3
338
+ The left side key key1 is search in the where directory, if it is found
339
+ and it is a dictionary then key2 is search in it and
340
+ all key are searched in sequence.
341
+ Returns _undef if not found, allowing to distinguish between None (explicit null) and missing.
342
+ """
343
+ cur_dic: Mapping[Any, Any] = where
344
+ for key in var_name.split("."):
345
+ # logger = logging.getLogger(f"{cls.__module__}.{cls.__name__}")
346
+ # logger.debug(f"searching for key: {key} in dict : {cur_val}")
347
+ if key in cur_dic:
348
+ cur_dic = cur_dic[key]
349
+ # logger.debug(f"Found key: {key} = {cur_val}")
350
+ else:
351
+ return _undef
352
+
353
+ return cur_dic
354
+
355
+ def _run_eval(
356
+ self,
357
+ callable: Callable[[str, Any, Any], Any],
358
+ var_val: Any = None,
359
+ var_name: str = "",
360
+ ) -> Any:
361
+ """
362
+ Call a Callable found in a Eval Form.
363
+ """
364
+ return callable(var_name, self, var_val)
365
+
366
+ def _set_value(self, name: str, value: Any) -> None:
367
+ """Set a value.
368
+
369
+ Depending on AttrDict implementation to handle dotted keys and not trigger recursion loops
370
+ if called from a descriptor.
371
+ """
372
+ self[name] = value
373
+
374
+ def resolve_vars(
375
+ self,
376
+ *,
377
+ contexts: Optional[str | Iterable[str]] = None,
378
+ values: Mapping[str, Any] = {},
379
+ no_exception: Optional[bool] = None,
380
+ no_env_search: Optional[bool] = None,
381
+ no_value_search: Optional[bool] = None,
382
+ no_conffile_search: Optional[bool] = None,
383
+ no_search: Optional[bool] = None,
384
+ conffile_path: Optional[Path] = None,
385
+ allow_override: Optional[bool] = None,
386
+ click_key_conversion: Optional[bool] = None,
387
+ ) -> Any:
388
+ """
389
+ Resolve the configuration variables defined in this AppConfig object.
390
+ The variable values are searched in these locations in the following order:
391
+ - a dict of values usually passed from the command line arguments
392
+ - environment variables
393
+ - a configuration file in yaml or json format
394
+ - a default value or a computed value
395
+ The first match found is used as the value for the variable.
396
+ When found the variable can be casted to a specific type and/or transformed with a callable.
397
+ The default value can also be computed with a callable that can use already defined variables in this AppConfig object.
398
+ One can combine a default value and a transformation callable to compute complex default values that can combine several already defined variables,
399
+ knowing that variables are resolved in the order they are declared in the AppConfig subclass.
400
+ """
401
+
402
+ self._logger.info(
403
+ f"Starting variable resolution, conf file = {str(conffile_path)} "
404
+ )
405
+
406
+ # Compute flags that can be override locally.
407
+ resolve_flags = Flags(
408
+ no_env_search=no_env_search,
409
+ no_value_search=no_value_search,
410
+ no_conffile_search=no_conffile_search,
411
+ no_search=no_search,
412
+ click_key_conversion=click_key_conversion,
413
+ allow_override=allow_override,
414
+ no_exception=no_exception,
415
+ )
416
+ local_flags = self._global_flags.merge(inplace=False, other=resolve_flags)
417
+ assert local_flags is not None
418
+
419
+ # Read configuration file if requiered
420
+ conf_file_vars = None
421
+ cur_conffile_path = conffile_path if conffile_path else self.conffile_path
422
+ if not local_flags.no_conffile_search and cur_conffile_path is not None:
423
+ if cur_conffile_path.is_file():
424
+ with cur_conffile_path.open("r") as f:
425
+ if cur_conffile_path.suffix.lower() == ".json":
426
+ self._logger.debug(f"Parsing json file {cur_conffile_path}")
427
+ conf_file_vars = json.load(f)
428
+ else:
429
+ self._logger.debug(
430
+ f"Parsing file {cur_conffile_path} with suffix {cur_conffile_path.suffix.lower()}"
431
+ )
432
+ conf_file_vars = yaml.safe_load(f)
433
+ else:
434
+ self._logger.info(f"configuration file {cur_conffile_path} not found.")
435
+
436
+ # Read the var definitions one by one in their definition order
437
+ for var in self._config_var_defs:
438
+ # compute the current flags for this variable by merging local flags with variable flags
439
+ cur_flags = local_flags.merge(inplace=False, other=var.flags)
440
+ assert cur_flags is not None
441
+ # self.logger.debug(f"looking for a value for variable: {var.Name}")
442
+
443
+ self._solver_logger.debug(f"Searching for var name {var.Name}:\n{str(var)}")
444
+
445
+ # if we are resolving the variables in some specific contexts (ie parameter context has value)
446
+ # we check if the variable belongs to one of these contexts and if not we skip it.
447
+ if contexts is None and var.Contexts is not None:
448
+ continue
449
+ if contexts is not None and var.Contexts is not None:
450
+ test_ctx = []
451
+ # put True in test_ctx for each context that matches, False otherwise
452
+ if is_sequence(contexts):
453
+ test_ctx = [c in var.Contexts for c in contexts]
454
+ else:
455
+ test_ctx = [contexts in var.Contexts]
456
+
457
+ # test if there is any True in test_ctx which means that at least one context matches
458
+ if not any(test_ctx):
459
+ continue
460
+
461
+ if var.Contexts is None and contexts is not None:
462
+ # variable without context are common to all contexts so we process them but only once.
463
+ if var.Name in self.__dict__:
464
+ continue
465
+
466
+ # if the variable is already defined and override is not allowed then
467
+ # we skip it (First valid context wins).
468
+ if not cur_flags.allow_override and var.Name in self.__dict__:
469
+ continue
470
+
471
+ val_found: str | None | list[Any] | Undefined = _undef
472
+ found = False
473
+
474
+ # Searching variable value in the values mapping
475
+ if (
476
+ values is not None
477
+ and not cur_flags.no_value_search
478
+ and var.ValueKey is not None
479
+ ):
480
+ # searching var key
481
+ if var.ValueKey in values and values[var.ValueKey] is not None:
482
+ val_found = values[var.ValueKey]
483
+ found = True
484
+ self._solver_logger.debug(
485
+ f"{var.Name} -> Found in Values = {val_found}"
486
+ )
487
+ if not found and cur_flags.click_key_conversion:
488
+ # try with click key conversion
489
+ click_key = var.ValueKey.lower().replace("-", "_")
490
+ if click_key in values and values[click_key] is not None:
491
+ val_found = values[click_key]
492
+ found = True
493
+ self._solver_logger.debug(
494
+ f"{var.Name} -> Found in Values with Click Key Conversion = {val_found}"
495
+ )
496
+ # Searching variable value in the environment variables
497
+ if not found and not cur_flags.no_env_search and var.EnvName is not None:
498
+ env_val = os.getenv(var.EnvName)
499
+ if env_val is not None:
500
+ val_found = env_val
501
+ found = True
502
+ self._solver_logger.debug(f"{var.Name} -> Found in Env = {val_found}")
503
+ # Searching variable value in the configuration file
504
+ if (
505
+ not found
506
+ and not cur_flags.no_conffile_search
507
+ and conf_file_vars is not None
508
+ and var.FileKey is not None
509
+ ):
510
+ file_val = self._find_var_in_dict(conf_file_vars, var.FileKey)
511
+ if file_val is not _undef:
512
+ val_found = file_val
513
+ found = True
514
+ self._solver_logger.debug(
515
+ f"{var.Name} -> Found in Configuration File = {val_found}"
516
+ )
517
+ # Setting variable value to the default value if defined.
518
+ if not found and var.Default is not _undef:
519
+ if var.Default is not None and callable(var.Default):
520
+ val_found = self._run_eval(callable=var.Default, var_name=var.Name)
521
+ else:
522
+ val_found = var.Default
523
+ found = True
524
+ self._solver_logger.debug(
525
+ f"{var.Name} -> Found in Default Value = {val_found}"
526
+ )
527
+
528
+ # Raise exception if no value was found and no_exception is True
529
+ if not found and not cur_flags.no_exception:
530
+ raise AppConfigException(
531
+ f"No value for var {var.Name} in context {contexts}"
532
+ )
533
+
534
+ if found:
535
+ if isinstance(val_found, _UndefinedSentinel):
536
+ raise AssertionError(
537
+ "Internal error: val_found is _undef while found=True"
538
+ )
539
+ # spliting lists
540
+ if (
541
+ var.SplitToList
542
+ and val_found is not None
543
+ and isinstance(val_found, str)
544
+ ):
545
+ sep: str = (
546
+ "," if isinstance(var.SplitToList, bool) else var.SplitToList
547
+ )
548
+ val_found = val_found.split(sep)
549
+
550
+ # Transform the variable value if specified.
551
+ if var.Transform is not None:
552
+ val_transfo = self._run_eval(
553
+ callable=var.Transform,
554
+ var_val=val_found,
555
+ var_name=var.Name,
556
+ )
557
+ self._solver_logger.debug(
558
+ f"{var.Name} -> Value Transformed: {val_found} => {val_transfo}"
559
+ )
560
+ val_found = val_transfo
561
+
562
+ # Validate and cast the variable value using pydantic TypeAdapter
563
+ if var.TypeInfo is not None and val_found is not None:
564
+ try:
565
+ ta = TypeAdapter(var.TypeInfo)
566
+ val_found = ta.validate_python(val_found)
567
+ except ValidationError as e:
568
+ raise AppConfigException(f"Validation failed for var {var.Name}: {e}")
569
+
570
+ self._set_value(var.Name, val_found)
571
+ elif var.Name not in self.__dict__:
572
+ # in case of overiding and when on subsenquent call no value were found we don't want to erase a value
573
+ # found on a previous run. This can happen when some global flags are overriden or for variable without
574
+ # context.
575
+ self._set_value(var.Name, None)
576
+
577
+ # Make special treatment for paths
578
+ if (
579
+ not var.NoDirProcessing
580
+ and var.TypeInfo == Path
581
+ and self[var.Name] != None
582
+ ):
583
+ var_value = self[var.Name]
584
+ new_val: list[Path] | Path | None = None
585
+ try:
586
+ if iter(var_value):
587
+ # we need to consider the case where the variable is a list of Path
588
+ new_val = []
589
+ for val in var_value:
590
+ res = self._process_paths(value=val, var=var)
591
+ new_val.append(res)
592
+ except Exception:
593
+ res = self._process_paths(value=var_value, var=var)
594
+ new_val = res
595
+ self._set_value(var.Name, new_val)
596
+
597
+ def _process_paths(self, *, value: Any, var: ConfigVarDef) -> Path:
598
+ """
599
+ Run the path processing for a single Path variable.
600
+
601
+ """
602
+
603
+ var_value: Path
604
+ if isinstance(value, str):
605
+ var_value = Path(value)
606
+ elif isinstance(value, Path):
607
+ var_value = value
608
+ else:
609
+ raise Exception("not a path")
610
+
611
+ res: Path = var_value
612
+ # First process the CanBeRelative case.
613
+ if var.CanBeRelativeTo is not None:
614
+
615
+ if var_value.is_absolute():
616
+ res = var_value
617
+ else:
618
+ # resolving the root directory from which to be relative to.
619
+ can_be_relative = var.CanBeRelativeTo
620
+ root_dir: Path
621
+ if isinstance(can_be_relative, str) and can_be_relative in self.__dict__:
622
+ root_dir = Path(self[can_be_relative])
623
+ else:
624
+ root_dir = Path(can_be_relative)
625
+ res = root_dir.joinpath(var_value)
626
+
627
+ # Expanding user home and resolving dots in path
628
+ res = res.expanduser().resolve()
629
+
630
+ # create directory if MakeDir is not None
631
+ if var.MakeDirs:
632
+ if var.MakeDirs == PathType.Dir:
633
+ res.mkdir(parents=True, exist_ok=True)
634
+ else:
635
+ res.parent.mkdir(parents=True, exist_ok=True)
636
+ return res
637
+
638
+ def __repr__(self) -> str:
639
+ mess = ["App Context:"]
640
+ for cur_var, cur_val in self.__dict__.items():
641
+ mess.append(f"\t{cur_var}:\t{cur_val}")
642
+
643
+ return "\n".join(mess)