path-link 0.2.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.
- path_link-0.2.0.dist-info/METADATA +643 -0
- path_link-0.2.0.dist-info/RECORD +20 -0
- path_link-0.2.0.dist-info/WHEEL +5 -0
- path_link-0.2.0.dist-info/entry_points.txt +2 -0
- path_link-0.2.0.dist-info/licenses/LICENSE +23 -0
- path_link-0.2.0.dist-info/top_level.txt +1 -0
- project_paths/__init__.py +165 -0
- project_paths/builder.py +84 -0
- project_paths/builtin_validators/__init__.py +6 -0
- project_paths/builtin_validators/sandbox.py +159 -0
- project_paths/builtin_validators/strict.py +126 -0
- project_paths/cli.py +201 -0
- project_paths/docs/ai_guidelines.md +668 -0
- project_paths/docs/developer_guide.md +399 -0
- project_paths/docs/metadata.json +119 -0
- project_paths/get_paths.py +104 -0
- project_paths/main.py +2 -0
- project_paths/model.py +76 -0
- project_paths/project_paths_static.py +14 -0
- project_paths/validation.py +94 -0
@@ -0,0 +1,399 @@
|
|
1
|
+
# CLAUDE.md
|
2
|
+
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
4
|
+
|
5
|
+
## Project Overview
|
6
|
+
|
7
|
+
`ptool-serena` is a Python library for type-safe project path management with built-in validation. It uses Pydantic models to dynamically create path configurations from `pyproject.toml` or custom `.paths` files, and can generate static dataclass models for enhanced IDE support.
|
8
|
+
|
9
|
+
## Development Setup
|
10
|
+
|
11
|
+
### Environment
|
12
|
+
|
13
|
+
Uses `uv` for dependency management:
|
14
|
+
|
15
|
+
```bash
|
16
|
+
# Setup environment
|
17
|
+
uv venv
|
18
|
+
source .venv/bin/activate # macOS/Linux
|
19
|
+
uv pip install -e ".[test]"
|
20
|
+
```
|
21
|
+
|
22
|
+
The project must be installed in editable mode. Never modify `PYTHONPATH` - all imports resolve through the editable install.
|
23
|
+
|
24
|
+
### Common Commands
|
25
|
+
|
26
|
+
```bash
|
27
|
+
# Run all tests
|
28
|
+
uv run pytest
|
29
|
+
|
30
|
+
# Run tests with coverage
|
31
|
+
uv run pytest --cov=src --cov-report=term-missing
|
32
|
+
|
33
|
+
# Run specific test file
|
34
|
+
uv run pytest tests/test_validators.py
|
35
|
+
|
36
|
+
# Lint code
|
37
|
+
uv run ruff check .
|
38
|
+
|
39
|
+
# Format code
|
40
|
+
uv run ruff format .
|
41
|
+
|
42
|
+
# Type check
|
43
|
+
uv run mypy src/
|
44
|
+
|
45
|
+
# Regenerate static model (required after pyproject.toml changes)
|
46
|
+
uv run python -c "from project_paths import write_dataclass_file; write_dataclass_file()"
|
47
|
+
|
48
|
+
# Or use justfile commands
|
49
|
+
just test
|
50
|
+
just lint
|
51
|
+
just format
|
52
|
+
just regen
|
53
|
+
```
|
54
|
+
|
55
|
+
### Smoke Test
|
56
|
+
|
57
|
+
Verify setup is working:
|
58
|
+
```bash
|
59
|
+
uv run python scripts/smoke_import.py
|
60
|
+
# Or one-liner:
|
61
|
+
uv run python -c "from project_paths import ProjectPaths; p = ProjectPaths.from_pyproject(); print('✅ OK:', len(p.to_dict()), 'paths loaded')"
|
62
|
+
```
|
63
|
+
|
64
|
+
## Architecture
|
65
|
+
|
66
|
+
### Core Components
|
67
|
+
|
68
|
+
1. **ProjectPaths Model** (`src/project_paths/model.py`)
|
69
|
+
- Main API for path management
|
70
|
+
- Uses Pydantic dynamic model creation via `create_model()`
|
71
|
+
- Direct instantiation is disabled - use factory methods only:
|
72
|
+
- `ProjectPaths.from_pyproject()` - loads from `[tool.project_paths]` in pyproject.toml
|
73
|
+
- `ProjectPaths.from_config(".paths")` - loads from dotenv-style files
|
74
|
+
- All path access returns `pathlib.Path` objects
|
75
|
+
- Supports dict-style access via `__getitem__`
|
76
|
+
|
77
|
+
2. **Builder System** (`src/project_paths/builder.py`)
|
78
|
+
- `build_field_definitions()` - constructs Pydantic field definitions from config
|
79
|
+
- `make_path_factory()` - creates lazy path resolvers with environment variable expansion
|
80
|
+
- `get_paths_from_pyproject()` - TOML parser for pyproject.toml
|
81
|
+
- `get_paths_from_dot_paths()` - dotenv parser for .paths files
|
82
|
+
|
83
|
+
3. **Validation Framework** (`src/project_paths/validation.py`)
|
84
|
+
- Protocol-based system using `PathValidator` protocol
|
85
|
+
- `ValidationResult` aggregates `Finding` objects with severity levels (INFO, WARNING, ERROR)
|
86
|
+
- `validate_or_raise()` helper throws `PathValidationError` on failure
|
87
|
+
- `CompositeValidator` chains multiple validators
|
88
|
+
|
89
|
+
4. **Built-in Validators** (`src/project_paths/builtin_validators/`)
|
90
|
+
- `StrictPathValidator` - enforces path existence, type (file/dir), and symlink rules
|
91
|
+
- Configurable via `required`, `must_be_dir`, `must_be_file`, `allow_symlinks` parameters
|
92
|
+
- `SandboxPathValidator` - prevents path traversal attacks and enforces base directory sandbox
|
93
|
+
- Configurable via `base_dir_key`, `allow_absolute`, `strict_mode`, `check_paths` parameters
|
94
|
+
- Error codes: `PATH_TRAVERSAL_ATTEMPT`, `ABSOLUTE_PATH_BLOCKED`, `PATH_ESCAPES_SANDBOX`
|
95
|
+
|
96
|
+
5. **Static Model Generation** (`src/project_paths/get_paths.py`)
|
97
|
+
- `write_dataclass_file()` generates `project_paths_static.py` from current config
|
98
|
+
- Uses atomic file replacement via temp file to avoid TOCTOU issues
|
99
|
+
- Must be regenerated after modifying `[tool.project_paths]` in pyproject.toml
|
100
|
+
|
101
|
+
6. **Documentation Access** (`src/project_paths/__init__.py`)
|
102
|
+
- `get_ai_guidelines()` - Returns AI assistant usage patterns (bundled assistant_context.md)
|
103
|
+
- `get_developer_guide()` - Returns architecture and development guide (bundled CLAUDE.md)
|
104
|
+
- `get_metadata()` - Returns machine-readable project metadata (bundled assistant_handoff.json)
|
105
|
+
- Documentation bundled via `[tool.setuptools.package-data]` for offline access
|
106
|
+
|
107
|
+
### Configuration Format
|
108
|
+
|
109
|
+
In `pyproject.toml`:
|
110
|
+
```toml
|
111
|
+
[tool.project_paths.paths]
|
112
|
+
config_dir = "config"
|
113
|
+
data_dir = "data"
|
114
|
+
|
115
|
+
[tool.project_paths.files]
|
116
|
+
settings_file = "config/settings.json"
|
117
|
+
```
|
118
|
+
|
119
|
+
In `.paths` files (dotenv format, no sections):
|
120
|
+
```
|
121
|
+
config_dir=config
|
122
|
+
data_dir=data
|
123
|
+
settings_file=config/settings.json
|
124
|
+
```
|
125
|
+
|
126
|
+
### Import Structure
|
127
|
+
|
128
|
+
The project uses `src` layout. All imports should be absolute from package name:
|
129
|
+
```python
|
130
|
+
from project_paths import ProjectPaths, ValidationResult, StrictPathValidator
|
131
|
+
from project_paths.model import _ProjectPathsBase
|
132
|
+
from project_paths.validation import Finding, Severity
|
133
|
+
```
|
134
|
+
|
135
|
+
## Critical Rules
|
136
|
+
|
137
|
+
### ProjectPaths Usage
|
138
|
+
|
139
|
+
**Correct:**
|
140
|
+
```python
|
141
|
+
from project_paths import ProjectPaths
|
142
|
+
|
143
|
+
# Load from pyproject.toml
|
144
|
+
paths = ProjectPaths.from_pyproject()
|
145
|
+
|
146
|
+
# Load from custom config
|
147
|
+
paths = ProjectPaths.from_config(".paths")
|
148
|
+
|
149
|
+
# Access paths
|
150
|
+
config = paths.config_dir
|
151
|
+
settings = paths["settings_file"]
|
152
|
+
```
|
153
|
+
|
154
|
+
**Incorrect:**
|
155
|
+
```python
|
156
|
+
# NEVER do this - direct instantiation is disabled
|
157
|
+
paths = ProjectPaths() # Raises NotImplementedError
|
158
|
+
```
|
159
|
+
|
160
|
+
### Static Model Sync
|
161
|
+
|
162
|
+
After any change to `[tool.project_paths]` in pyproject.toml, you MUST regenerate the static model:
|
163
|
+
```bash
|
164
|
+
uv run python -c "from project_paths import write_dataclass_file; write_dataclass_file()"
|
165
|
+
```
|
166
|
+
|
167
|
+
The CI check `just check-regen` verifies this file is in sync.
|
168
|
+
|
169
|
+
### TOCTOU Prevention
|
170
|
+
|
171
|
+
Avoid time-of-check-time-of-use vulnerabilities in filesystem operations:
|
172
|
+
|
173
|
+
**Unsafe:**
|
174
|
+
```python
|
175
|
+
if not path.exists():
|
176
|
+
path.mkdir() # Race condition possible
|
177
|
+
```
|
178
|
+
|
179
|
+
**Safe:**
|
180
|
+
```python
|
181
|
+
try:
|
182
|
+
path.mkdir(exist_ok=False)
|
183
|
+
except FileExistsError:
|
184
|
+
if not path.is_dir():
|
185
|
+
raise
|
186
|
+
```
|
187
|
+
|
188
|
+
The `StrictPathValidator` includes `allow_symlinks` parameter to mitigate symlink-based attacks.
|
189
|
+
|
190
|
+
### Path Traversal Protection
|
191
|
+
|
192
|
+
Use `SandboxPathValidator` to prevent path traversal attacks and ensure paths stay within the project sandbox:
|
193
|
+
|
194
|
+
**Maximum Security (Recommended):**
|
195
|
+
```python
|
196
|
+
from project_paths import ProjectPaths, SandboxPathValidator
|
197
|
+
|
198
|
+
paths = ProjectPaths.from_pyproject()
|
199
|
+
validator = SandboxPathValidator(
|
200
|
+
base_dir_key="base_dir",
|
201
|
+
allow_absolute=False, # Block all absolute paths
|
202
|
+
strict_mode=True, # Block '..' patterns
|
203
|
+
check_paths=[] # Check all paths
|
204
|
+
)
|
205
|
+
|
206
|
+
result = validator.validate(paths)
|
207
|
+
if not result.ok():
|
208
|
+
for error in result.errors():
|
209
|
+
print(f"Security issue: {error.code} in {error.field}")
|
210
|
+
```
|
211
|
+
|
212
|
+
**Permissive Mode (Allow absolute paths within sandbox):**
|
213
|
+
```python
|
214
|
+
validator = SandboxPathValidator(
|
215
|
+
allow_absolute=True, # Allow absolute paths if within base_dir
|
216
|
+
strict_mode=False # Allow '..' if it resolves safely
|
217
|
+
)
|
218
|
+
```
|
219
|
+
|
220
|
+
**Security Features:**
|
221
|
+
- Detects `..` path traversal patterns in strict mode
|
222
|
+
- Validates all paths resolve within `base_dir` after symlink resolution
|
223
|
+
- Configurable absolute path handling
|
224
|
+
- Clear error codes for security violations
|
225
|
+
|
226
|
+
**Error Codes:**
|
227
|
+
- `PATH_TRAVERSAL_ATTEMPT` - Path contains `..` pattern (strict mode only)
|
228
|
+
- `ABSOLUTE_PATH_BLOCKED` - Absolute path not allowed (when `allow_absolute=False`)
|
229
|
+
- `PATH_ESCAPES_SANDBOX` - Resolved path is outside `base_dir`
|
230
|
+
- `SANDBOX_BASE_MISSING` - Base directory key not found
|
231
|
+
- `SANDBOX_BASE_UNRESOLVABLE` - Cannot resolve base directory
|
232
|
+
- `PATH_UNRESOLVABLE` - Cannot resolve path (warning level)
|
233
|
+
|
234
|
+
## Testing
|
235
|
+
|
236
|
+
### Test Structure
|
237
|
+
|
238
|
+
- Tests mirror source structure: `src/project_paths/model.py` → `tests/test_model.py`
|
239
|
+
- Tests use pytest fixtures and `tmp_path` for isolated filesystem operations
|
240
|
+
- Mock `_ProjectPathsBase` instances in tests using `MagicMock(spec=_ProjectPathsBase)`
|
241
|
+
|
242
|
+
### Running Tests
|
243
|
+
|
244
|
+
```bash
|
245
|
+
# All tests
|
246
|
+
uv run pytest
|
247
|
+
|
248
|
+
# Specific test file
|
249
|
+
uv run pytest tests/test_validators.py
|
250
|
+
|
251
|
+
# With coverage
|
252
|
+
uv run pytest --cov=src --cov-report=term-missing
|
253
|
+
|
254
|
+
# Single test
|
255
|
+
uv run pytest tests/test_validators.py::test_strict_validator_required
|
256
|
+
```
|
257
|
+
|
258
|
+
## Validation Patterns
|
259
|
+
|
260
|
+
### Basic Validation
|
261
|
+
```python
|
262
|
+
from project_paths import ProjectPaths, validate_or_raise, StrictPathValidator
|
263
|
+
|
264
|
+
paths = ProjectPaths.from_pyproject()
|
265
|
+
validator = StrictPathValidator(
|
266
|
+
required=["config_dir", "data_dir"],
|
267
|
+
must_be_dir=["config_dir"],
|
268
|
+
must_be_file=["settings_file"]
|
269
|
+
)
|
270
|
+
|
271
|
+
# Raises PathValidationError on failure
|
272
|
+
validate_or_raise(paths, validator)
|
273
|
+
```
|
274
|
+
|
275
|
+
### Composite Validation
|
276
|
+
```python
|
277
|
+
from project_paths import CompositeValidator
|
278
|
+
|
279
|
+
validator = CompositeValidator(parts=[
|
280
|
+
StrictPathValidator(required=["config_dir"]),
|
281
|
+
CustomValidator()
|
282
|
+
])
|
283
|
+
|
284
|
+
result = validator.validate(paths)
|
285
|
+
if not result.ok():
|
286
|
+
for error in result.errors():
|
287
|
+
print(f"{error.code}: {error.message}")
|
288
|
+
```
|
289
|
+
|
290
|
+
### Custom Validators
|
291
|
+
|
292
|
+
Implement the `PathValidator` protocol:
|
293
|
+
```python
|
294
|
+
from dataclasses import dataclass
|
295
|
+
from project_paths import ValidationResult, Finding, Severity
|
296
|
+
|
297
|
+
@dataclass
|
298
|
+
class CustomValidator:
|
299
|
+
def validate(self, paths) -> ValidationResult:
|
300
|
+
result = ValidationResult()
|
301
|
+
# Add validation logic
|
302
|
+
if some_condition:
|
303
|
+
result.add(Finding(
|
304
|
+
severity=Severity.ERROR,
|
305
|
+
code="CUSTOM_ERROR",
|
306
|
+
field="field_name",
|
307
|
+
message="Error description"
|
308
|
+
))
|
309
|
+
return result
|
310
|
+
```
|
311
|
+
|
312
|
+
## Accessing Documentation Programmatically
|
313
|
+
|
314
|
+
The package includes bundled documentation accessible via three helper functions. This enables offline access and is especially useful for AI assistants helping users.
|
315
|
+
|
316
|
+
```python
|
317
|
+
from project_paths import get_ai_guidelines, get_developer_guide, get_metadata
|
318
|
+
import json
|
319
|
+
|
320
|
+
# Access AI assistant guidelines (assistant_context.md)
|
321
|
+
ai_docs = get_ai_guidelines()
|
322
|
+
|
323
|
+
# Access developer guide (this file - CLAUDE.md)
|
324
|
+
dev_docs = get_developer_guide()
|
325
|
+
|
326
|
+
# Access machine-readable metadata (assistant_handoff.json)
|
327
|
+
metadata = json.loads(get_metadata())
|
328
|
+
print(f"Version: {metadata['version']}")
|
329
|
+
print(f"Public APIs: {len(metadata['public_api'])}")
|
330
|
+
```
|
331
|
+
|
332
|
+
**Location:** Documentation files are stored in `src/project_paths/docs/` and bundled with the package via `[tool.setuptools.package-data]` in `pyproject.toml`.
|
333
|
+
|
334
|
+
**Use Cases:**
|
335
|
+
- AI assistants helping users with the package
|
336
|
+
- Offline/airgapped environments
|
337
|
+
- Programmatic access to package metadata
|
338
|
+
|
339
|
+
## Package Structure
|
340
|
+
|
341
|
+
```
|
342
|
+
src/project_paths/
|
343
|
+
├── __init__.py # Public API exports + documentation access functions
|
344
|
+
├── model.py # ProjectPaths class and factory methods
|
345
|
+
├── builder.py # Config loading and field building
|
346
|
+
├── get_paths.py # Static model generation
|
347
|
+
├── validation.py # Validation framework
|
348
|
+
├── project_paths_static.py # Auto-generated static model
|
349
|
+
├── cli.py # CLI tool (ptool command)
|
350
|
+
├── builtin_validators/
|
351
|
+
│ ├── __init__.py
|
352
|
+
│ ├── strict.py # StrictPathValidator
|
353
|
+
│ └── sandbox.py # SandboxPathValidator
|
354
|
+
└── docs/ # Bundled documentation (accessible offline)
|
355
|
+
├── ai_guidelines.md # AI assistant usage patterns (assistant_context.md)
|
356
|
+
├── developer_guide.md # Architecture & development (CLAUDE.md)
|
357
|
+
└── metadata.json # Machine-readable metadata (assistant_handoff.json)
|
358
|
+
|
359
|
+
tests/
|
360
|
+
├── test_validators.py
|
361
|
+
├── test_sandbox_validator.py
|
362
|
+
├── test_static_model_equivalence.py
|
363
|
+
├── test_path_policy.py
|
364
|
+
├── test_env_expansion.py
|
365
|
+
├── test_cli.py
|
366
|
+
└── test_example_project.py
|
367
|
+
|
368
|
+
scripts/
|
369
|
+
├── smoke_import.py # Quick verification script
|
370
|
+
└── test_coverage_tool.py # Test coverage analysis
|
371
|
+
```
|
372
|
+
|
373
|
+
## Dependencies
|
374
|
+
|
375
|
+
**Runtime:**
|
376
|
+
- `pydantic>=2.11.0` - Dynamic model creation and validation
|
377
|
+
- `python-dotenv>=1.0.1` - Parsing .paths files
|
378
|
+
|
379
|
+
**Development:**
|
380
|
+
- `pytest` - Test framework
|
381
|
+
- `pytest-cov` - Coverage reporting
|
382
|
+
- `mypy` - Type checking
|
383
|
+
- `ruff` - Linting and formatting
|
384
|
+
|
385
|
+
## Troubleshooting
|
386
|
+
|
387
|
+
### Import errors
|
388
|
+
- Ensure virtual environment is activated
|
389
|
+
- Verify editable install: `uv pip install -e ".[test]"`
|
390
|
+
- Check import uses package name: `from project_paths import ...`
|
391
|
+
|
392
|
+
### Static model out of sync
|
393
|
+
- Run: `just check-regen` to detect
|
394
|
+
- Fix with: `just regen`
|
395
|
+
|
396
|
+
### Test failures
|
397
|
+
- Use `tmp_path` fixture for filesystem tests
|
398
|
+
- Mock `_ProjectPathsBase` when testing validators
|
399
|
+
- Ensure tests are isolated and don't depend on cwd state
|
@@ -0,0 +1,119 @@
|
|
1
|
+
{
|
2
|
+
"dist_name": "ptool-serena",
|
3
|
+
"import_name": "project_paths",
|
4
|
+
"version": "0.2.0",
|
5
|
+
"factories": [
|
6
|
+
"ProjectPaths.from_pyproject",
|
7
|
+
"ProjectPaths.from_config"
|
8
|
+
],
|
9
|
+
"cli_commands": [
|
10
|
+
{
|
11
|
+
"command": "ptool print",
|
12
|
+
"description": "Print all configured paths as JSON",
|
13
|
+
"options": ["--source {pyproject,config}", "--config PATH"]
|
14
|
+
},
|
15
|
+
{
|
16
|
+
"command": "ptool validate",
|
17
|
+
"description": "Validate project structure",
|
18
|
+
"options": ["--source {pyproject,config}", "--config PATH", "--strict", "--raise"]
|
19
|
+
},
|
20
|
+
{
|
21
|
+
"command": "ptool gen-static",
|
22
|
+
"description": "Generate static dataclass model",
|
23
|
+
"options": ["--out PATH"]
|
24
|
+
}
|
25
|
+
],
|
26
|
+
"validators": [
|
27
|
+
{
|
28
|
+
"name": "StrictPathValidator",
|
29
|
+
"module": "project_paths.builtin_validators.strict",
|
30
|
+
"description": "Ensures paths exist and match expected types (file/directory)"
|
31
|
+
},
|
32
|
+
{
|
33
|
+
"name": "SandboxPathValidator",
|
34
|
+
"module": "project_paths.builtin_validators.sandbox",
|
35
|
+
"description": "Prevents path traversal attacks and enforces base directory sandbox"
|
36
|
+
},
|
37
|
+
{
|
38
|
+
"name": "CompositeValidator",
|
39
|
+
"module": "project_paths.validation",
|
40
|
+
"description": "Combines multiple validators into a single validation pipeline"
|
41
|
+
}
|
42
|
+
],
|
43
|
+
"public_api": [
|
44
|
+
"ProjectPaths",
|
45
|
+
"write_dataclass_file",
|
46
|
+
"Severity",
|
47
|
+
"Finding",
|
48
|
+
"ValidationResult",
|
49
|
+
"PathValidator",
|
50
|
+
"PathValidationError",
|
51
|
+
"validate_or_raise",
|
52
|
+
"CompositeValidator",
|
53
|
+
"StrictPathValidator",
|
54
|
+
"SandboxPathValidator",
|
55
|
+
"get_ai_guidelines",
|
56
|
+
"get_developer_guide",
|
57
|
+
"get_metadata"
|
58
|
+
],
|
59
|
+
"invariants": [
|
60
|
+
"No direct constructor calls (raises NotImplementedError)",
|
61
|
+
"Key=value .paths (no sections, dotenv format)",
|
62
|
+
"All paths are pathlib.Path objects",
|
63
|
+
"Static model must be regenerated after pyproject.toml changes"
|
64
|
+
],
|
65
|
+
"test_status": {
|
66
|
+
"passing": 53,
|
67
|
+
"coverage": "60%",
|
68
|
+
"target_coverage": "90%",
|
69
|
+
"last_verified": "2025-10-10"
|
70
|
+
},
|
71
|
+
"critical_commands": {
|
72
|
+
"smoke_test": "uv run python -c \"from project_paths import ProjectPaths; p = ProjectPaths.from_pyproject(); print('✅ OK:', len(p.to_dict()), 'paths loaded')\"",
|
73
|
+
"run_tests": "uv run pytest",
|
74
|
+
"check_coverage": "uv run pytest --cov=src --cov-report=term-missing:skip-covered",
|
75
|
+
"regenerate_static": "uv run python -c \"from project_paths import write_dataclass_file; write_dataclass_file()\"",
|
76
|
+
"verify_static_sync": "just check-regen"
|
77
|
+
},
|
78
|
+
"known_issues": {
|
79
|
+
"resolved": [
|
80
|
+
"main.py direct constructor call - FIXED 2025-10-10",
|
81
|
+
"Static model base_dir hardcoding - FIXED 2025-10-10",
|
82
|
+
"SandboxPathValidator not exported - FIXED 2025-10-10"
|
83
|
+
],
|
84
|
+
"active": []
|
85
|
+
},
|
86
|
+
"architecture_notes": {
|
87
|
+
"pattern": "Dynamic Pydantic model creation via create_model()",
|
88
|
+
"validation": "Protocol-based with ValidationResult/Finding pattern",
|
89
|
+
"key_files": [
|
90
|
+
"src/project_paths/model.py - Factory methods and _ProjectPathsBase",
|
91
|
+
"src/project_paths/builder.py - Config loading and field definitions",
|
92
|
+
"src/project_paths/validation.py - Validation framework",
|
93
|
+
"src/project_paths/builtin_validators/strict.py - StrictPathValidator",
|
94
|
+
"src/project_paths/builtin_validators/sandbox.py - SandboxPathValidator",
|
95
|
+
"src/project_paths/get_paths.py - Static model generation"
|
96
|
+
]
|
97
|
+
},
|
98
|
+
"policy_compliance": {
|
99
|
+
"code_quality_standard": "CODE_QUALITY.json (SOLID, KISS, YAGNI)",
|
100
|
+
"reasoning_framework": "CHAIN_OF_THOUGHT_GOLDEN_ALL_IN_ONE.json",
|
101
|
+
"violations": 0,
|
102
|
+
"last_audit": "2025-10-10"
|
103
|
+
},
|
104
|
+
"documentation": {
|
105
|
+
"user_guide": "README.md",
|
106
|
+
"dev_guide": "CLAUDE.md",
|
107
|
+
"refactor_plan": "REFACTOR_PLAN_1.md",
|
108
|
+
"refactor_status": "REFACTOR_STATUS.md",
|
109
|
+
"context": "assistant_context.md",
|
110
|
+
"bundled_in_package": true,
|
111
|
+
"access_functions": {
|
112
|
+
"get_ai_guidelines": "Returns assistant_context.md content (AI usage patterns)",
|
113
|
+
"get_developer_guide": "Returns CLAUDE.md content (architecture & dev setup)",
|
114
|
+
"get_metadata": "Returns this file as JSON (machine-readable metadata)"
|
115
|
+
},
|
116
|
+
"package_location": "src/project_paths/docs/",
|
117
|
+
"offline_accessible": true
|
118
|
+
}
|
119
|
+
}
|
@@ -0,0 +1,104 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
import os
|
3
|
+
from pathlib import Path
|
4
|
+
from typing import Optional, Union
|
5
|
+
|
6
|
+
from .model import ProjectPaths
|
7
|
+
|
8
|
+
|
9
|
+
def generate_static_model_text() -> str:
|
10
|
+
"""
|
11
|
+
Generates the Python code for the static dataclass model.
|
12
|
+
"""
|
13
|
+
lines = [
|
14
|
+
"from pathlib import Path",
|
15
|
+
"from dataclasses import dataclass, field",
|
16
|
+
"",
|
17
|
+
"# This file is auto-generated by ptool-serena. Do not edit manually.",
|
18
|
+
"# Run `ptool gen-static` or `just regen` to regenerate.",
|
19
|
+
"",
|
20
|
+
"@dataclass(frozen=True)",
|
21
|
+
"class ProjectPathsStatic:",
|
22
|
+
' """A static, auto-generated dataclass for project paths."""',
|
23
|
+
"",
|
24
|
+
]
|
25
|
+
|
26
|
+
# Instantiate from pyproject.toml to get the default fields
|
27
|
+
model = ProjectPaths.from_pyproject()
|
28
|
+
|
29
|
+
# Use Pydantic's model_fields to get the defined fields and their defaults
|
30
|
+
for field_name, field_info in model.model_fields.items():
|
31
|
+
# The default_factory is a lambda, we need to call it to get the path
|
32
|
+
if field_info.default_factory:
|
33
|
+
default_path = field_info.default_factory() # type: ignore[call-arg,misc]
|
34
|
+
|
35
|
+
# Special case: base_dir should use Path.cwd() for portability
|
36
|
+
if field_name == "base_dir":
|
37
|
+
line = f" {field_name}: Path = field(default_factory=Path.cwd)"
|
38
|
+
# Special case for Path.home() to use the factory
|
39
|
+
elif default_path == Path.home():
|
40
|
+
line = f" {field_name}: Path = field(default_factory=Path.home)"
|
41
|
+
else:
|
42
|
+
# We want to store the path as a string relative to the base_dir for portability
|
43
|
+
# Determine if the path is truly inside the base directory.
|
44
|
+
# Using os.path.relpath can be tricky across OSes and for non-subpaths.
|
45
|
+
# A more reliable check is to see if base_dir is one of the parents of the path.
|
46
|
+
base_dir = getattr(model, "base_dir", Path.cwd())
|
47
|
+
is_subpath = base_dir.resolve() in default_path.resolve().parents
|
48
|
+
|
49
|
+
if is_subpath:
|
50
|
+
# For paths inside the project, make them relative for portability.
|
51
|
+
rel_path = os.path.relpath(
|
52
|
+
default_path.resolve(), base_dir.resolve()
|
53
|
+
)
|
54
|
+
rel_path_posix = str(Path(rel_path).as_posix())
|
55
|
+
line = f' {field_name}: Path = field(default_factory=lambda: Path.cwd() / "{rel_path_posix}")'
|
56
|
+
else:
|
57
|
+
# For external absolute paths, store them directly.
|
58
|
+
line = f' {field_name}: Path = field(default=Path("{default_path}"))'
|
59
|
+
|
60
|
+
else:
|
61
|
+
# Handle fields with a simple default value
|
62
|
+
line = (
|
63
|
+
f' {field_name}: Path = field(default=Path("{field_info.default}"))'
|
64
|
+
)
|
65
|
+
|
66
|
+
lines.append(line)
|
67
|
+
|
68
|
+
return "\n".join(lines)
|
69
|
+
|
70
|
+
|
71
|
+
def write_dataclass_file(
|
72
|
+
output_path: Optional[Union[str, Path]] = None,
|
73
|
+
class_name: str = "ProjectPathsStatic", # class_name is not used yet, but kept for future
|
74
|
+
) -> None:
|
75
|
+
"""
|
76
|
+
Generates and writes the static dataclass file.
|
77
|
+
"""
|
78
|
+
if output_path is None:
|
79
|
+
# Default path within the src directory
|
80
|
+
output_path = Path(__file__).parent / "project_paths_static.py"
|
81
|
+
|
82
|
+
resolved_path = Path(output_path).resolve()
|
83
|
+
resolved_path.parent.mkdir(parents=True, exist_ok=True)
|
84
|
+
|
85
|
+
# Generate the code
|
86
|
+
generated_code = generate_static_model_text()
|
87
|
+
|
88
|
+
# Write to a temporary file first to avoid race conditions (TOCTOU)
|
89
|
+
temp_path = resolved_path.with_suffix(".tmp")
|
90
|
+
|
91
|
+
# Write and sync to disk before replace (prevents TOCTOU vulnerability)
|
92
|
+
with temp_path.open("w", encoding="utf-8") as f:
|
93
|
+
f.write(generated_code)
|
94
|
+
f.flush()
|
95
|
+
os.fsync(f.fileno()) # Ensure data is written to disk
|
96
|
+
|
97
|
+
# Atomically replace the old file with the new one (atomic on POSIX)
|
98
|
+
temp_path.replace(resolved_path)
|
99
|
+
|
100
|
+
# Verify the file exists (after atomic replace, no TOCTOU risk)
|
101
|
+
if resolved_path.exists():
|
102
|
+
print(f"✅ Static model written to {resolved_path}")
|
103
|
+
else:
|
104
|
+
raise FileNotFoundError(f"❌ Failed to write static model to {resolved_path}")
|
project_paths/main.py
ADDED
project_paths/model.py
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
from pathlib import Path
|
3
|
+
from typing import Union, Any
|
4
|
+
|
5
|
+
from pydantic import BaseModel, ConfigDict, create_model
|
6
|
+
|
7
|
+
from .builder import (
|
8
|
+
build_field_definitions,
|
9
|
+
get_paths_from_dot_paths,
|
10
|
+
get_paths_from_pyproject,
|
11
|
+
)
|
12
|
+
|
13
|
+
|
14
|
+
class _ProjectPathsBase(BaseModel):
|
15
|
+
"""
|
16
|
+
Internal base class for ProjectPaths.
|
17
|
+
This class has a functional __init__ and provides the core logic.
|
18
|
+
It should not be used directly by end-users.
|
19
|
+
"""
|
20
|
+
|
21
|
+
model_config = ConfigDict(validate_assignment=True, arbitrary_types_allowed=True)
|
22
|
+
|
23
|
+
def to_dict(self) -> dict[str, Path]:
|
24
|
+
"""Returns a dictionary of all resolved path attributes."""
|
25
|
+
return self.model_dump(include=set(self.model_fields.keys())) # type: ignore[no-any-return]
|
26
|
+
|
27
|
+
def get_paths(self) -> list[Path]:
|
28
|
+
"""Returns a list of all resolved Path objects."""
|
29
|
+
return [v for v in self.to_dict().values() if isinstance(v, Path)]
|
30
|
+
|
31
|
+
def __getitem__(self, key: str) -> Path:
|
32
|
+
"""Enables dictionary-style access to path attributes."""
|
33
|
+
if key not in self.model_fields:
|
34
|
+
raise KeyError(f"'{key}' is not a configured path.")
|
35
|
+
return getattr(self, key) # type: ignore[no-any-return]
|
36
|
+
|
37
|
+
|
38
|
+
class ProjectPaths:
|
39
|
+
"""
|
40
|
+
Main path management class.
|
41
|
+
|
42
|
+
Do not instantiate this class directly. Use the factory methods:
|
43
|
+
- `ProjectPaths.from_pyproject()`
|
44
|
+
- `ProjectPaths.from_config("path/to/.paths")`
|
45
|
+
"""
|
46
|
+
|
47
|
+
def __init__(self, **kwargs: Any):
|
48
|
+
raise NotImplementedError(
|
49
|
+
"Direct instantiation of ProjectPaths is not supported. "
|
50
|
+
"Use a factory method: `from_pyproject()` or `from_config()`."
|
51
|
+
)
|
52
|
+
|
53
|
+
@classmethod
|
54
|
+
def from_pyproject(cls) -> _ProjectPathsBase:
|
55
|
+
"""Creates a ProjectPaths instance from pyproject.toml."""
|
56
|
+
field_defs = build_field_definitions(loader_func=get_paths_from_pyproject)
|
57
|
+
DynamicModel = create_model( # type: ignore[call-overload]
|
58
|
+
"ProjectPathsDynamic",
|
59
|
+
__base__=(_ProjectPathsBase,), # Config inherited from base class
|
60
|
+
**field_defs,
|
61
|
+
)
|
62
|
+
return DynamicModel() # type: ignore[no-any-return]
|
63
|
+
|
64
|
+
@classmethod
|
65
|
+
def from_config(cls, config_path: Union[str, Path]) -> _ProjectPathsBase:
|
66
|
+
"""Creates a ProjectPaths instance from a custom .paths file."""
|
67
|
+
resolved_path = Path(config_path)
|
68
|
+
field_defs = build_field_definitions(
|
69
|
+
loader_func=get_paths_from_dot_paths, config_path=resolved_path
|
70
|
+
)
|
71
|
+
DynamicModel = create_model( # type: ignore[call-overload]
|
72
|
+
"ProjectPathsDynamic",
|
73
|
+
__base__=(_ProjectPathsBase,), # Config inherited from base class
|
74
|
+
**field_defs,
|
75
|
+
)
|
76
|
+
return DynamicModel() # type: ignore[no-any-return]
|