config-cli-gui 0.1.9__tar.gz → 0.2.0__tar.gz

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.
Files changed (78) hide show
  1. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/HISTORY.md +17 -0
  2. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/PKG-INFO +1 -1
  3. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/README.md +35 -24
  4. config_cli_gui-0.2.0/config.yaml +48 -0
  5. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/docs/usage/config.md +1 -1
  6. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/src/config_cli_gui/_version.py +3 -3
  7. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/src/config_cli_gui/cli.py +1 -1
  8. config_cli_gui-0.2.0/src/config_cli_gui/config.py +245 -0
  9. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/src/config_cli_gui/gui.py +17 -16
  10. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/src/config_cli_gui.egg-info/PKG-INFO +1 -1
  11. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/src/config_cli_gui.egg-info/SOURCES.txt +2 -0
  12. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/example_project/core/logging.py +6 -6
  13. config_cli_gui-0.2.0/tests/example_project/gui/config.yaml +48 -0
  14. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/example_project/gui/gui_example.py +6 -6
  15. config_cli_gui-0.2.0/tests/test_config_manager.py +203 -0
  16. config_cli_gui-0.2.0/tests/test_docs.py +230 -0
  17. config_cli_gui-0.1.9/config.yaml +0 -51
  18. config_cli_gui-0.1.9/src/config_cli_gui/config.py +0 -245
  19. config_cli_gui-0.1.9/tests/example_project/gui/config.yaml +0 -51
  20. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.github/FUNDING.yml +0 -0
  21. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  22. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  23. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  24. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.github/actions/setup-environment/action.yml +0 -0
  25. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.github/dependabot.yml +0 -0
  26. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.github/init.sh +0 -0
  27. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.github/release_message.sh +0 -0
  28. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.github/update_funding.py +0 -0
  29. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.github/workflows/main.yml +0 -0
  30. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.github/workflows/release.yml +0 -0
  31. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.github/workflows/update_readme.yml +0 -0
  32. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.gitignore +0 -0
  33. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.idea/runConfigurations/config_generate.xml +0 -0
  34. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.pre-commit-config.yaml +0 -0
  35. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/.readthedocs.yaml +0 -0
  36. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/LICENSE +0 -0
  37. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/Makefile +0 -0
  38. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/docs/.nav.yml +0 -0
  39. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/docs/_static/img/favicon.png +0 -0
  40. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/docs/_static/img/logo.png +0 -0
  41. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/docs/css/custom.css +0 -0
  42. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/docs/develop/contributing.md +0 -0
  43. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/docs/develop/make_windows.md +0 -0
  44. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/docs/develop/naming_convention.md +0 -0
  45. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/docs/funding/funding.md +0 -0
  46. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/docs/getting-started/install.md +0 -0
  47. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/docs/getting-started/virtual-environment.md +0 -0
  48. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/docs/index.md +0 -0
  49. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/docs/usage/cli.md +0 -0
  50. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/mkdocs.yml +0 -0
  51. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/pyproject.toml +0 -0
  52. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/scripts/show_filelist.ps1 +0 -0
  53. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/scripts/show_tree.ps1 +0 -0
  54. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/scripts/show_tree.py +0 -0
  55. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/scripts/update_readme.py +0 -0
  56. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/setup.cfg +0 -0
  57. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/src/__init__.py +0 -0
  58. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/src/config_cli_gui/__init__.py +0 -0
  59. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/src/config_cli_gui/docs.py +0 -0
  60. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/src/config_cli_gui.egg-info/dependency_links.txt +0 -0
  61. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/src/config_cli_gui.egg-info/entry_points.txt +0 -0
  62. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/src/config_cli_gui.egg-info/requires.txt +0 -0
  63. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/src/config_cli_gui.egg-info/top_level.txt +0 -0
  64. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/template.yml.url +0 -0
  65. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/__init__.py +0 -0
  66. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/example_project/__init__.py +0 -0
  67. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/example_project/__main__.py +0 -0
  68. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/example_project/cli/__init__.py +0 -0
  69. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/example_project/cli/__main__.py +0 -0
  70. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/example_project/cli/cli_example.py +0 -0
  71. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/example_project/config/__init__.py +0 -0
  72. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/example_project/config/config_example.py +0 -0
  73. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/example_project/core/__init__.py +0 -0
  74. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/example_project/core/base.py +0 -0
  75. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/example_project/gui/__init__.py +0 -0
  76. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/example_project/gui/__main__.py +0 -0
  77. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/tests/test_generic_cli.py +0 -0
  78. {config_cli_gui-0.1.9 → config_cli_gui-0.2.0}/uv.lock +0 -0
@@ -4,6 +4,23 @@ Changelog
4
4
 
5
5
  (unreleased)
6
6
  ------------
7
+ - #9 fix posix path issue. [Paul Magister]
8
+ - #9 fix posix path issue. [Paul Magister]
9
+
10
+
11
+ 0.1.10 (2025-11-13)
12
+ -------------------
13
+ - Docs: Update HISTORY.md for release 0.1.10. [Paul Magister]
14
+ - #9 unittests docs.py. [Paul Magister]
15
+ - #9 unittests config.py. [Paul Magister]
16
+ - #9 roll back to .value usage to be type save: documentation. [Paul
17
+ Magister]
18
+ - #9 roll back to .value usage to be type save. [Paul Magister]
19
+
20
+
21
+ 0.1.9 (2025-11-12)
22
+ ------------------
23
+ - Docs: Update HISTORY.md for release 0.1.9. [Paul Magister]
7
24
  - #8 fix bug with .value. [Paul Magister]
8
25
 
9
26
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: config-cli-gui
3
- Version: 0.1.9
3
+ Version: 0.2.0
4
4
  Summary: Feature-rich Python project template for config-cli-gui.
5
5
  Author: pamagister
6
6
  Requires-Python: <3.12,>=3.10
@@ -58,11 +58,16 @@ Start by defining your application's configuration parameters in a central `conf
58
58
  ```python
59
59
  # my_project/config_example.py
60
60
 
61
+ from datetime import datetime
62
+ from pathlib import Path
63
+
61
64
  from config_cli_gui.config import (
65
+ Color,
62
66
  ConfigCategory,
63
67
  ConfigManager,
64
68
  ConfigParameter,
65
69
  )
70
+ from config_cli_gui.docs import DocumentationGenerator
66
71
 
67
72
 
68
73
  class CliConfig(ConfigCategory):
@@ -74,55 +79,61 @@ class CliConfig(ConfigCategory):
74
79
  # Positional argument
75
80
  input: ConfigParameter = ConfigParameter(
76
81
  name="input",
77
- default="",
82
+ value="",
78
83
  help="Path to input (file or folder)",
79
84
  required=True,
80
85
  is_cli=True,
81
86
  )
82
87
 
83
- min_dist: ConfigParameter = ConfigParameter(
84
- name="min_dist",
85
- default=20,
86
- help="Maximum distance between two waypoints",
88
+ # Optional CLI arguments
89
+ output: ConfigParameter = ConfigParameter(
90
+ name="output",
91
+ value="",
92
+ help="Path to output destination",
87
93
  is_cli=True,
88
94
  )
89
95
 
90
- extract_waypoints: ConfigParameter = ConfigParameter(
91
- name="extract_waypoints",
92
- default=True,
93
- help="Extract starting points of each track as waypoint",
96
+ min_dist: ConfigParameter = ConfigParameter(
97
+ name="min_dist",
98
+ value=20,
99
+ help="Maximum distance between two waypoints",
94
100
  is_cli=True,
95
101
  )
96
102
 
97
103
 
98
- class AppConfig(ConfigCategory):
99
- """Application-specific configuration parameters."""
100
-
104
+ class MiscConfig(ConfigCategory):
101
105
  def get_category_name(self) -> str:
102
- return "app"
106
+ return "misc"
107
+
108
+ some_file: ConfigParameter = ConfigParameter(
109
+ name="some_file",
110
+ value=Path("some_file.txt"),
111
+ help="Path to the file to use",
112
+ )
103
113
 
104
- log_level: ConfigParameter = ConfigParameter(
105
- name="log_level",
106
- default="INFO",
107
- choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
108
- help="Logging level for the application",
114
+ some_color: ConfigParameter = ConfigParameter(
115
+ name="some_color",
116
+ value=Color(255, 0, 0),
117
+ help="Color setting for the application",
109
118
  )
110
119
 
111
- log_file_max_size: ConfigParameter = ConfigParameter(
112
- name="log_file_max_size",
113
- default=10,
114
- help="Maximum log file size in MB before rotation",
120
+ some_date: ConfigParameter = ConfigParameter(
121
+ name="some_date",
122
+ value=datetime.now(),
123
+ help="Date setting for the application",
115
124
  )
116
125
 
117
126
 
118
127
  class ProjectConfigManager(ConfigManager): # Inherit from ConfigManager
119
128
  """Main configuration manager that handles all parameter categories."""
120
129
 
121
- categories = (CliConfig(), AppConfig())
130
+ cli: CliConfig
131
+ misc: MiscConfig
122
132
 
123
133
  def __init__(self, config_file: str | None = None, **kwargs):
124
134
  """Initialize the configuration manager with all parameter categories."""
125
- super().__init__(self.categories, config_file, **kwargs)
135
+ categories = (CliConfig(), MiscConfig())
136
+ super().__init__(categories, config_file, **kwargs)
126
137
 
127
138
 
128
139
  ```
@@ -0,0 +1,48 @@
1
+ app:
2
+ # Date format to use | type=str, default=%Y-%m-%d
3
+ date_format: '%Y-%m-%d'
4
+ # Enable logging to console | type=bool, default=True
5
+ enable_console_logging: true
6
+ # Enable logging to file | type=bool, default=True
7
+ enable_file_logging: true
8
+ # Number of backup log files to keep | type=int, default=5
9
+ log_backup_count: 5
10
+ # Maximum log file size in MB before rotation | type=int, default=10
11
+ log_file_max_size: 10
12
+ # Log message format style | type=str, default=detailed
13
+ log_format: detailed
14
+ # Logging level for the application | type=str, default=INFO
15
+ log_level: INFO
16
+ # Maximum number of worker threads | type=int, default=4
17
+ max_workers: 4
18
+ cli:
19
+ # Include elevation data in waypoints | type=bool, default=True [CLI]
20
+ elevation: true
21
+ # Extract starting points of each track as waypoint | type=bool, default=True [CLI]
22
+ extract_waypoints: true
23
+ # Path to input (file or folder) | type=str, default= [CLI]
24
+ input: ''
25
+ # Maximum distance between two waypoints | type=int, default=20 [CLI]
26
+ min_dist: 20
27
+ # Path to output destination | type=str, default= [CLI]
28
+ output: ''
29
+ gui:
30
+ # Automatically scroll to the newest log entries | type=bool, default=True
31
+ auto_scroll_log: true
32
+ # Height of the log window in pixels | type=int, default=200
33
+ log_window_height: 200
34
+ # Maximum number of log lines to keep in GUI | type=int, default=1000
35
+ max_log_lines: 1000
36
+ # GUI theme setting | type=str, default=light
37
+ theme: light
38
+ # Default window height | type=int, default=600
39
+ window_height: 600
40
+ # Default window width | type=int, default=800
41
+ window_width: 800
42
+ misc:
43
+ # Color setting for the application | type=Color, default=#ff0000
44
+ some_color: '#ff0000'
45
+ # Date setting for the application | type=datetime, default=2025-11-13 22:49:38.801511
46
+ some_date: '2025-11-13T22:49:38.801511'
47
+ # Path to the file to use | type=PosixPath, default=some_file.txt
48
+ some_file: some_file.txt
@@ -43,5 +43,5 @@ The parameters in the cli category can be accessed via the command line interfac
43
43
  |------------|-----------|-----------------------------------|-----------------------------------------------------|---------|
44
44
  | some_file | PosixPath | Path to the file to use | PosixPath('some_file.txt') | - |
45
45
  | some_color | Color | Color setting for the application | Color(255, 0, 0) | - |
46
- | some_date | datetime | Date setting for the application | datetime.datetime(2025, 11, 12, 22, 24, 14, 121460) | - |
46
+ | some_date | datetime | Date setting for the application | datetime.datetime(2025, 11, 13, 22, 49, 38, 801511) | - |
47
47
 
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.1.9'
32
- __version_tuple__ = version_tuple = (0, 1, 9)
31
+ __version__ = version = '0.2.0'
32
+ __version_tuple__ = version_tuple = (0, 2, 0)
33
33
 
34
- __commit_id__ = commit_id = 'gc9df12403'
34
+ __commit_id__ = commit_id = 'gd5e29e239'
@@ -130,7 +130,7 @@ class CliGenerator:
130
130
  updated_config.add_category(name, category)
131
131
 
132
132
  # Apply overrides again after copying categories
133
- updated_config._apply_kwargs(cli_overrides)
133
+ updated_config.apply_overrides(cli_overrides)
134
134
 
135
135
  # Try to get logger if logging is configured
136
136
  try:
@@ -0,0 +1,245 @@
1
+ import json
2
+ from abc import ABC, abstractmethod
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import yaml
9
+ from pydantic import BaseModel
10
+
11
+
12
+ class Color:
13
+ """Simple color class for RGB values."""
14
+
15
+ def __init__(self, r: int = 0, g: int = 0, b: int = 0):
16
+ self.r = max(0, min(255, r))
17
+ self.g = max(0, min(255, g))
18
+ self.b = max(0, min(255, b))
19
+
20
+ def to_list(self) -> list[int]:
21
+ return [self.r, self.g, self.b]
22
+
23
+ def to_hex(self) -> str:
24
+ return f"#{self.r:02x}{self.g:02x}{self.b:02x}"
25
+
26
+ @classmethod
27
+ def from_list(cls, rgb_list: list[int | str]) -> "Color":
28
+ if len(rgb_list) >= 3:
29
+ return cls(int(rgb_list[0]), int(rgb_list[1]), int(rgb_list[2]))
30
+ return cls()
31
+
32
+ @classmethod
33
+ def from_hex(cls, hex_color: str) -> "Color":
34
+ hex_color = hex_color.lstrip("#")
35
+ if len(hex_color) == 6:
36
+ return cls(
37
+ int(hex_color[0:2], 16),
38
+ int(hex_color[2:4], 16),
39
+ int(hex_color[4:6], 16),
40
+ )
41
+ return cls()
42
+
43
+ def __str__(self):
44
+ return self.to_hex()
45
+
46
+ def __repr__(self):
47
+ return f"Color({self.r}, {self.g}, {self.b})"
48
+
49
+
50
+ @dataclass
51
+ class ConfigParameter:
52
+ """Represents a single configuration parameter with metadata."""
53
+
54
+ name: str
55
+ value: Any
56
+ choices: list | tuple | None = None
57
+ help: str = ""
58
+ cli_arg: str | None = None
59
+ required: bool = False
60
+ is_cli: bool = False
61
+ category: str = "general"
62
+
63
+ def __post_init__(self):
64
+ if self.is_cli and self.cli_arg is None and not self.required:
65
+ self.cli_arg = f"--{self.name}"
66
+ if isinstance(self.value, bool) and self.choices is None:
67
+ self.choices = [True, False]
68
+
69
+ @property
70
+ def type_(self) -> type:
71
+ """Return the Python type of this parameter’s value."""
72
+ return type(self.value)
73
+
74
+
75
+ class ConfigCategory(BaseModel, ABC):
76
+ """Base class for configuration categories."""
77
+
78
+ @abstractmethod
79
+ def get_category_name(self) -> str:
80
+ pass
81
+
82
+ def get_parameters(self) -> list[ConfigParameter]:
83
+ """Return ConfigParameter instances that are actual instance attributes."""
84
+ params = []
85
+
86
+ for value in vars(self).values(): # faster, only instance attrs
87
+ if isinstance(value, ConfigParameter):
88
+ value.category = self.get_category_name()
89
+ params.append(value)
90
+
91
+ return params
92
+
93
+
94
+ class ConfigManager:
95
+ """Generic configuration manager handling multiple configuration categories."""
96
+
97
+ def __init__(
98
+ self,
99
+ categories: tuple[ConfigCategory, ...],
100
+ config_file: str | None = None,
101
+ **overrides: Any,
102
+ ):
103
+ self._categories: dict[str, ConfigCategory] = {}
104
+
105
+ # Register categories and expose them as attributes
106
+ for category in categories:
107
+ if not isinstance(category, ConfigCategory):
108
+ raise TypeError(f"Expected ConfigCategory instance, got {type(category)}")
109
+ self.add_category(category.get_category_name(), category)
110
+
111
+ # Load configuration from file if provided
112
+ if config_file:
113
+ self.load_from_file(config_file)
114
+
115
+ # Apply overrides (category__param=value)
116
+ self.apply_overrides(overrides)
117
+
118
+ def add_category(self, name: str, category: ConfigCategory):
119
+ self._categories[name] = category
120
+ setattr(self, name, category)
121
+
122
+ def get_category(self, name: str) -> ConfigCategory | None:
123
+ return self._categories.get(name)
124
+
125
+ def apply_overrides(self, overrides: dict[str, Any]):
126
+ """Apply keyword overrides in format category__param=value."""
127
+ for key, value in overrides.items():
128
+ if "__" not in key:
129
+ continue
130
+ category_name, param_name = key.split("__", 1)
131
+ category = self._categories.get(category_name)
132
+ if category and hasattr(category, param_name):
133
+ param = getattr(category, param_name)
134
+ if isinstance(param, ConfigParameter):
135
+ param.value = value
136
+ else:
137
+ setattr(category, param_name, value)
138
+
139
+ def load_from_file(self, config_file: str):
140
+ path = Path(config_file)
141
+ if not path.exists():
142
+ raise FileNotFoundError(f"Configuration file not found: {config_file}")
143
+
144
+ with open(path, "r", encoding="utf-8") as f:
145
+ data = yaml.safe_load(f) if path.suffix in [".yml", ".yaml"] else json.load(f)
146
+
147
+ self._apply_config_data(data)
148
+
149
+ def _apply_config_data(self, data: dict):
150
+ for category_name, category_data in data.items():
151
+ category = self._categories.get(category_name)
152
+ if not category:
153
+ continue
154
+ for param_name, param_value in category_data.items():
155
+ param: ConfigParameter = getattr(category, param_name, None)
156
+ if not isinstance(param, ConfigParameter):
157
+ continue
158
+
159
+ # Type conversions
160
+ if isinstance(param.value, Color) and isinstance(param_value, list):
161
+ param_value = Color.from_list(param_value)
162
+ if isinstance(param.value, Color) and isinstance(param_value, str):
163
+ param_value = Color.from_hex(param_value)
164
+ elif isinstance(param.value, Path):
165
+ param_value = Path(param_value)
166
+ elif isinstance(param.value, datetime):
167
+ param_value = datetime.fromisoformat(param_value)
168
+
169
+ param.value = param_value
170
+
171
+ def save_to_file(self, config_file: str, format_: str = "auto"):
172
+ path = Path(config_file)
173
+ data = self.to_dict()
174
+
175
+ if format_ == "auto":
176
+ format_ = "yaml" if path.suffix in [".yml", ".yaml"] else "json"
177
+
178
+ path.parent.mkdir(parents=True, exist_ok=True)
179
+ with open(path, "w", encoding="utf-8") as f:
180
+ if format_ == "yaml":
181
+ yaml.dump(data, f, indent=2)
182
+ else:
183
+ json.dump(data, f, indent=2)
184
+
185
+ if format_ == "yaml":
186
+ self._append_comments_to_yaml(path)
187
+
188
+ def to_dict(self) -> dict[str, Any]:
189
+ """Convert all configuration categories to a dictionary of plain values."""
190
+ result: dict[str, dict[str, Any]] = {}
191
+ for category in self._categories.values():
192
+ category_name = category.get_category_name()
193
+ result[category_name] = {}
194
+ for param in category.get_parameters():
195
+ val = getattr(category, param.name).value
196
+ if isinstance(val, Color):
197
+ val = val.to_hex()
198
+ elif isinstance(val, Path):
199
+ val = str(val.as_posix())
200
+ elif isinstance(val, datetime):
201
+ val = val.isoformat()
202
+ result[category_name][param.name] = val
203
+ return result
204
+
205
+ def get_all_parameters(self) -> list[ConfigParameter]:
206
+ return [p for c in self._categories.values() for p in c.get_parameters()]
207
+
208
+ def get_cli_parameters(self) -> list[ConfigParameter]:
209
+ return [p for p in self.get_all_parameters() if p.is_cli]
210
+
211
+ def _append_comments_to_yaml(self, path: Path):
212
+ """Append helpful metadata comments to the YAML file."""
213
+ lines = path.read_text(encoding="utf-8").splitlines()
214
+ new_lines = []
215
+ all_params = {p.name: p for p in self.get_all_parameters()}
216
+ current_category = None
217
+
218
+ for line in lines:
219
+ stripped = line.strip()
220
+ if (
221
+ stripped.endswith(":")
222
+ and not stripped.startswith("#")
223
+ and line.startswith(stripped)
224
+ ):
225
+ current_category = stripped[:-1]
226
+ new_lines.append(line)
227
+ continue
228
+
229
+ parts = stripped.split(":", 1)
230
+ if len(parts) > 1:
231
+ param_name = parts[0].strip()
232
+ if param_name in all_params:
233
+ param = all_params[param_name]
234
+ if current_category and param.category == current_category:
235
+ indent = " " * (len(line) - len(stripped))
236
+ comment = (
237
+ f"{indent}# {param.help} | "
238
+ f"type={param.type_.__name__}, default={param.value}"
239
+ f"{' [CLI]' if param.is_cli else ''}"
240
+ )
241
+ new_lines.append(comment)
242
+
243
+ new_lines.append(line)
244
+
245
+ path.write_text("\n".join(new_lines), encoding="utf-8")
@@ -258,18 +258,19 @@ class GenericSettingsDialog:
258
258
  canvas.configure(yscrollcommand=scrollbar.set)
259
259
 
260
260
  # Add parameters
261
- self._add_category_parameters(scrollable_frame, category_name, category)
261
+ self._add_category_parameters(scrollable_frame, category)
262
262
 
263
263
  canvas.pack(side="left", fill="both", expand=True)
264
264
  scrollbar.pack(side="right", fill="y")
265
265
 
266
- def _add_category_parameters(self, parent, category_name: str, category: ConfigCategory):
266
+ def _add_category_parameters(self, parent, category: ConfigCategory):
267
267
  """Add parameter widgets for a specific category."""
268
268
  row = 0
269
+ category_name: str = category.get_category_name()
269
270
  parameters = category.get_parameters()
270
271
 
271
272
  for param in parameters:
272
- param.value = getattr(category, param.name)
273
+ param.value = getattr(category, param.name).value
273
274
  if param.required:
274
275
  # Skip required parameters as they are not configurable in GUI
275
276
  continue
@@ -391,8 +392,8 @@ class GenericSettingsDialog:
391
392
  entry = ttk.Entry(frame, textvariable=var, width=10)
392
393
  entry.pack(side=tk.LEFT)
393
394
 
394
- color_display = tk.Label(frame, width=7, bg=color_value.to_hex())
395
- color_display.pack(side=tk.LEFT, padx=(5, 0))
395
+ color_display = tk.Label(frame, width=8, bg=color_value.to_hex())
396
+ color_display.pack(side=tk.LEFT, padx=(8, 2))
396
397
 
397
398
  def pick_color():
398
399
  color = colorchooser.askcolor(color=var.get())
@@ -491,35 +492,35 @@ class GenericSettingsDialog:
491
492
  # Parse value based on parameter type
492
493
  category_name, param_name = key.split("__", 1)
493
494
  category = self.config_manager.get_category(category_name)
494
- param = getattr(category, param_name)
495
+ param_value = getattr(category, param_name).value
495
496
 
496
497
  # Convert value to appropriate type
497
- if type(param) == bool:
498
+ if type(param_value) == bool:
498
499
  overrides[key] = value
499
- elif type(param) == Path:
500
+ elif type(param_value) == Path:
500
501
  overrides[key] = Path(value)
501
- elif type(param) == Color:
502
+ elif type(param_value) == Color:
502
503
  overrides[key] = Color.from_hex(value)
503
- elif type(param) == datetime:
504
+ elif type(param_value) == datetime:
504
505
  overrides[key] = datetime.strptime(value, "%Y-%m-%d %H:%M")
505
- elif type(param) in (list, tuple):
506
+ elif type(param_value) in (list, tuple):
506
507
  # Parse comma-separated values
507
508
  items = [item.strip() for item in value.split(",") if item.strip()]
508
- overrides[key] = type(param)(items)
509
- elif type(param) == dict:
509
+ overrides[key] = type(param_value)(items)
510
+ elif type(param_value) == dict:
510
511
  # Parse JSON format
511
512
  import json
512
513
 
513
514
  overrides[key] = json.loads(value)
514
- elif type(param) == int:
515
+ elif type(param_value) == int:
515
516
  overrides[key] = int(value)
516
- elif type(param) == float:
517
+ elif type(param_value) == float:
517
518
  overrides[key] = float(value)
518
519
  else:
519
520
  overrides[key] = value
520
521
 
521
522
  # Apply overrides to config manager
522
- self.config_manager._apply_kwargs(overrides)
523
+ self.config_manager.apply_overrides(overrides)
523
524
 
524
525
  # Save to file
525
526
  self.config_manager.save_to_file(self.config_file)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: config-cli-gui
3
- Version: 0.1.9
3
+ Version: 0.2.0
4
4
  Summary: Feature-rich Python project template for config-cli-gui.
5
5
  Author: pamagister
6
6
  Requires-Python: <3.12,>=3.10
@@ -54,6 +54,8 @@ src/config_cli_gui.egg-info/entry_points.txt
54
54
  src/config_cli_gui.egg-info/requires.txt
55
55
  src/config_cli_gui.egg-info/top_level.txt
56
56
  tests/__init__.py
57
+ tests/test_config_manager.py
58
+ tests/test_docs.py
57
59
  tests/test_generic_cli.py
58
60
  tests/example_project/__init__.py
59
61
  tests/example_project/__main__.py
@@ -53,7 +53,7 @@ class LoggerManager:
53
53
  self.logger.handlers.clear()
54
54
 
55
55
  # Set log level from config
56
- log_level = getattr(logging, self.config.app.log_level.upper())
56
+ log_level = getattr(logging, self.config.app.log_level.value.upper())
57
57
  self.logger.setLevel(log_level)
58
58
 
59
59
  # Create formatters
@@ -142,15 +142,15 @@ class LoggerManager:
142
142
  self.logger.setLevel(log_level)
143
143
 
144
144
  # Update config
145
- self.config.get_category("app").log_level = level.upper()
145
+ self.config.app.log_level.value = level.upper()
146
146
 
147
147
  def log_config_summary(self):
148
148
  """Log current configuration summary."""
149
149
  self.logger.info("=== Configuration Summary ===")
150
- self.logger.info(f"Log level: {self.config.app.log_level}")
151
- self.logger.info(f"Input: {self.config.cli.input}")
152
- self.logger.info(f"Output: {self.config.get_category('cli').output}")
153
- self.logger.info(f"Max workers: {self.config.app.max_workers}")
150
+ self.logger.info(f"Log level: {self.config.app.log_level.value}")
151
+ self.logger.info(f"Input: {self.config.cli.input.value}")
152
+ self.logger.info(f"Output: {self.config.cli.output.value}")
153
+ self.logger.info(f"Max workers: {self.config.app.max_workers.value}")
154
154
  self.logger.info("==============================")
155
155
 
156
156
 
@@ -0,0 +1,48 @@
1
+ app:
2
+ # Date format to use | type=str, default=%d-%m-%YYYY
3
+ date_format: '%d-%m-%YYYY'
4
+ # Enable logging to console | type=bool, default=True
5
+ enable_console_logging: true
6
+ # Enable logging to file | type=bool, default=True
7
+ enable_file_logging: true
8
+ # Number of backup log files to keep | type=int, default=50
9
+ log_backup_count: 50
10
+ # Maximum log file size in MB before rotation | type=int, default=10
11
+ log_file_max_size: 10
12
+ # Log message format style | type=str, default=detailed
13
+ log_format: detailed
14
+ # Logging level for the application | type=str, default=INFO
15
+ log_level: INFO
16
+ # Maximum number of worker threads | type=int, default=4
17
+ max_workers: 4
18
+ cli:
19
+ # Include elevation data in waypoints | type=bool, default=True [CLI]
20
+ elevation: true
21
+ # Extract starting points of each track as waypoint | type=bool, default=True [CLI]
22
+ extract_waypoints: true
23
+ # Path to input (file or folder) | type=str, default= [CLI]
24
+ input: ''
25
+ # Maximum distance between two waypoints | type=int, default=2480 [CLI]
26
+ min_dist: 2480
27
+ # Path to output destination | type=str, default=test [CLI]
28
+ output: test
29
+ gui:
30
+ # Automatically scroll to the newest log entries | type=bool, default=True
31
+ auto_scroll_log: true
32
+ # Height of the log window in pixels | type=int, default=200
33
+ log_window_height: 200
34
+ # Maximum number of log lines to keep in GUI | type=int, default=1000
35
+ max_log_lines: 1000
36
+ # GUI theme setting | type=str, default=light
37
+ theme: light
38
+ # Default window height | type=int, default=600
39
+ window_height: 600
40
+ # Default window width | type=int, default=800
41
+ window_width: 800
42
+ misc:
43
+ # Color setting for the application | type=Color, default=#c64800
44
+ some_color: '#c64800'
45
+ # Date setting for the application | type=datetime, default=2025-11-12 17:10:00
46
+ some_date: '2025-11-12T17:10:00'
47
+ # Path to the file to use | type=str, default=some_file.txt
48
+ some_file: some_file.txt
@@ -241,7 +241,7 @@ class MainGui:
241
241
 
242
242
  # Log level selector
243
243
  ttk.Label(log_controls, text="Log Level:").pack(side=tk.LEFT, padx=(10, 5))
244
- self.log_level_var = tk.StringVar(value=self._config.app.log_level)
244
+ self.log_level_var = tk.StringVar(value=self._config.app.log_level.value)
245
245
  log_level_combo = ttk.Combobox(
246
246
  log_controls,
247
247
  textvariable=self.log_level_var,
@@ -454,10 +454,10 @@ class MainGui:
454
454
  # Create and run project
455
455
  project = BaseGPXProcessor(
456
456
  files_to_process, # Pass selected files
457
- self._config.cli.output,
458
- self._config.cli.min_dist,
459
- self._config.app.date_format,
460
- self._config.cli.elevation,
457
+ self._config.cli.output.value,
458
+ self._config.cli.min_dist.value,
459
+ self._config.app.date_format.value,
460
+ self._config.cli.elevation.value,
461
461
  self.logger,
462
462
  )
463
463
  # implement switch case for different processing modes
@@ -504,7 +504,7 @@ class MainGui:
504
504
  if dialog.result == "ok":
505
505
  self.logger.info("Settings updated successfully")
506
506
  # Update log level selector if it changed
507
- self.log_level_var.set(self._config.app.log_level)
507
+ self.log_level_var.set(self._config.app.log_level.value)
508
508
 
509
509
  def _open_help(self):
510
510
  """Open help documentation in browser."""