Skip to content

Commit ead67d7

Browse files
ibro45claude
andauthored
Modernize API with .update(), freeze/MISSING, and type coercion (#3)
* Remove CLI module in favor of manual parsing pattern Remove the built-in CLI parsing utilities (`Config.from_cli()`, `parse_override()`, `parse_overrides()`) to simplify the library and encourage users to handle their own argument parsing with standard libraries like argparse, click, typer, or fire. **Why:** - Reduces library complexity and maintenance burden - Users have full control over CLI behavior and flags - Works better with diverse CLI frameworks - The recommended pattern is simple: just 3 lines of code for basic CLI override parsing **Migration path:** ```python # Old way config = Config.from_cli("config.yaml", sys.argv[1:]) # New way (3 lines) config = Config() config.update("config.yaml") for arg in sys.argv[1:]: if "=" in arg: key, value = arg.split("=", 1) try: value = ast.literal_eval(value) except (ValueError, SyntaxError): pass config.set(key, value) ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * Add type coercion system for schema validation Introduce automatic type coercion to convert compatible types during validation, making configs more flexible and user-friendly. **Features:** - String to numeric conversion (`"123"` → `123`, `"3.14"` → `3.14`) - Numeric type conversion (`1` → `1.0` for float fields) - String to bool parsing (`"true"`, `"yes"`, `"1"` → `True`) - List/tuple interconversion - Dict to dataclass conversion (nested structures) - Preserves None values and handles Optional types correctly **Example:** ```python @DataClass class Config: port: int rate: float config = Config() config.update({"port": "8080", "rate": "0.5"}) # Strings coerced! config.validate(Config) # ✓ Works! port=8080, rate=0.5 ``` **Implementation:** - `coerce_value()`: Core coercion logic with recursive support - Smart type detection and conversion with fallback to original value - Integration with `_validate_field()` in schema.py 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * Enhance config API with freeze/unfreeze and MISSING support Add advanced configuration features for better control and partial config support. **New Features:** 1. **Frozen Configs** - Prevent mutations after initialization: ```python config.freeze() # Lock config config.set("key", "value") # Raises FrozenConfigError config.unfreeze() # Allow mutations again ``` 2. **MISSING Sentinel** - Support partial configs with required-but-not-yet-set values: ```python config = Config(allow_missing=True) config.update({"api_key": MISSING}) # Placeholder for later config.set("api_key", os.getenv("API_KEY")) # Fill in config.validate(schema) # Ensure complete (allow_missing=False by default) ``` 3. **Continuous Validation** - Validate on every mutation when schema provided: ```python config = Config(schema=MySchema) # Enable continuous validation config.update(data) # Validates immediately! config.set("port", "invalid") # Raises ValidationError ``` **API Changes:** - Add `freeze()`, `unfreeze()`, `is_frozen()` methods - Add `MISSING` sentinel constant - Add `allow_missing` parameter to `Config()` and `validate()` - Integrate type coercion into validation pipeline - Enhanced error messages with field paths **Tests:** - Comprehensive test coverage for freeze/unfreeze behavior - MISSING sentinel edge cases - Continuous validation scenarios - Updated existing tests for new API patterns 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * Comprehensive documentation updates for v0.0.5 Update all documentation to reflect the new API design, removed CLI module, and new features (type coercion, freeze/unfreeze, MISSING sentinel). **Major Changes:** 1. **Installation Guide** (`installation.md`): - Simplified just installation instructions (use package managers) - Removed optional dependencies section (minimal deps by design) - Cleaner setup instructions 2. **Quickstart Guide** (`quickstart.md`): - Update to new API: `Config()` + `.update()` pattern (no more `.load()`) - Show manual CLI override parsing (3-line pattern with ast.literal_eval) - Method chaining examples for cleaner code 3. **CLI Overrides** (`cli.md` - major rewrite): - Remove `Config.from_cli()`, `parse_override()` references - Show auto-detection pattern: `.update()` handles both files and overrides - Examples for argparse, Click, Typer, Fire - Manual override parsing with `parse_overrides()` for advanced use - Simplified mental model: just loop over args! 4. **Core User Guides**: - `basics.md`: Update all examples to `Config().update()` pattern - `operators.md`: Add composition decision flow diagram - `advanced.md`: Add frozen configs and MISSING sentinel sections - `schema-validation.md`: Document continuous validation and type coercion - `references.md`: Update API references 5. **Index Page** (`index.md`): - Update feature descriptions (continuous validation) - Modernize all code examples - Show new patterns throughout 6. **Examples Cleanup**: - Delete outdated examples: `simple.md`, `deep-learning.md`, `custom-classes.md` - Add new `quick-reference.md` with concise API overview 7. **MkDocs Config** (`mkdocs.yml`): - Remove examples section from navigation - Add quick-reference to user guide - Update structure for clearer organization **Documentation Philosophy:** - Show the simplest pattern first (Config().update()) - Encourage users to use their preferred CLI library - Emphasize composition-by-default with explicit operators - Highlight continuous validation benefits 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]> * Add schema suffix to schema dataclasses in docs examples * Improve github workflows * Fix GitHub Actions workflows and type checking errors Workflow Fixes: - Fix YAML syntax in check_pull_request_title.yml - Remove problematic types parameter (not needed with custom subjectPattern) Type Checking Fixes (66 errors resolved): - Keep strict mypy flags enabled (disallow_any_generics, warn_unreachable) - Add explicit type parameters throughout (dict[str, Any], list[Any], set[Any]) - Add type annotations to helper functions (_is_union_type, etc.) - Fix PathLike type parameter (Union[str, os.PathLike[str]]) - Add proper type hints to levenshtein_distance and ensure_tuple - Add type assertions for dataclass field types - Handle false positive unreachable warnings with targeted type ignores Files modified: - schema.py, coercion.py: Type annotations and assertions - config.py, loader.py, items.py: Dict/list/set type parameters - preprocessor.py, operators.py, resolver.py: Type parameters - utils/types.py, utils/misc.py, utils/module.py: Type fixes - errors/suggestions.py: Levenshtein distance types All 24 source files pass strict type checking. * Fix false mypy flag * Add codecov.yml. Fix more mypy. --------- Co-authored-by: Claude <[email protected]>
1 parent c95f7e6 commit ead67d7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+2721
-1698
lines changed

.codecov.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Codecov Configuration
2+
# Documentation: https://docs.codecov.com/docs/codecovyml-reference
3+
4+
coverage:
5+
precision: 2 # Number of decimal places (0-5)
6+
round: down # How to round coverage (down/up/nearest)
7+
range: 70..100 # Color coding range (red at 70%, green at 100%)
8+
9+
status:
10+
# Project coverage: overall repository coverage
11+
project:
12+
default:
13+
target: 90% # Minimum coverage threshold
14+
threshold: null # No threshold - only fail if below target
15+
base: auto # Compare against base branch
16+
informational: false # Fail the check if below target
17+
18+
# Patch coverage: coverage on changed lines only
19+
patch:
20+
default:
21+
target: 80% # New code should have at least 80% coverage
22+
threshold: 0% # No wiggle room for patch coverage
23+
base: auto
24+
informational: false # Fail if new code doesn't meet target
25+
26+
# Pull request comment configuration
27+
comment:
28+
layout: "diff, flags, files, footer" # What to show in PR comments
29+
behavior: default # Comment on all PRs
30+
require_changes: false # Comment even if coverage unchanged
31+
require_base: false # Comment even without base report
32+
require_head: true # Only comment if head report exists
33+
34+
# Paths to ignore in coverage reports
35+
ignore:
36+
- "tests/*" # Test files
37+
- "tests/**/*" # All test subdirectories
38+
- "docs/*" # Documentation
39+
- "site/*" # Built documentation site
40+
- "htmlcov/*" # Coverage HTML reports
41+
- ".venv/*" # Virtual environment
42+
- ".tox/*" # Tox environments
43+
- "**/__pycache__/*" # Python cache
44+
- "**/conftest.py" # Pytest configuration
45+
- "update_tests.py" # Utility scripts
46+
47+
# GitHub Checks configuration
48+
github_checks:
49+
annotations: true # Show coverage annotations on changed files

.github/actions/setup/action.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: 'Setup Environment'
2+
description: 'Setup Python with uv and install dependencies'
3+
inputs:
4+
python-version:
5+
description: 'Python version'
6+
required: false
7+
default: '3.12'
8+
install-deps:
9+
description: 'Install dependencies'
10+
required: false
11+
default: 'true'
12+
install-groups:
13+
description: 'Dependency groups to install'
14+
required: false
15+
default: '--all-extras --all-groups'
16+
runs:
17+
using: 'composite'
18+
steps:
19+
- name: Install uv
20+
uses: astral-sh/setup-uv@v6
21+
with:
22+
python-version: ${{ inputs.python-version }}
23+
enable-cache: true
24+
25+
- name: Install dependencies
26+
if: inputs.install-deps == 'true'
27+
run: uv sync ${{ inputs.install-groups }}
28+
shell: bash

.github/scripts/test_summary.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/usr/bin/env python3
2+
"""Generate GitHub Actions test summary from JUnit XML."""
3+
4+
import sys
5+
import xml.etree.ElementTree as ET
6+
from pathlib import Path
7+
8+
9+
def main() -> int:
10+
"""Parse JUnit XML and write summary to GitHub Actions step summary."""
11+
xml_path = Path("test-results.xml")
12+
13+
if not xml_path.exists():
14+
print("⚠️ No test results found", file=sys.stderr)
15+
return 1
16+
17+
try:
18+
tree = ET.parse(xml_path)
19+
root = tree.getroot()
20+
21+
tests = int(root.get("tests", 0))
22+
failures = int(root.get("failures", 0))
23+
errors = int(root.get("errors", 0))
24+
skipped = int(root.get("skipped", 0))
25+
passed = tests - failures - errors - skipped
26+
27+
# Determine status emoji
28+
if failures + errors > 0:
29+
status = "❌"
30+
elif skipped == tests:
31+
status = "⏭️"
32+
else:
33+
status = "✅"
34+
35+
# Print summary lines
36+
print(f"{status} **Test Results Summary**")
37+
print(f"- ✅ Passed: {passed}")
38+
print(f"- ❌ Failed: {failures}")
39+
print(f"- ⚠️ Errors: {errors}")
40+
print(f"- ⏭️ Skipped: {skipped}")
41+
print(f"- **Total: {tests}**")
42+
43+
# Exit with error if tests failed
44+
return 1 if (failures + errors > 0) else 0
45+
46+
except ET.ParseError as e:
47+
print(f"❌ Failed to parse XML: {e}", file=sys.stderr)
48+
return 1
49+
50+
51+
if __name__ == "__main__":
52+
sys.exit(main())
Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,47 @@
11
name: "Check PR title"
2+
23
on:
34
pull_request:
45
types: [edited, opened, synchronize, reopened]
56

7+
concurrency:
8+
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
9+
cancel-in-progress: true
10+
11+
permissions:
12+
contents: read
13+
pull-requests: read
14+
statuses: write
15+
616
jobs:
717
pr-title-check:
818
runs-on: ubuntu-latest
19+
timeout-minutes: 5
920
if: ${{ github.event.pull_request.user.login != 'allcontributors[bot]' }}
1021
steps:
11-
# Echo the user's login
12-
- name: Echo user login
13-
run: echo ${{ github.event.pull_request.user.login }}
14-
15-
- uses: naveenk1223/action-pr-title@master
22+
- uses: amannn/[email protected]
23+
env:
24+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1625
with:
26+
# Require imperative mood (e.g. "Add feature" not "Adds feature")
1727
# ^ Start of string
1828
# [A-Z] First character must be an uppercase ASCII letter
19-
# [a-zA-Z]* Followed by zero or more ASCII letters
20-
# (?<![^s]s) Negative lookbehind: disallow a single 's' at the end of the first word
21-
# ( .+)+ At least one space and one or more characters (requires more words)
29+
# [a-z]* Followed by zero or more lowercase letters
30+
# (?<![^s]s) Negative lookbehind: disallow single 's' at end of first word
31+
# ( .+)+ At least one space and one or more characters
2232
# [^.] Final character must not be a period
2333
# $ End of string
24-
regex: "^[A-Z][a-zA-Z]*(?<![^s]s)( .+)+[^.]$"
25-
# Valid titles:
26-
# - "Do something"
27-
# - "Address something"
28-
# Invalid title:
29-
# - "do something"
30-
# - "Do something."
31-
# - "Does something"
32-
# - "Do"
33-
# - "Addresses something"
34-
min_length: 10
35-
max_length: 72
34+
subjectPattern: '^[A-Z][a-z]*(?<![^s]s)( .+)+[^.]$'
35+
subjectPatternError: |
36+
The PR title must use imperative mood and proper formatting:
37+
- Start with uppercase letter (e.g., "Add" not "add")
38+
- Use base form of verb (e.g., "Add" not "Adds" or "Adding")
39+
- Have multiple words (minimum 10 characters)
40+
- Not end with a period
41+
42+
Valid: "Add new feature", "Fix bug in parser"
43+
Invalid: "add feature", "Adds feature", "Add.", "Fix"
44+
# Disable type prefixes (we don't use conventional commits format)
45+
requireScope: false
46+
ignoreLabels:
47+
- ignore-title-check

.github/workflows/ci-full.yml

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
name: CI Full
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
workflow_dispatch:
8+
9+
concurrency:
10+
group: ${{ github.workflow }}-${{ github.ref }}
11+
cancel-in-progress: false
12+
13+
permissions:
14+
contents: read
15+
checks: write
16+
17+
env:
18+
FORCE_COLOR: 1
19+
20+
jobs:
21+
tests:
22+
name: Python ${{ matrix.python }}
23+
runs-on: ubuntu-latest
24+
timeout-minutes: 30
25+
strategy:
26+
fail-fast: false
27+
matrix:
28+
python: ["3.10", "3.11", "3.12"]
29+
30+
steps:
31+
- uses: actions/checkout@v4
32+
with:
33+
fetch-depth: 0
34+
35+
- uses: ./.github/actions/setup
36+
with:
37+
python-version: ${{ matrix.python }}
38+
39+
- name: Run all tests with coverage
40+
run: |
41+
uv run coverage run -m pytest tests --durations=10 \
42+
--junit-xml=test-results.xml \
43+
--html=test-report.html --self-contained-html
44+
45+
- name: Generate test summary
46+
if: always()
47+
run: |
48+
echo "## Test Results - Python ${{ matrix.python }}" >> $GITHUB_STEP_SUMMARY
49+
python3 .github/scripts/test_summary.py >> $GITHUB_STEP_SUMMARY
50+
51+
- name: Upload test results
52+
if: always()
53+
uses: actions/upload-artifact@v4
54+
with:
55+
name: test-results-py${{ matrix.python }}
56+
retention-days: 7
57+
path: |
58+
test-results.xml
59+
test-report.html
60+
.coverage
61+
62+
- name: Generate coverage report
63+
if: matrix.python == '3.12'
64+
run: uv run coverage xml
65+
66+
- name: Upload coverage to Codecov
67+
if: matrix.python == '3.12'
68+
uses: codecov/[email protected]
69+
with:
70+
files: ./coverage.xml
71+
fail_ci_if_error: true
72+
env:
73+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

.github/workflows/ci.yml

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
8+
workflow_dispatch:
9+
10+
concurrency:
11+
group: ${{ github.workflow }}-${{ github.ref }}
12+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
13+
14+
permissions:
15+
contents: read
16+
checks: write
17+
18+
env:
19+
FORCE_COLOR: 1
20+
21+
jobs:
22+
quality:
23+
name: ${{ matrix.check }}
24+
runs-on: ubuntu-latest
25+
timeout-minutes: 10
26+
strategy:
27+
fail-fast: false
28+
matrix:
29+
check:
30+
- format
31+
- lint
32+
- types
33+
34+
steps:
35+
- uses: actions/checkout@v4
36+
with:
37+
fetch-depth: 0
38+
39+
- uses: ./.github/actions/setup
40+
with:
41+
python-version: "3.12"
42+
install-deps: ${{ matrix.check == 'types' && 'true' || 'false' }}
43+
44+
- name: Run format check
45+
if: matrix.check == 'format'
46+
run: uvx ruff format --diff
47+
48+
- name: Run lint check
49+
if: matrix.check == 'lint'
50+
run: uvx ruff check
51+
52+
- name: Run type check
53+
if: matrix.check == 'types'
54+
run: uv run mypy src
55+
56+
tests:
57+
name: Tests (Python 3.12)
58+
runs-on: ubuntu-latest
59+
timeout-minutes: 30
60+
61+
steps:
62+
- uses: actions/checkout@v4
63+
with:
64+
fetch-depth: 0
65+
66+
- uses: ./.github/actions/setup
67+
with:
68+
python-version: "3.12"
69+
70+
- name: Run pytest with coverage
71+
run: |
72+
uv run coverage run -m pytest tests --durations=10 -m "not slow" \
73+
--junit-xml=test-results.xml \
74+
--html=test-report.html --self-contained-html
75+
76+
- name: Generate test summary
77+
if: always()
78+
run: python3 .github/scripts/test_summary.py >> $GITHUB_STEP_SUMMARY
79+
80+
- name: Upload test results
81+
if: always()
82+
uses: actions/upload-artifact@v4
83+
with:
84+
name: test-results-py312
85+
retention-days: 7
86+
path: |
87+
test-results.xml
88+
test-report.html
89+
.coverage
90+
91+
- name: Generate coverage report
92+
run: uv run coverage xml
93+
94+
- name: Upload coverage to Codecov
95+
uses: codecov/[email protected]
96+
with:
97+
files: ./coverage.xml
98+
fail_ci_if_error: true
99+
env:
100+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

0 commit comments

Comments
 (0)