Skip to content

Commit 0f1adfe

Browse files
committed
update: config loadier and logging
1 parent b62d10a commit 0f1adfe

File tree

7 files changed

+227
-27
lines changed

7 files changed

+227
-27
lines changed

CLAUDE.md

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,26 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
88
- **Hatch** as the build system and project manager
99
- **Python 3.12+** as the minimum required version
1010
- **pydantic-settings** for configuration management
11+
- **structlog** for structured logging
1112
- **AGPL-3.0** license
1213

1314
## Development Commands
1415

16+
### Running the Application
17+
```bash
18+
# Run with hatch (preferred method)
19+
hatch run compile-flags [options]
20+
21+
# Examples:
22+
hatch run compile-flags --help
23+
hatch run compile-flags -v # INFO level logging
24+
hatch run compile-flags -vv # DEBUG level logging
25+
hatch run compile-flags -vv -l rust -b /path/to/build
26+
27+
# Run directly via Python module
28+
hatch run python -m compile_flags [options]
29+
```
30+
1531
### Package Management
1632
```bash
1733
# Install in development mode
@@ -21,15 +37,6 @@ hatch shell
2137
hatch build
2238
```
2339

24-
### Testing
25-
```bash
26-
# Run tests (when test framework is added)
27-
hatch test
28-
29-
# Run tests with coverage
30-
hatch test --cover
31-
```
32-
3340
### Type Checking
3441
```bash
3542
# Run mypy type checker
@@ -39,19 +46,46 @@ hatch run types:check
3946
hatch run types:check compile_flags/module.py
4047
```
4148

42-
## Project Structure
49+
## Architecture
50+
51+
### Configuration System
52+
The application uses a hybrid approach combining **argparse** for CLI parsing with **Pydantic Settings** for configuration management:
53+
54+
1. **CLI Parsing (`__main__.py`)**: Uses argparse with `action="count"` to support `-v`, `-vv`, `-vvv` verbosity levels
55+
2. **Configuration Model (`config.py`)**: Pydantic `BaseSettings` model that:
56+
- Validates all configuration values with type safety
57+
- Uses `@model_validator` to compute `log_level` from `verbose` count
58+
- Integrates with `CliApp.run()` pattern via `CliSettingsSource`
59+
60+
### Logging System
61+
- **structlog** for structured logging with colored console output
62+
- Log levels mapped from verbosity: 0=WARNING, 1=INFO, 2+=DEBUG
63+
- Configuration in `config.py:configure_logging()`
64+
- All log messages include structured key-value pairs for better debugging
65+
66+
### Entry Point Flow
67+
```
68+
__main__.py:parse_args()
69+
→ CliApp.run(Config, cli_settings_source=...)
70+
→ config.py:compute_log_level() (model_validator)
71+
→ __main__.py:configure_logging()
72+
→ Application logic with structured logging
73+
```
74+
75+
## Code Style
4376

44-
- `compile_flags/` - Main package source code
45-
- `__about__.py` - Version information
46-
- `__init__.py` - Package initialization
47-
- `tests/` - Test suite (pytest-based)
48-
- `pyproject.toml` - Project configuration and dependencies
77+
- Use modern Python type hints: `dict` not `Dict`, `| None` not `Optional`
78+
- Type annotate everything: variables, function arguments, and return types
79+
- All source files must include SPDX license headers (MIT for code files)
80+
- Version is managed in `compile_flags/__about__.py`
4981

5082
## Important Notes
5183

52-
- The project uses modern Python type hints (e.g., `dict` instead of `Dict`, `| None` instead of `Optional`)
53-
- Version is managed in `compile_flags/__about__.py` (note: pyproject.toml incorrectly references `src/compile_flags/__about__.py`)
54-
- Coverage configuration excludes `__about__.py` from coverage reports
55-
- All source files include SPDX license headers (MIT for code files)
56-
- Use modern Python annotations, for example, use "| None" instead of "Optional".
57-
- Type annotate everything, including variables, function arguments, and function returns.
84+
- The hatch script at `pyproject.toml:39` uses `{args}` to pass CLI arguments through
85+
- When adding CLI arguments, update both `parse_args()` in `__main__.py` and the corresponding field in `Config` model
86+
- The `Config` model has `case_sensitive=False` for environment variable support
87+
- Environment variables can override defaults using `COMPILE_FLAGS_` prefix
88+
- Use modern Python type hints: `dict` not `Dict`, `| None` not `Optional`
89+
- Type annotate everything: variables, function arguments, and return types
90+
- All source files must include SPDX license headers (AGPL-3.0 for source files)
91+
- Version is managed in `compile_flags/__about__.py`

compile_flags/__about__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# SPDX-FileCopyrightText: 2025-present Yiannis Charalambous <yiannis128@hotmail.com>
22
#
3-
# SPDX-License-Identifier: MIT
3+
# SPDX-License-Identifier: AGPL-3.0
44
__version__ = "0.0.1"

compile_flags/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# SPDX-FileCopyrightText: 2025-present Yiannis Charalambous <yiannis128@hotmail.com>
22
#
3-
# SPDX-License-Identifier: MIT
3+
# SPDX-License-Identifier: AGPL-3.0

compile_flags/__main__.py

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,77 @@
11
# SPDX-FileCopyrightText: 2025-present Yiannis Charalambous <yiannis128@hotmail.com>
22
#
3-
# SPDX-License-Identifier: MIT
3+
# SPDX-License-Identifier: AGPL-3.0
4+
5+
import argparse
6+
import logging
7+
import sys
8+
9+
import structlog
10+
from pydantic_settings import CliApp, CliSettingsSource
11+
12+
from compile_flags.config import Config, configure_logging
13+
14+
15+
def create_parser() -> argparse.ArgumentParser:
16+
"""Create argparse parser with custom arguments."""
17+
parser: argparse.ArgumentParser = argparse.ArgumentParser(
18+
prog="compile-flags",
19+
description="Multi-language compilation flags detector tool",
20+
)
21+
22+
# Only add custom arguments that need special handling
23+
# CliSettingsSource will automatically add all other Config fields
24+
parser.add_argument(
25+
"-v",
26+
"--verbose",
27+
action="count",
28+
default=0,
29+
help="Increase verbosity (use -v for INFO, -vv for DEBUG, -vvv for maximum verbosity)",
30+
)
31+
32+
return parser
433

534

635
def main() -> None:
7-
print("hello world")
36+
"""Main entry point for compile-flags CLI."""
37+
# Create parser with custom arguments
38+
parser: argparse.ArgumentParser = create_parser()
39+
40+
# Parse verbose level before Pydantic CLI parsing
41+
args, _ = parser.parse_known_args()
42+
verbose: int = args.verbose if hasattr(args, "verbose") else 0
43+
44+
# Map verbose count to log level
45+
if verbose == 0:
46+
log_level = logging.WARNING
47+
elif verbose == 1:
48+
log_level = logging.INFO
49+
else: # 2+
50+
log_level = logging.DEBUG
51+
52+
# Configure logging before loading config, will also update in the config.
53+
configure_logging(log_level)
54+
55+
# Create custom CLI settings source using our argparse parser
56+
# This will automatically add all non-excluded Config fields to the parser
57+
cli_settings: CliSettingsSource = CliSettingsSource(
58+
Config,
59+
cli_parse_args=True,
60+
root_parser=parser,
61+
cli_implicit_flags=True,
62+
)
63+
64+
# Use CliApp.run with custom CLI settings source
65+
config: Config = CliApp.run(Config, cli_settings_source=cli_settings)
66+
67+
# Get structured logger
68+
log: structlog.stdlib.BoundLogger = structlog.get_logger()
69+
70+
# Log configuration
71+
log.debug("Configuration loaded", **config.model_dump(mode="python"))
72+
log.info("Starting compilation flags detection", build_dir=config.build_dir)
73+
74+
print("TODO")
875

976

1077
if __name__ == "__main__":

compile_flags/config.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# SPDX-FileCopyrightText: 2025-present Yiannis Charalambous <yiannis128@hotmail.com>
2+
#
3+
# SPDX-License-Identifier: AGPL-3.0
4+
5+
import logging
6+
import sys
7+
8+
import structlog
9+
from pydantic import AliasChoices, Field, PrivateAttr, computed_field
10+
from pydantic_settings import BaseSettings, SettingsConfigDict
11+
12+
13+
def _alias_choice(value: str) -> AliasChoices:
14+
"""
15+
Create aliases for config fields to work with multiple sources.
16+
17+
Args:
18+
value: The field name
19+
20+
Returns:
21+
AliasChoices with field name, dashed version, and env var version
22+
"""
23+
return AliasChoices(
24+
value, # exact field name for direct matching
25+
value.replace("_", "-"), # dashed alias for CLI
26+
f"COMPILE_FLAGS_{value.replace('-', '_').upper()}", # prefixed env var alias
27+
)
28+
29+
30+
def configure_logging(log_level: int) -> None:
31+
"""
32+
Configure structlog with appropriate processors and log level.
33+
34+
Args:
35+
log_level: The logging level to use
36+
"""
37+
# Configure standard library logging
38+
logging.basicConfig(
39+
format="%(message)s",
40+
level=log_level,
41+
stream=sys.stdout,
42+
)
43+
44+
# Configure structlog
45+
structlog.configure(
46+
processors=[
47+
structlog.contextvars.merge_contextvars,
48+
structlog.processors.add_log_level,
49+
structlog.processors.StackInfoRenderer(),
50+
structlog.dev.set_exc_info,
51+
structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S", utc=False),
52+
structlog.dev.ConsoleRenderer(colors=True),
53+
],
54+
wrapper_class=structlog.make_filtering_bound_logger(log_level),
55+
context_class=dict,
56+
logger_factory=structlog.PrintLoggerFactory(),
57+
cache_logger_on_first_use=False,
58+
)
59+
60+
61+
class Config(BaseSettings):
62+
"""Configuration for compile-flags tool."""
63+
64+
# override from BaseSettings
65+
model_config = SettingsConfigDict(
66+
case_sensitive=False,
67+
cli_parse_args=True,
68+
)
69+
70+
output_file: str | None = Field(
71+
default=None,
72+
validation_alias=_alias_choice("output_file"),
73+
description="Output file path for compilation flags",
74+
)
75+
76+
language: str | None = Field(
77+
default=None,
78+
validation_alias=_alias_choice("language"),
79+
description="Programming language to detect flags for (e.g., c, cpp, rust)",
80+
)
81+
82+
build_dir: str = Field(
83+
default=".",
84+
validation_alias=_alias_choice("build_dir"),
85+
description="Build directory to analyze",
86+
)
87+
88+
@computed_field
89+
@property
90+
def log_level(self) -> int:
91+
"""The current log level."""
92+
return logging.getLogger().getEffectiveLevel()
93+
94+
@computed_field
95+
@property
96+
def log_level_name(self) -> str:
97+
"""The current log level name."""
98+
return logging.getLevelName(self.log_level)

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ classifiers = [
2121
]
2222
dependencies = [
2323
"pydantic-settings",
24+
"structlog",
2425
]
2526

2627
[project.scripts]
@@ -35,7 +36,7 @@ Source = "https://github.com/esbmc/compile-flags"
3536
path = "compile_flags/__about__.py"
3637

3738
[tool.hatch.envs.default.scripts]
38-
compile-flags = "python -m compile_flags"
39+
compile-flags = "python -m compile_flags {args}"
3940

4041
[tool.hatch.envs.types]
4142
extra-dependencies = [

tests/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
# SPDX-FileCopyrightText: 2025-present Yiannis Charalambous <yiannis128@hotmail.com>
22
#
3-
# SPDX-License-Identifier: MIT
3+
# SPDX-License-Identifier: AGPL-3.0

0 commit comments

Comments
 (0)