Skip to content

Commit e3ae77a

Browse files
committed
feat(parser): Add SemanticParseResult and SemanticContext
- SemanticParseResult uses semantic types instead of strings - SemanticContext provides type-safe parser state - Include conversion methods between regular and semantic versions - Full interoperability with existing parser system
1 parent 8065d37 commit e3ae77a

2 files changed

Lines changed: 263 additions & 0 deletions

File tree

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
"""Semantic context using semantic types for type safety.
2+
3+
This module provides SemanticContext, which is like Context but uses
4+
semantic types instead of plain strings for enhanced type safety.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from dataclasses import dataclass, field
10+
from typing import Optional
11+
12+
from cli_patterns.core.parser_types import (
13+
CommandId,
14+
CommandList,
15+
ContextKey,
16+
ContextState,
17+
ParseMode,
18+
make_command_id,
19+
make_context_key,
20+
make_parse_mode,
21+
)
22+
from cli_patterns.ui.parser.types import Context
23+
24+
25+
@dataclass
26+
class SemanticContext:
27+
"""Parsing context containing session state and history using semantic types.
28+
29+
This is the semantic type equivalent of Context, providing type safety
30+
for parsing context operations while maintaining the same structure.
31+
32+
Attributes:
33+
mode: Current parsing mode (semantic type)
34+
history: Command history list (semantic types)
35+
session_state: Dictionary of session state data (semantic types)
36+
current_directory: Current working directory (optional)
37+
"""
38+
39+
mode: ParseMode = field(default_factory=lambda: make_parse_mode("text"))
40+
history: CommandList = field(default_factory=list)
41+
session_state: ContextState = field(default_factory=dict)
42+
current_directory: Optional[str] = None
43+
44+
@classmethod
45+
def from_context(cls, context: Context) -> SemanticContext:
46+
"""Create a SemanticContext from a regular Context.
47+
48+
Args:
49+
context: Regular Context to convert
50+
51+
Returns:
52+
SemanticContext with semantic types
53+
"""
54+
return cls(
55+
mode=make_parse_mode(context.mode),
56+
history=[make_command_id(cmd) for cmd in context.history],
57+
session_state={
58+
make_context_key(key): value
59+
for key, value in context.session_state.items()
60+
if isinstance(
61+
value, str
62+
) # Only convert string values to maintain type safety
63+
},
64+
current_directory=context.current_directory,
65+
)
66+
67+
def to_context(self) -> Context:
68+
"""Convert this SemanticContext to a regular Context.
69+
70+
Returns:
71+
Regular Context with string types
72+
"""
73+
return Context(
74+
mode=str(self.mode),
75+
history=[str(cmd) for cmd in self.history],
76+
session_state={
77+
str(key): value for key, value in self.session_state.items()
78+
},
79+
current_directory=self.current_directory,
80+
)
81+
82+
def add_to_history(self, command: CommandId) -> None:
83+
"""Add command to history.
84+
85+
Args:
86+
command: Semantic command to add to history
87+
"""
88+
self.history.append(command)
89+
90+
def get_recent_commands(self, count: int) -> CommandList:
91+
"""Get the most recent commands from history.
92+
93+
Args:
94+
count: Number of recent commands to retrieve
95+
96+
Returns:
97+
List of recent commands (semantic types)
98+
"""
99+
if count <= 0:
100+
return []
101+
return self.history[-count:]
102+
103+
def get_state(
104+
self, key: ContextKey, default: Optional[str] = None
105+
) -> Optional[str]:
106+
"""Get session state value by key.
107+
108+
Args:
109+
key: Semantic state key to retrieve
110+
default: Default value if key doesn't exist
111+
112+
Returns:
113+
State value or default
114+
"""
115+
return self.session_state.get(key, default)
116+
117+
def set_state(self, key: ContextKey, value: Optional[str]) -> None:
118+
"""Set session state value.
119+
120+
Args:
121+
key: Semantic state key to set
122+
value: Value to set (None to remove key)
123+
"""
124+
if value is None:
125+
self.session_state.pop(key, None)
126+
else:
127+
self.session_state[key] = value
128+
129+
def has_state(self, key: ContextKey) -> bool:
130+
"""Check if a state key exists.
131+
132+
Args:
133+
key: Semantic state key to check
134+
135+
Returns:
136+
True if key exists, False otherwise
137+
"""
138+
return key in self.session_state
139+
140+
def clear_history(self) -> None:
141+
"""Clear command history."""
142+
self.history.clear()
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
"""Semantic parse result using semantic types for type safety.
2+
3+
This module provides SemanticParseResult, which is like ParseResult but uses
4+
semantic types instead of plain strings for enhanced type safety.
5+
"""
6+
7+
from __future__ import annotations
8+
9+
from dataclasses import dataclass, field
10+
from typing import Optional
11+
12+
from cli_patterns.core.parser_types import (
13+
ArgumentList,
14+
ArgumentValue,
15+
CommandId,
16+
FlagName,
17+
FlagSet,
18+
OptionDict,
19+
OptionKey,
20+
make_argument_value,
21+
make_command_id,
22+
make_flag_name,
23+
make_option_key,
24+
)
25+
from cli_patterns.ui.parser.types import ParseResult
26+
27+
28+
@dataclass
29+
class SemanticParseResult:
30+
"""Result of parsing user input into structured command data using semantic types.
31+
32+
This is the semantic type equivalent of ParseResult, providing type safety
33+
for command parsing operations while maintaining the same structure.
34+
35+
Attributes:
36+
command: The main command parsed from input (semantic type)
37+
args: List of positional arguments (semantic types)
38+
flags: Set of single-letter flags (semantic types)
39+
options: Dictionary of key-value options (semantic types)
40+
raw_input: Original input string
41+
shell_command: Shell command for shell parsers (optional)
42+
"""
43+
44+
command: CommandId
45+
args: ArgumentList = field(default_factory=list)
46+
flags: FlagSet = field(default_factory=set)
47+
options: OptionDict = field(default_factory=dict)
48+
raw_input: str = ""
49+
shell_command: Optional[str] = None
50+
51+
@classmethod
52+
def from_parse_result(cls, result: ParseResult) -> SemanticParseResult:
53+
"""Create a SemanticParseResult from a regular ParseResult.
54+
55+
Args:
56+
result: Regular ParseResult to convert
57+
58+
Returns:
59+
SemanticParseResult with semantic types
60+
"""
61+
return cls(
62+
command=make_command_id(result.command),
63+
args=[make_argument_value(arg) for arg in result.args],
64+
flags={make_flag_name(flag) for flag in result.flags},
65+
options={
66+
make_option_key(key): make_argument_value(value)
67+
for key, value in result.options.items()
68+
},
69+
raw_input=result.raw_input,
70+
shell_command=result.shell_command,
71+
)
72+
73+
def to_parse_result(self) -> ParseResult:
74+
"""Convert this SemanticParseResult to a regular ParseResult.
75+
76+
Returns:
77+
Regular ParseResult with string types
78+
"""
79+
return ParseResult(
80+
command=str(self.command),
81+
args=[str(arg) for arg in self.args],
82+
flags={str(flag) for flag in self.flags},
83+
options={str(key): str(value) for key, value in self.options.items()},
84+
raw_input=self.raw_input,
85+
shell_command=self.shell_command,
86+
)
87+
88+
def has_flag(self, flag: FlagName) -> bool:
89+
"""Check if a flag is present.
90+
91+
Args:
92+
flag: Semantic flag name to check
93+
94+
Returns:
95+
True if flag is present, False otherwise
96+
"""
97+
return flag in self.flags
98+
99+
def get_option(self, key: OptionKey) -> Optional[ArgumentValue]:
100+
"""Get option value by key safely.
101+
102+
Args:
103+
key: Semantic option key to retrieve
104+
105+
Returns:
106+
Option value or None if key doesn't exist
107+
"""
108+
return self.options.get(key)
109+
110+
def get_arg(self, index: int) -> Optional[ArgumentValue]:
111+
"""Get positional argument by index safely.
112+
113+
Args:
114+
index: Position index to retrieve
115+
116+
Returns:
117+
Positional argument at index or None if index is out of range
118+
"""
119+
if 0 <= index < len(self.args):
120+
return self.args[index]
121+
return None

0 commit comments

Comments
 (0)