Skip to content

Commit 89cfa10

Browse files
dugshubclaude
andcommitted
feat: Address PR feedback for semantic types implementation
- Add comprehensive documentation guide (docs/semantic-types.md) covering usage, migration, best practices - Clean up type shadowing in SemanticParseError by using semantic_suggestions attribute - Add optional validation to factory functions with validate parameter (default False for zero overhead) - Implement TypeGuard functions for runtime type checking (is_command_id, is_option_key, etc.) - Update tests to use new semantic_suggestions attribute All tests passing (495 tests), MyPy strict mode clean, ruff checks pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 5148627 commit 89cfa10

File tree

5 files changed

+482
-19
lines changed

5 files changed

+482
-19
lines changed

docs/semantic-types.md

Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
# Semantic Types Guide
2+
3+
## Overview
4+
5+
CLI Patterns uses semantic types to provide compile-time type safety and prevent type confusion in the parser system. These types use Python's `NewType` feature to create distinct types at compile time with zero runtime overhead.
6+
7+
## Core Semantic Types
8+
9+
### Command and Parser Types
10+
- `CommandId`: Unique identifier for commands (e.g., "help", "run", "test")
11+
- `OptionKey`: Command option keys (e.g., "--verbose", "--output")
12+
- `FlagName`: Boolean flag names (e.g., "--debug", "--quiet")
13+
- `ArgumentValue`: String argument values
14+
- `ParseMode`: Parser mode identifiers (e.g., "text", "shell", "semantic")
15+
- `ContextKey`: Context variable keys for parser state
16+
17+
## When to Use Semantic Types
18+
19+
### Use Semantic Types When:
20+
- Defining command identifiers in registries
21+
- Passing option keys between parser components
22+
- Storing parser context values
23+
- Building parse results with typed fields
24+
- Creating command suggestions and error messages
25+
26+
### Use Regular Strings When:
27+
- Displaying user-facing messages
28+
- Working with raw input before parsing
29+
- Interfacing with external libraries
30+
- Performance-critical hot paths (though overhead is minimal)
31+
32+
## Migration Guide
33+
34+
### Converting Existing Code
35+
36+
#### Before (using plain strings):
37+
```python
38+
def register_command(name: str, handler: Callable) -> None:
39+
commands[name] = handler
40+
41+
def parse_command(input: str) -> tuple[str, dict[str, str]]:
42+
command = input.split()[0]
43+
options = parse_options(input)
44+
return command, options
45+
```
46+
47+
#### After (using semantic types):
48+
```python
49+
from cli_patterns.core.parser_types import CommandId, OptionKey, make_command_id, make_option_key
50+
51+
def register_command(name: CommandId, handler: Callable) -> None:
52+
commands[name] = handler
53+
54+
def parse_command(input: str) -> tuple[CommandId, dict[OptionKey, str]]:
55+
command = make_command_id(input.split()[0])
56+
options = {make_option_key(k): v for k, v in parse_options(input).items()}
57+
return command, options
58+
```
59+
60+
### Conversion Methods
61+
62+
The semantic parser components provide bidirectional conversion:
63+
64+
```python
65+
from cli_patterns.ui.parser.semantic_result import SemanticParseResult
66+
from cli_patterns.ui.parser.types import ParseResult
67+
68+
# Convert from regular to semantic
69+
regular_result = ParseResult(command="help", options={"verbose": "true"})
70+
semantic_result = SemanticParseResult.from_parse_result(regular_result)
71+
72+
# Convert from semantic to regular
73+
regular_again = semantic_result.to_parse_result()
74+
```
75+
76+
## Factory Functions
77+
78+
Use factory functions to create semantic types:
79+
80+
```python
81+
from cli_patterns.core.parser_types import (
82+
make_command_id,
83+
make_option_key,
84+
make_flag_name,
85+
make_argument_value,
86+
make_parse_mode,
87+
make_context_key
88+
)
89+
90+
# Creating semantic types
91+
cmd = make_command_id("help")
92+
opt = make_option_key("--verbose")
93+
flag = make_flag_name("--debug")
94+
arg = make_argument_value("output.txt")
95+
mode = make_parse_mode("semantic")
96+
ctx_key = make_context_key("current_command")
97+
```
98+
99+
## Type Aliases for Collections
100+
101+
Use provided type aliases for better readability:
102+
103+
```python
104+
from cli_patterns.core.parser_types import CommandList, OptionDict, FlagSet
105+
106+
# Type-safe collections
107+
commands: CommandList = [make_command_id("help"), make_command_id("run")]
108+
options: OptionDict = {make_option_key("--output"): "file.txt"}
109+
flags: FlagSet = {make_flag_name("--verbose"), make_flag_name("--debug")}
110+
```
111+
112+
## Working with SemanticParseResult
113+
114+
The `SemanticParseResult` class provides a type-safe parse result:
115+
116+
```python
117+
from cli_patterns.ui.parser.semantic_result import SemanticParseResult
118+
from cli_patterns.core.parser_types import make_command_id, make_option_key
119+
120+
# Creating a semantic parse result
121+
result = SemanticParseResult(
122+
command=make_command_id("test"),
123+
options={make_option_key("--coverage"): "true"},
124+
arguments=["test_file.py"],
125+
mode=make_parse_mode("text")
126+
)
127+
128+
# Accessing typed fields
129+
cmd: CommandId = result.command
130+
opts: dict[OptionKey, str] = result.options
131+
```
132+
133+
## Error Handling with Semantic Types
134+
135+
The `SemanticParseError` provides rich error context:
136+
137+
```python
138+
from cli_patterns.ui.parser.semantic_errors import SemanticParseError
139+
from cli_patterns.core.parser_types import make_command_id, make_option_key
140+
141+
# Creating semantic errors
142+
error = SemanticParseError(
143+
message="Unknown option",
144+
command=make_command_id("test"),
145+
invalid_option=make_option_key("--unknown"),
146+
valid_options=[make_option_key("--verbose"), make_option_key("--output")],
147+
suggestions=[make_command_id("test")]
148+
)
149+
150+
# Accessing semantic fields
151+
cmd: CommandId = error.command
152+
invalid: OptionKey = error.invalid_option
153+
valid: list[OptionKey] = error.valid_options
154+
```
155+
156+
## Best Practices
157+
158+
### 1. Use Factory Functions
159+
Always use factory functions rather than direct type casting:
160+
```python
161+
# Good
162+
cmd = make_command_id("help")
163+
164+
# Avoid
165+
cmd = CommandId("help") # Works but less clear
166+
```
167+
168+
### 2. Maintain Type Consistency
169+
Keep semantic types throughout your parser pipeline:
170+
```python
171+
def process_command(cmd: CommandId) -> CommandId:
172+
# Process and return same type
173+
return cmd
174+
175+
# Don't mix types unnecessarily
176+
def process_command(cmd: CommandId) -> str: # Avoid unless needed
177+
return str(cmd)
178+
```
179+
180+
### 3. Use Type Aliases
181+
Leverage type aliases for complex types:
182+
```python
183+
from cli_patterns.core.parser_types import CommandList, OptionDict
184+
185+
def get_commands() -> CommandList:
186+
return [make_command_id("help"), make_command_id("run")]
187+
188+
def get_options() -> OptionDict:
189+
return {make_option_key("--verbose"): "true"}
190+
```
191+
192+
### 4. Document Type Conversions
193+
When converting between semantic and regular types, document why:
194+
```python
195+
# Convert to string for display to user
196+
display_name = str(command_id)
197+
198+
# Convert from user input to semantic type
199+
command_id = make_command_id(user_input.strip())
200+
```
201+
202+
## Extending the Type System
203+
204+
To add new semantic types:
205+
206+
1. Define the type in `core/parser_types.py`:
207+
```python
208+
from typing import NewType
209+
210+
# Define new semantic type
211+
ConfigKey = NewType('ConfigKey', str)
212+
213+
# Add factory function
214+
def make_config_key(value: str) -> ConfigKey:
215+
return ConfigKey(value)
216+
217+
# Add type alias if needed
218+
ConfigDict = dict[ConfigKey, str]
219+
```
220+
221+
2. Add conversion support if needed:
222+
```python
223+
class SemanticConfig:
224+
def __init__(self, config: dict[ConfigKey, str]):
225+
self.config = config
226+
227+
@classmethod
228+
def from_dict(cls, config: dict[str, str]) -> 'SemanticConfig':
229+
return cls({make_config_key(k): v for k, v in config.items()})
230+
231+
def to_dict(self) -> dict[str, str]:
232+
return {str(k): v for k, v in self.config.items()}
233+
```
234+
235+
## Performance Considerations
236+
237+
Semantic types have **zero runtime overhead** because:
238+
- `NewType` creates aliases at compile time only
239+
- No runtime type checking or validation
240+
- Type information is erased after compilation
241+
- Factory functions are simple identity functions
242+
243+
## IDE Support
244+
245+
Semantic types improve IDE experience:
246+
- Autocomplete shows only valid operations for each type
247+
- Type checking catches mixing of incompatible types
248+
- Better documentation through meaningful type names
249+
- Refactoring tools understand type relationships
250+
251+
## Troubleshooting
252+
253+
### Common Issues
254+
255+
1. **Type Mismatch Errors**
256+
```python
257+
# Error: Cannot assign str to CommandId
258+
cmd: CommandId = "help" #
259+
260+
# Fix: Use factory function
261+
cmd: CommandId = make_command_id("help") #
262+
```
263+
264+
2. **Missing Conversions**
265+
```python
266+
# Error: dict[str, str] not compatible with OptionDict
267+
options: OptionDict = {"--verbose": "true"} #
268+
269+
# Fix: Convert keys to semantic types
270+
options: OptionDict = {make_option_key("--verbose"): "true"} #
271+
```
272+
273+
3. **JSON Serialization**
274+
```python
275+
import json
276+
from cli_patterns.core.parser_types import CommandId, make_command_id
277+
278+
cmd = make_command_id("help")
279+
280+
# Semantic types serialize as strings
281+
json_str = json.dumps({"command": cmd}) # Works fine
282+
283+
# Deserialize needs conversion
284+
data = json.loads(json_str)
285+
cmd = make_command_id(data["command"]) # Convert back to semantic type
286+
```
287+
288+
## Further Reading
289+
290+
- [Python NewType Documentation](https://docs.python.org/3/library/typing.html#newtype)
291+
- [MyPy Documentation on NewType](https://mypy.readthedocs.io/en/stable/more_types.html#newtypes)
292+
- [CLI Patterns Architecture Guide](../CLAUDE.md)

0 commit comments

Comments
 (0)