config-cli-gui 0.0.2__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.
- __init__.py +0 -0
- config_cli_gui/__init__.py +0 -0
- config_cli_gui/_version.py +21 -0
- config_cli_gui/cli_generator.py +177 -0
- config_cli_gui/config_framework.py +362 -0
- config_cli_gui/gui_generator.py +225 -0
- config_cli_gui-0.0.2.dist-info/METADATA +282 -0
- config_cli_gui-0.0.2.dist-info/RECORD +26 -0
- config_cli_gui-0.0.2.dist-info/WHEEL +5 -0
- config_cli_gui-0.0.2.dist-info/entry_points.txt +3 -0
- config_cli_gui-0.0.2.dist-info/licenses/LICENSE +24 -0
- config_cli_gui-0.0.2.dist-info/top_level.txt +4 -0
- example_project/__init__.py +0 -0
- example_project/__main__.py +8 -0
- example_project/cli/__init__.py +0 -0
- example_project/cli/__main__.py +8 -0
- example_project/cli/cli.py +132 -0
- example_project/config/__init__.py +0 -0
- example_project/config/config.py +209 -0
- example_project/core/__init__.py +0 -0
- example_project/core/base.py +634 -0
- example_project/core/logging.py +219 -0
- example_project/gui/__init__.py +0 -0
- example_project/gui/__main__.py +8 -0
- example_project/gui/gui.py +542 -0
- main.py +153 -0
__init__.py
ADDED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# file generated by setuptools-scm
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
|
|
4
|
+
__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
|
|
5
|
+
|
|
6
|
+
TYPE_CHECKING = False
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from typing import Tuple
|
|
9
|
+
from typing import Union
|
|
10
|
+
|
|
11
|
+
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
|
12
|
+
else:
|
|
13
|
+
VERSION_TUPLE = object
|
|
14
|
+
|
|
15
|
+
version: str
|
|
16
|
+
__version__: str
|
|
17
|
+
__version_tuple__: VERSION_TUPLE
|
|
18
|
+
version_tuple: VERSION_TUPLE
|
|
19
|
+
|
|
20
|
+
__version__ = version = '0.0.2'
|
|
21
|
+
__version_tuple__ = version_tuple = (0, 0, 2)
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# config_cli_gui/cli.py
|
|
2
|
+
"""Generic CLI generator for configuration framework."""
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import traceback
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from config_cli_gui.config_framework import ConfigManager
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CliGenerator:
|
|
13
|
+
"""Generates CLI interface from ConfigManager."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, config_manager: ConfigManager, app_name: str = "app"):
|
|
16
|
+
self.config_manager = config_manager
|
|
17
|
+
self.app_name = app_name
|
|
18
|
+
|
|
19
|
+
def create_argument_parser(self, description: str = None) -> argparse.ArgumentParser:
|
|
20
|
+
"""Create argument parser from configuration."""
|
|
21
|
+
if description is None:
|
|
22
|
+
description = f"Command line interface for {self.app_name}"
|
|
23
|
+
|
|
24
|
+
parser = argparse.ArgumentParser(
|
|
25
|
+
description=description,
|
|
26
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Config file argument
|
|
30
|
+
parser.add_argument(
|
|
31
|
+
"--config",
|
|
32
|
+
default=None,
|
|
33
|
+
help="Path to configuration file (JSON or YAML)",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Verbose/quiet options for log level override
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"-v", "--verbose", action="store_true", help="Enable verbose logging (DEBUG level)"
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"-q", "--quiet", action="store_true", help="Enable quiet mode (WARNING level only)"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Get CLI parameters
|
|
45
|
+
cli_params = self.config_manager.get_cli_parameters()
|
|
46
|
+
|
|
47
|
+
# Generate arguments from CLI config parameters
|
|
48
|
+
for param in cli_params:
|
|
49
|
+
if param.required and param.cli_arg is None:
|
|
50
|
+
# Positional argument
|
|
51
|
+
parser.add_argument(param.name, help=param.help)
|
|
52
|
+
else:
|
|
53
|
+
# Optional argument
|
|
54
|
+
kwargs = {
|
|
55
|
+
"default": argparse.SUPPRESS,
|
|
56
|
+
"help": f"{param.help} (default: {param.default})",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
# Handle different parameter types
|
|
60
|
+
if param.choices and param.type_ != bool:
|
|
61
|
+
kwargs["choices"] = param.choices
|
|
62
|
+
|
|
63
|
+
if param.type_ == int:
|
|
64
|
+
kwargs["type"] = int
|
|
65
|
+
elif param.type_ == float:
|
|
66
|
+
kwargs["type"] = float
|
|
67
|
+
elif param.type_ == bool:
|
|
68
|
+
kwargs["action"] = "store_true" if not param.default else "store_false"
|
|
69
|
+
kwargs["help"] = f"{param.help} (default: {param.default})"
|
|
70
|
+
elif param.type_ == str:
|
|
71
|
+
kwargs["type"] = str
|
|
72
|
+
|
|
73
|
+
parser.add_argument(param.cli_arg, **kwargs)
|
|
74
|
+
|
|
75
|
+
return parser
|
|
76
|
+
|
|
77
|
+
def create_config_overrides(self, args: argparse.Namespace) -> dict[str, Any]:
|
|
78
|
+
"""Create configuration overrides from CLI arguments."""
|
|
79
|
+
cli_params = self.config_manager.get_cli_parameters()
|
|
80
|
+
overrides = {}
|
|
81
|
+
|
|
82
|
+
for param in cli_params:
|
|
83
|
+
if hasattr(args, param.name):
|
|
84
|
+
arg_value = getattr(args, param.name)
|
|
85
|
+
# Add CLI category prefix for override system
|
|
86
|
+
overrides[f"cli__{param.name}"] = arg_value
|
|
87
|
+
|
|
88
|
+
# Handle log level overrides from verbose/quiet flags
|
|
89
|
+
if hasattr(args, "verbose") and args.verbose:
|
|
90
|
+
overrides["app__log_level"] = "DEBUG"
|
|
91
|
+
elif hasattr(args, "quiet") and args.quiet:
|
|
92
|
+
overrides["app__log_level"] = "WARNING"
|
|
93
|
+
|
|
94
|
+
return overrides
|
|
95
|
+
|
|
96
|
+
def run_cli(
|
|
97
|
+
self,
|
|
98
|
+
main_function: Callable[[ConfigManager], int],
|
|
99
|
+
description: str = None,
|
|
100
|
+
validator: Callable[[ConfigManager], bool] = None,
|
|
101
|
+
) -> int:
|
|
102
|
+
"""Run the CLI application with error handling.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
main_function: Function that takes ConfigManager and returns exit code
|
|
106
|
+
description: CLI description
|
|
107
|
+
validator: Optional function to validate configuration
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Exit code
|
|
111
|
+
"""
|
|
112
|
+
logger = None
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
# Parse command line arguments
|
|
116
|
+
parser = self.create_argument_parser(description)
|
|
117
|
+
args = parser.parse_args()
|
|
118
|
+
|
|
119
|
+
# Create configuration overrides from CLI arguments
|
|
120
|
+
cli_overrides = self.create_config_overrides(args)
|
|
121
|
+
|
|
122
|
+
# Create new config manager with overrides
|
|
123
|
+
config_file = args.config if hasattr(args, "config") and args.config else None
|
|
124
|
+
updated_config = ConfigManager(config_file=config_file, **cli_overrides)
|
|
125
|
+
|
|
126
|
+
# Copy categories from original config manager
|
|
127
|
+
for name, category in self.config_manager._categories.items():
|
|
128
|
+
updated_config.add_category(name, category)
|
|
129
|
+
|
|
130
|
+
# Apply overrides again after copying categories
|
|
131
|
+
updated_config._apply_kwargs(cli_overrides)
|
|
132
|
+
|
|
133
|
+
# Try to get logger if logging is configured
|
|
134
|
+
try:
|
|
135
|
+
from .logging import get_logger
|
|
136
|
+
|
|
137
|
+
logger = get_logger(f"{self.app_name}.cli")
|
|
138
|
+
logger.info(f"Starting {self.app_name} CLI")
|
|
139
|
+
logger.debug(f"Command line arguments: {vars(args)}")
|
|
140
|
+
except ImportError:
|
|
141
|
+
pass
|
|
142
|
+
|
|
143
|
+
# Validate configuration if validator provided
|
|
144
|
+
if validator and not validator(updated_config):
|
|
145
|
+
if logger:
|
|
146
|
+
logger.error("Configuration validation failed")
|
|
147
|
+
else:
|
|
148
|
+
print("Configuration validation failed")
|
|
149
|
+
return 1
|
|
150
|
+
|
|
151
|
+
# Run main function
|
|
152
|
+
return main_function(updated_config)
|
|
153
|
+
|
|
154
|
+
except FileNotFoundError as e:
|
|
155
|
+
if logger:
|
|
156
|
+
logger.error(f"File not found: {e}")
|
|
157
|
+
logger.debug("Full traceback:", exc_info=True)
|
|
158
|
+
else:
|
|
159
|
+
print(f"Error: {e}")
|
|
160
|
+
traceback.print_exc()
|
|
161
|
+
return 1
|
|
162
|
+
|
|
163
|
+
except KeyboardInterrupt:
|
|
164
|
+
if logger:
|
|
165
|
+
logger.warning("Process interrupted by user")
|
|
166
|
+
else:
|
|
167
|
+
print("Process interrupted by user")
|
|
168
|
+
return 130
|
|
169
|
+
|
|
170
|
+
except Exception as e:
|
|
171
|
+
if logger:
|
|
172
|
+
logger.error(f"Unexpected error: {e}")
|
|
173
|
+
logger.debug("Full traceback:", exc_info=True)
|
|
174
|
+
else:
|
|
175
|
+
print(f"Unexpected error: {e}")
|
|
176
|
+
traceback.print_exc()
|
|
177
|
+
return 1
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
# config_framework/core.py
|
|
2
|
+
"""Generic configuration framework for Python applications."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from textwrap import dedent
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import yaml
|
|
12
|
+
from pydantic import BaseModel
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class ConfigParameter:
|
|
17
|
+
"""Represents a single configuration parameter with all its metadata."""
|
|
18
|
+
|
|
19
|
+
name: str
|
|
20
|
+
default: Any
|
|
21
|
+
type_: type
|
|
22
|
+
choices: list[str | bool] = None
|
|
23
|
+
help: str = ""
|
|
24
|
+
cli_arg: str = None
|
|
25
|
+
required: bool = False
|
|
26
|
+
category: str = "general"
|
|
27
|
+
|
|
28
|
+
def __post_init__(self):
|
|
29
|
+
if self.cli_arg is None and not self.required:
|
|
30
|
+
self.cli_arg = f"--{self.name}"
|
|
31
|
+
if self.type_ is bool and self.choices is None:
|
|
32
|
+
self.choices = [True, False]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class BaseConfigCategory(BaseModel, ABC):
|
|
36
|
+
"""Base class for configuration categories."""
|
|
37
|
+
|
|
38
|
+
@abstractmethod
|
|
39
|
+
def get_category_name(self) -> str:
|
|
40
|
+
"""Return the category name for this configuration group."""
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
def get_parameters(self) -> list[ConfigParameter]:
|
|
44
|
+
"""Get all ConfigParameter objects from this category."""
|
|
45
|
+
parameters = []
|
|
46
|
+
for field_name in self.model_fields:
|
|
47
|
+
param = getattr(self, field_name)
|
|
48
|
+
if isinstance(param, ConfigParameter):
|
|
49
|
+
param.category = self.get_category_name()
|
|
50
|
+
parameters.append(param)
|
|
51
|
+
return parameters
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class CliConfigCategory(BaseConfigCategory):
|
|
55
|
+
"""Base class for CLI-specific configuration parameters."""
|
|
56
|
+
|
|
57
|
+
def get_category_name(self) -> str:
|
|
58
|
+
return "cli"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ConfigManager:
|
|
62
|
+
"""Generic configuration manager that can handle multiple configuration categories."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, config_file: str = None, **kwargs):
|
|
65
|
+
"""Initialize configuration manager.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
config_file: Path to configuration file (JSON or YAML)
|
|
69
|
+
**kwargs: Override parameters in format category__parameter
|
|
70
|
+
"""
|
|
71
|
+
self._categories: dict[str, BaseConfigCategory] = {}
|
|
72
|
+
self._cli_category_name: str = None
|
|
73
|
+
|
|
74
|
+
# Load from file if provided
|
|
75
|
+
if config_file:
|
|
76
|
+
self.load_from_file(config_file)
|
|
77
|
+
|
|
78
|
+
# Override with provided kwargs
|
|
79
|
+
self._apply_kwargs(kwargs)
|
|
80
|
+
|
|
81
|
+
def add_category(self, name: str, category: BaseConfigCategory):
|
|
82
|
+
"""Add a configuration category.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
name: Name of the category (e.g., 'cli', 'app', 'gui')
|
|
86
|
+
category: Configuration category instance
|
|
87
|
+
"""
|
|
88
|
+
self._categories[name] = category
|
|
89
|
+
if isinstance(category, CliConfigCategory):
|
|
90
|
+
self._cli_category_name = name
|
|
91
|
+
|
|
92
|
+
def get_category(self, name: str) -> BaseConfigCategory:
|
|
93
|
+
"""Get a configuration category by name."""
|
|
94
|
+
return self._categories.get(name)
|
|
95
|
+
|
|
96
|
+
def get_cli_category(self) -> BaseConfigCategory | None:
|
|
97
|
+
"""Get the CLI configuration category."""
|
|
98
|
+
if self._cli_category_name:
|
|
99
|
+
return self._categories[self._cli_category_name]
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
def _apply_kwargs(self, kwargs: dict[str, Any]):
|
|
103
|
+
"""Apply keyword arguments to override configuration values."""
|
|
104
|
+
for key, value in kwargs.items():
|
|
105
|
+
if "__" in key:
|
|
106
|
+
category_name, param_name = key.split("__", 1)
|
|
107
|
+
if category_name in self._categories:
|
|
108
|
+
category = self._categories[category_name]
|
|
109
|
+
if hasattr(category, param_name):
|
|
110
|
+
param = getattr(category, param_name)
|
|
111
|
+
if isinstance(param, ConfigParameter):
|
|
112
|
+
param.default = value
|
|
113
|
+
|
|
114
|
+
def load_from_file(self, config_file: str):
|
|
115
|
+
"""Load configuration from JSON or YAML file."""
|
|
116
|
+
config_path = Path(config_file)
|
|
117
|
+
if not config_path.exists():
|
|
118
|
+
raise FileNotFoundError(f"Configuration file not found: {config_file}")
|
|
119
|
+
|
|
120
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
121
|
+
if config_path.suffix.lower() in [".yml", ".yaml"]:
|
|
122
|
+
config_data = yaml.safe_load(f)
|
|
123
|
+
else:
|
|
124
|
+
config_data = json.load(f)
|
|
125
|
+
|
|
126
|
+
# Apply loaded configuration
|
|
127
|
+
for category_name, category_data in config_data.items():
|
|
128
|
+
if category_name in self._categories:
|
|
129
|
+
category = self._categories[category_name]
|
|
130
|
+
for param_name, param_value in category_data.items():
|
|
131
|
+
if hasattr(category, param_name):
|
|
132
|
+
param = getattr(category, param_name)
|
|
133
|
+
if isinstance(param, ConfigParameter):
|
|
134
|
+
param.default = param_value
|
|
135
|
+
|
|
136
|
+
def save_to_file(self, config_file: str, format_: str = "auto"):
|
|
137
|
+
"""Save current configuration to file."""
|
|
138
|
+
config_path = Path(config_file)
|
|
139
|
+
config_data = self.to_dict()
|
|
140
|
+
|
|
141
|
+
# Determine format
|
|
142
|
+
if format_ == "auto":
|
|
143
|
+
format_ = "yaml" if config_path.suffix.lower() in [".yml", ".yaml"] else "json"
|
|
144
|
+
|
|
145
|
+
# Ensure directory exists
|
|
146
|
+
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
147
|
+
|
|
148
|
+
with open(config_path, "w", encoding="utf-8") as f:
|
|
149
|
+
if format_ == "yaml":
|
|
150
|
+
yaml.dump(config_data, f, default_flow_style=False, indent=2)
|
|
151
|
+
else:
|
|
152
|
+
json.dump(config_data, f, indent=2)
|
|
153
|
+
|
|
154
|
+
def to_dict(self) -> dict[str, Any]:
|
|
155
|
+
"""Convert configuration to dictionary."""
|
|
156
|
+
result = {}
|
|
157
|
+
for category_name, category in self._categories.items():
|
|
158
|
+
category_dict = {}
|
|
159
|
+
for param in category.get_parameters():
|
|
160
|
+
category_dict[param.name] = param.default
|
|
161
|
+
result[category_name] = category_dict
|
|
162
|
+
return result
|
|
163
|
+
|
|
164
|
+
def get_all_parameters(self) -> list[ConfigParameter]:
|
|
165
|
+
"""Get all parameters from all categories."""
|
|
166
|
+
parameters = []
|
|
167
|
+
for category in self._categories.values():
|
|
168
|
+
parameters.extend(category.get_parameters())
|
|
169
|
+
return parameters
|
|
170
|
+
|
|
171
|
+
def get_cli_parameters(self) -> list[ConfigParameter]:
|
|
172
|
+
"""Get only CLI parameters."""
|
|
173
|
+
cli_category = self.get_cli_category()
|
|
174
|
+
if cli_category:
|
|
175
|
+
return cli_category.get_parameters()
|
|
176
|
+
return []
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
class DocumentationGenerator:
|
|
180
|
+
"""Generates documentation and configuration files from ConfigManager."""
|
|
181
|
+
|
|
182
|
+
def __init__(self, config_manager: ConfigManager):
|
|
183
|
+
self.config_manager = config_manager
|
|
184
|
+
|
|
185
|
+
def generate_config_markdown_doc(self, output_file: str):
|
|
186
|
+
"""Generate Markdown documentation for all configuration parameters."""
|
|
187
|
+
|
|
188
|
+
def pad(s, width):
|
|
189
|
+
return s + " " * (width - len(s))
|
|
190
|
+
|
|
191
|
+
markdown_content = dedent("""
|
|
192
|
+
# Configuration Parameters
|
|
193
|
+
|
|
194
|
+
These parameters are available to configure the behavior of your application.
|
|
195
|
+
The parameters in the cli category can be accessed via the command line interface.
|
|
196
|
+
|
|
197
|
+
""").lstrip()
|
|
198
|
+
|
|
199
|
+
for category_name, category in self.config_manager._categories.items():
|
|
200
|
+
markdown_content += f'## Category "{category_name}"\n\n'
|
|
201
|
+
|
|
202
|
+
# Collect all parameters for this category
|
|
203
|
+
rows = []
|
|
204
|
+
header = ["Name", "Type", "Description", "Default", "Choices"]
|
|
205
|
+
|
|
206
|
+
for param in category.get_parameters():
|
|
207
|
+
name = param.name
|
|
208
|
+
typ = param.type_.__name__
|
|
209
|
+
desc = param.help
|
|
210
|
+
default = repr(param.default)
|
|
211
|
+
choices = str(param.choices) if param.choices else "-"
|
|
212
|
+
|
|
213
|
+
rows.append((name, typ, desc, default, choices))
|
|
214
|
+
|
|
215
|
+
if not rows:
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
# Calculate column widths
|
|
219
|
+
all_rows = [header] + rows
|
|
220
|
+
widths = [max(len(str(col)) for col in column) for column in zip(*all_rows)]
|
|
221
|
+
|
|
222
|
+
# Create Markdown table
|
|
223
|
+
table = (
|
|
224
|
+
"| "
|
|
225
|
+
+ " | ".join(pad(h, w) for h, w in zip(header, widths))
|
|
226
|
+
+ " |\n"
|
|
227
|
+
+ "|-"
|
|
228
|
+
+ "-|-".join("-" * w for w in widths)
|
|
229
|
+
+ "-|\n"
|
|
230
|
+
)
|
|
231
|
+
for row in rows:
|
|
232
|
+
table += "| " + " | ".join(pad(str(col), w) for col, w in zip(row, widths)) + " |\n"
|
|
233
|
+
|
|
234
|
+
markdown_content += table + "\n"
|
|
235
|
+
|
|
236
|
+
# Write to file
|
|
237
|
+
Path(output_file).parent.mkdir(parents=True, exist_ok=True)
|
|
238
|
+
with open(output_file, "w", encoding="utf-8") as f:
|
|
239
|
+
f.write(markdown_content)
|
|
240
|
+
|
|
241
|
+
def generate_default_config_file(self, output_file: str):
|
|
242
|
+
"""Generate a default configuration file with all parameters and descriptions."""
|
|
243
|
+
output_path = Path(output_file)
|
|
244
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
245
|
+
|
|
246
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
247
|
+
f.write("# Configuration File\n")
|
|
248
|
+
f.write("# This file was auto-generated. Modify as needed.\n\n")
|
|
249
|
+
|
|
250
|
+
for category_name, category in self.config_manager._categories.items():
|
|
251
|
+
f.write(f"# {category_name.upper()} Configuration\n")
|
|
252
|
+
f.write(f"{category_name}:\n")
|
|
253
|
+
|
|
254
|
+
for param in category.get_parameters():
|
|
255
|
+
f.write(f" # {param.help}\n")
|
|
256
|
+
if param.choices:
|
|
257
|
+
f.write(f" # Choices: {param.choices}\n")
|
|
258
|
+
f.write(f" # Type: {param.type_.__name__}\n")
|
|
259
|
+
f.write(f" {param.name}: {repr(param.default)}\n\n")
|
|
260
|
+
|
|
261
|
+
f.write("\n")
|
|
262
|
+
|
|
263
|
+
def generate_cli_markdown_doc(self, output_file: str, app_name: str = "app"):
|
|
264
|
+
"""Generate Markdown CLI documentation."""
|
|
265
|
+
cli_params = self.config_manager.get_cli_parameters()
|
|
266
|
+
|
|
267
|
+
if not cli_params:
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
rows = []
|
|
271
|
+
required_params = []
|
|
272
|
+
optional_params = []
|
|
273
|
+
|
|
274
|
+
for param in cli_params:
|
|
275
|
+
cli_arg = f"`--{param.name}`" if not param.required else f"`{param.name}`"
|
|
276
|
+
typ = param.type_.__name__
|
|
277
|
+
desc = param.help
|
|
278
|
+
default = (
|
|
279
|
+
"*required*"
|
|
280
|
+
if param.required or param.default in (None, "")
|
|
281
|
+
else repr(param.default)
|
|
282
|
+
)
|
|
283
|
+
choices = str(param.choices) if param.choices else "-"
|
|
284
|
+
|
|
285
|
+
rows.append((cli_arg, typ, desc, default, choices))
|
|
286
|
+
if default == "*required*":
|
|
287
|
+
required_params.append(param)
|
|
288
|
+
else:
|
|
289
|
+
optional_params.append(param)
|
|
290
|
+
|
|
291
|
+
# Generate table
|
|
292
|
+
def pad(s, width):
|
|
293
|
+
return s + " " * (width - len(s))
|
|
294
|
+
|
|
295
|
+
widths = [max(len(str(col)) for col in column) for column in zip(*rows)]
|
|
296
|
+
header = ["Option", "Type", "Description", "Default", "Choices"]
|
|
297
|
+
|
|
298
|
+
table = (
|
|
299
|
+
"| "
|
|
300
|
+
+ " | ".join(pad(h, w) for h, w in zip(header, widths))
|
|
301
|
+
+ " |\n"
|
|
302
|
+
+ "|-"
|
|
303
|
+
+ "-|-".join("-" * w for w in widths)
|
|
304
|
+
+ "-|\n"
|
|
305
|
+
)
|
|
306
|
+
for row in rows:
|
|
307
|
+
table += "| " + " | ".join(pad(str(col), w) for col, w in zip(row, widths)) + " |\n"
|
|
308
|
+
|
|
309
|
+
# Generate examples
|
|
310
|
+
examples = []
|
|
311
|
+
required_arg = required_params[0].name if required_params else "example.input"
|
|
312
|
+
|
|
313
|
+
examples.append(
|
|
314
|
+
dedent(
|
|
315
|
+
f"""
|
|
316
|
+
### 1. Basic usage
|
|
317
|
+
|
|
318
|
+
```bash
|
|
319
|
+
python -m {app_name} {required_arg}
|
|
320
|
+
```
|
|
321
|
+
"""
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
# Add more examples with optional parameters
|
|
326
|
+
for i, param in enumerate(optional_params[:3], 2):
|
|
327
|
+
if param.name in ["verbose", "quiet"]:
|
|
328
|
+
continue
|
|
329
|
+
example_value = param.choices[0] if param.choices else param.default
|
|
330
|
+
examples.append(
|
|
331
|
+
dedent(f"""
|
|
332
|
+
### {i}. With {param.name} parameter
|
|
333
|
+
|
|
334
|
+
```bash
|
|
335
|
+
python -m {app_name} --{param.name} {example_value} {required_arg}
|
|
336
|
+
```
|
|
337
|
+
""")
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
markdown = dedent(
|
|
341
|
+
f"""
|
|
342
|
+
# Command Line Interface
|
|
343
|
+
|
|
344
|
+
Command line options for {app_name}
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
python -m {app_name} [OPTIONS] {required_arg if required_params else ""}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## Options
|
|
351
|
+
|
|
352
|
+
{table}
|
|
353
|
+
|
|
354
|
+
## Examples
|
|
355
|
+
|
|
356
|
+
{"".join(examples)}
|
|
357
|
+
"""
|
|
358
|
+
).strip()
|
|
359
|
+
|
|
360
|
+
Path(output_file).parent.mkdir(parents=True, exist_ok=True)
|
|
361
|
+
with open(output_file, "w", encoding="utf-8") as f:
|
|
362
|
+
f.write(markdown)
|