ry-tool 1.0.1__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.
- ry_tool/__init__.py +27 -0
- ry_tool/__main__.py +9 -0
- ry_tool/_cli.py +244 -0
- ry_tool/app.py +420 -0
- ry_tool/context.py +297 -0
- ry_tool/executor.py +475 -0
- ry_tool/installer.py +176 -0
- ry_tool/loader.py +280 -0
- ry_tool/matcher.py +233 -0
- ry_tool/parser.py +248 -0
- ry_tool/template.py +306 -0
- ry_tool/utils.py +396 -0
- ry_tool-1.0.1.dist-info/METADATA +112 -0
- ry_tool-1.0.1.dist-info/RECORD +16 -0
- ry_tool-1.0.1.dist-info/WHEEL +4 -0
- ry_tool-1.0.1.dist-info/entry_points.txt +3 -0
ry_tool/parser.py
ADDED
@@ -0,0 +1,248 @@
|
|
1
|
+
"""
|
2
|
+
Command line parser that understands semantic structure.
|
3
|
+
|
4
|
+
Purpose: Convert raw args like ['commit', '-m', 'msg', '--amend']
|
5
|
+
into structured data with flags, arguments, and commands.
|
6
|
+
No YAML knowledge, just pure argument parsing.
|
7
|
+
"""
|
8
|
+
from dataclasses import dataclass, field
|
9
|
+
from typing import List, Dict, Any, Optional, Tuple
|
10
|
+
|
11
|
+
|
12
|
+
@dataclass
|
13
|
+
class ParsedCommand:
|
14
|
+
"""Structured representation of a parsed command."""
|
15
|
+
command: str
|
16
|
+
subcommand: Optional[str] = None
|
17
|
+
flags: Dict[str, Any] = field(default_factory=dict)
|
18
|
+
positionals: List[str] = field(default_factory=list)
|
19
|
+
remaining: List[str] = field(default_factory=list) # After -- separator
|
20
|
+
raw_args: List[str] = field(default_factory=list) # Original args for reference
|
21
|
+
|
22
|
+
|
23
|
+
class CommandParser:
|
24
|
+
"""
|
25
|
+
Parse command line arguments with flag schema awareness.
|
26
|
+
|
27
|
+
This parser understands:
|
28
|
+
- Flags with values: -m "message", --package backend
|
29
|
+
- Boolean flags: -a, --force
|
30
|
+
- Positional arguments
|
31
|
+
- Double dash separator: --
|
32
|
+
- Subcommands: remote add origin
|
33
|
+
"""
|
34
|
+
|
35
|
+
def parse(self, args: List[str], schema: Dict[str, Any] = None) -> ParsedCommand:
|
36
|
+
"""
|
37
|
+
Parse arguments with optional schema for flag types.
|
38
|
+
|
39
|
+
Args:
|
40
|
+
args: Raw command line arguments
|
41
|
+
schema: Optional schema defining flag types
|
42
|
+
{'m': 'string', 'force': 'bool', 'bump': 'enum'}
|
43
|
+
|
44
|
+
Returns:
|
45
|
+
ParsedCommand with structured data
|
46
|
+
"""
|
47
|
+
if not args:
|
48
|
+
return ParsedCommand(command="", raw_args=args)
|
49
|
+
|
50
|
+
schema = schema or {}
|
51
|
+
result = ParsedCommand(command=args[0], raw_args=args.copy())
|
52
|
+
|
53
|
+
i = 1
|
54
|
+
while i < len(args):
|
55
|
+
arg = args[i]
|
56
|
+
|
57
|
+
# Double dash separator
|
58
|
+
if arg == '--':
|
59
|
+
result.remaining = args[i+1:]
|
60
|
+
break
|
61
|
+
|
62
|
+
# Flag (short or long)
|
63
|
+
if arg.startswith('-'):
|
64
|
+
flag_name, flag_value, consumed = self._parse_flag(args, i, schema)
|
65
|
+
if flag_name:
|
66
|
+
result.flags[flag_name] = flag_value
|
67
|
+
i += consumed
|
68
|
+
else:
|
69
|
+
# Positional argument
|
70
|
+
result.positionals.append(arg)
|
71
|
+
i += 1
|
72
|
+
|
73
|
+
# Check if first positional might be a subcommand
|
74
|
+
if result.positionals and not result.subcommand:
|
75
|
+
# This is a simple heuristic - could be enhanced
|
76
|
+
if result.command in ['git', 'docker', 'kubectl']:
|
77
|
+
result.subcommand = result.positionals.pop(0)
|
78
|
+
|
79
|
+
return result
|
80
|
+
|
81
|
+
def _parse_flag(self, args: List[str], index: int, schema: Dict) -> Tuple[str, Any, int]:
|
82
|
+
"""
|
83
|
+
Parse a flag and its value if applicable.
|
84
|
+
|
85
|
+
Returns:
|
86
|
+
(flag_name, flag_value, args_consumed)
|
87
|
+
"""
|
88
|
+
arg = args[index]
|
89
|
+
|
90
|
+
# Long flag with = (--message="hello")
|
91
|
+
if '--' in arg and '=' in arg:
|
92
|
+
flag_part, value = arg.split('=', 1)
|
93
|
+
flag_name = flag_part.lstrip('-')
|
94
|
+
return flag_name, value, 1
|
95
|
+
|
96
|
+
# Regular flag
|
97
|
+
flag_name = arg.lstrip('-')
|
98
|
+
|
99
|
+
# Check schema for flag type
|
100
|
+
flag_type = self._get_flag_type(flag_name, schema)
|
101
|
+
|
102
|
+
if flag_type == 'bool':
|
103
|
+
return flag_name, True, 1
|
104
|
+
|
105
|
+
# Flag takes a value - consume next arg
|
106
|
+
if index + 1 < len(args) and not args[index + 1].startswith('-'):
|
107
|
+
return flag_name, args[index + 1], 2
|
108
|
+
|
109
|
+
# Flag without value (treat as bool)
|
110
|
+
return flag_name, True, 1
|
111
|
+
|
112
|
+
def _get_flag_type(self, flag_name: str, schema: Dict) -> str:
|
113
|
+
"""
|
114
|
+
Determine flag type from schema.
|
115
|
+
|
116
|
+
Handles aliases like m/message.
|
117
|
+
"""
|
118
|
+
if not schema:
|
119
|
+
return 'string' # Default assumption
|
120
|
+
|
121
|
+
# Direct match
|
122
|
+
if flag_name in schema:
|
123
|
+
flag_def = schema[flag_name]
|
124
|
+
if isinstance(flag_def, str):
|
125
|
+
return flag_def
|
126
|
+
elif isinstance(flag_def, dict):
|
127
|
+
return flag_def.get('type', 'string')
|
128
|
+
|
129
|
+
# Check aliases (m/message format)
|
130
|
+
for key, value in schema.items():
|
131
|
+
if '/' in key:
|
132
|
+
short, long = key.split('/', 1)
|
133
|
+
if flag_name in [short, long]:
|
134
|
+
if isinstance(value, str):
|
135
|
+
return value
|
136
|
+
elif isinstance(value, dict):
|
137
|
+
return value.get('type', 'string')
|
138
|
+
|
139
|
+
return 'string' # Default
|
140
|
+
|
141
|
+
def parse_with_command_schema(self, args: List[str], command_schema: Dict) -> ParsedCommand:
|
142
|
+
"""
|
143
|
+
Parse with knowledge of command structure.
|
144
|
+
|
145
|
+
Args:
|
146
|
+
args: Raw arguments
|
147
|
+
command_schema: Full command schema with flags and arguments definitions
|
148
|
+
|
149
|
+
Returns:
|
150
|
+
ParsedCommand with proper typing based on schema
|
151
|
+
"""
|
152
|
+
# Extract flag schema from command schema
|
153
|
+
flag_schema = {}
|
154
|
+
if 'flags' in command_schema:
|
155
|
+
for flag_key, flag_def in command_schema['flags'].items():
|
156
|
+
if '/' in flag_key:
|
157
|
+
# Handle m/message style
|
158
|
+
short, long = flag_key.split('/', 1)
|
159
|
+
flag_schema[short] = flag_def
|
160
|
+
flag_schema[long] = flag_def
|
161
|
+
else:
|
162
|
+
flag_schema[flag_key] = flag_def
|
163
|
+
|
164
|
+
return self.parse(args, flag_schema)
|
165
|
+
|
166
|
+
def generate_help(self, library_config, command: str = None) -> str:
|
167
|
+
"""
|
168
|
+
Generate help text for library or specific command.
|
169
|
+
|
170
|
+
Args:
|
171
|
+
library_config: LibraryConfig object with library metadata
|
172
|
+
command: Optional specific command to show help for
|
173
|
+
|
174
|
+
Returns:
|
175
|
+
Formatted help text
|
176
|
+
"""
|
177
|
+
help_lines = []
|
178
|
+
|
179
|
+
if command and command in library_config.commands:
|
180
|
+
# Command-specific help
|
181
|
+
cmd = library_config.commands[command]
|
182
|
+
help_lines.append(f"{library_config.name} {command}")
|
183
|
+
|
184
|
+
if 'description' in cmd:
|
185
|
+
help_lines.append(f" {cmd['description']}")
|
186
|
+
help_lines.append("")
|
187
|
+
|
188
|
+
# Arguments
|
189
|
+
if 'arguments' in cmd:
|
190
|
+
help_lines.append("Arguments:")
|
191
|
+
for arg_name, arg_config in cmd['arguments'].items():
|
192
|
+
if isinstance(arg_config, str):
|
193
|
+
required = arg_config == 'required'
|
194
|
+
else:
|
195
|
+
required = arg_config.get('required', False)
|
196
|
+
desc = arg_config.get('description', '') if isinstance(arg_config, dict) else ''
|
197
|
+
status = 'required' if required else 'optional'
|
198
|
+
help_lines.append(f" {arg_name:<15} {status:<10} {desc}")
|
199
|
+
help_lines.append("")
|
200
|
+
|
201
|
+
# Flags
|
202
|
+
if 'flags' in cmd:
|
203
|
+
help_lines.append("Flags:")
|
204
|
+
for flag_name, flag_config in cmd['flags'].items():
|
205
|
+
# Handle alias format (m/message)
|
206
|
+
if '/' in flag_name:
|
207
|
+
short, long = flag_name.split('/', 1)
|
208
|
+
flag_display = f"-{short}, --{long}"
|
209
|
+
else:
|
210
|
+
flag_display = f"--{flag_name}"
|
211
|
+
|
212
|
+
if isinstance(flag_config, str):
|
213
|
+
flag_type = flag_config
|
214
|
+
desc = ''
|
215
|
+
else:
|
216
|
+
flag_type = flag_config.get('type', 'string')
|
217
|
+
desc = flag_config.get('description', '')
|
218
|
+
|
219
|
+
help_lines.append(f" {flag_display:<20} ({flag_type})")
|
220
|
+
if desc:
|
221
|
+
help_lines.append(f" {desc}")
|
222
|
+
help_lines.append("")
|
223
|
+
|
224
|
+
# Examples
|
225
|
+
if 'examples' in cmd:
|
226
|
+
help_lines.append("Examples:")
|
227
|
+
for example in cmd['examples']:
|
228
|
+
help_lines.append(f" {example}")
|
229
|
+
help_lines.append("")
|
230
|
+
else:
|
231
|
+
# Library help - list all commands
|
232
|
+
help_lines.append(f"{library_config.name} - {library_config.description}")
|
233
|
+
help_lines.append(f"Version: {library_config.version}")
|
234
|
+
help_lines.append(f"Type: {library_config.type}")
|
235
|
+
help_lines.append("")
|
236
|
+
|
237
|
+
if library_config.commands:
|
238
|
+
help_lines.append("Commands:")
|
239
|
+
for cmd_name, cmd_config in library_config.commands.items():
|
240
|
+
if cmd_name != '*': # Skip catch-all
|
241
|
+
desc = cmd_config.get('description', 'No description')
|
242
|
+
help_lines.append(f" {cmd_name:<15} {desc}")
|
243
|
+
help_lines.append("")
|
244
|
+
help_lines.append("Use: ry-next <library> <command> --help for command details")
|
245
|
+
|
246
|
+
return '\n'.join(help_lines)
|
247
|
+
|
248
|
+
|
ry_tool/template.py
ADDED
@@ -0,0 +1,306 @@
|
|
1
|
+
"""
|
2
|
+
Template processor for variable substitution.
|
3
|
+
|
4
|
+
Purpose: Replace template variables like {{flags.m}} with actual values.
|
5
|
+
Simple and focused - just template substitution, no execution logic.
|
6
|
+
"""
|
7
|
+
import re
|
8
|
+
from typing import Any, Dict, TypeVar, Callable
|
9
|
+
from .context import ExecutionContext
|
10
|
+
|
11
|
+
# Generic type for recursive processing
|
12
|
+
T = TypeVar('T')
|
13
|
+
|
14
|
+
|
15
|
+
class TemplateProcessor:
|
16
|
+
"""
|
17
|
+
Process templates with variable substitution.
|
18
|
+
|
19
|
+
Supports:
|
20
|
+
- Simple variables: {{flags.m}}
|
21
|
+
- Defaults: {{flags.m|default:"no message"}}
|
22
|
+
- Filters: {{flags.m|upper}}, {{positionals|join:", "}}
|
23
|
+
"""
|
24
|
+
|
25
|
+
def __init__(self, context: ExecutionContext):
|
26
|
+
"""
|
27
|
+
Initialize with execution context.
|
28
|
+
|
29
|
+
Args:
|
30
|
+
context: ExecutionContext with all available variables
|
31
|
+
"""
|
32
|
+
self.context = context
|
33
|
+
self.filters = self._build_filters()
|
34
|
+
|
35
|
+
def process(self, template: str) -> str:
|
36
|
+
"""
|
37
|
+
Process template string, replacing all variables.
|
38
|
+
|
39
|
+
Args:
|
40
|
+
template: String with {{variable}} placeholders
|
41
|
+
|
42
|
+
Returns:
|
43
|
+
Processed string with variables replaced
|
44
|
+
"""
|
45
|
+
if not isinstance(template, str):
|
46
|
+
return str(template)
|
47
|
+
|
48
|
+
# Find all template variables
|
49
|
+
pattern = r'\{\{([^}]+)\}\}'
|
50
|
+
|
51
|
+
def replace_var(match):
|
52
|
+
var_expr = match.group(1).strip()
|
53
|
+
return str(self._evaluate_expression(var_expr))
|
54
|
+
|
55
|
+
return re.sub(pattern, replace_var, template)
|
56
|
+
|
57
|
+
def process_recursive(self, data: T) -> T:
|
58
|
+
"""
|
59
|
+
Recursively process any data structure containing templates.
|
60
|
+
|
61
|
+
Uses type dispatch for clean handling of different data types.
|
62
|
+
|
63
|
+
Args:
|
64
|
+
data: Any data structure potentially containing template strings
|
65
|
+
|
66
|
+
Returns:
|
67
|
+
Data structure with all templates processed
|
68
|
+
"""
|
69
|
+
return self._dispatch_process(data)
|
70
|
+
|
71
|
+
def _dispatch_process(self, data: Any) -> Any:
|
72
|
+
"""Type-based dispatch for processing different data types."""
|
73
|
+
if isinstance(data, str):
|
74
|
+
return self.process(data)
|
75
|
+
elif isinstance(data, dict):
|
76
|
+
return {key: self._dispatch_process(value) for key, value in data.items()}
|
77
|
+
elif isinstance(data, list):
|
78
|
+
return [self._dispatch_process(item) for item in data]
|
79
|
+
elif isinstance(data, tuple):
|
80
|
+
return tuple(self._dispatch_process(item) for item in data)
|
81
|
+
else:
|
82
|
+
return data
|
83
|
+
|
84
|
+
|
85
|
+
def _evaluate_expression(self, expr: str) -> Any:
|
86
|
+
"""
|
87
|
+
Evaluate a template expression.
|
88
|
+
|
89
|
+
Handles:
|
90
|
+
- Simple paths: flags.m
|
91
|
+
- Defaults: flags.m|default:"none"
|
92
|
+
- Filters: flags.m|upper
|
93
|
+
"""
|
94
|
+
# Split by pipe for filters/defaults
|
95
|
+
parts = expr.split('|')
|
96
|
+
base_expr = parts[0].strip()
|
97
|
+
|
98
|
+
# Get base value
|
99
|
+
value = self._get_value(base_expr)
|
100
|
+
|
101
|
+
# Apply filters/defaults
|
102
|
+
for part in parts[1:]:
|
103
|
+
part = part.strip()
|
104
|
+
if part.startswith('default:'):
|
105
|
+
default_val = part[8:].strip().strip('"\'')
|
106
|
+
if value is None or value == "":
|
107
|
+
value = default_val
|
108
|
+
else:
|
109
|
+
# Apply filter
|
110
|
+
value = self._apply_filter(value, part)
|
111
|
+
|
112
|
+
return value
|
113
|
+
|
114
|
+
def _get_value(self, path: str) -> Any:
|
115
|
+
"""
|
116
|
+
Get value from context by path.
|
117
|
+
|
118
|
+
Special variables:
|
119
|
+
- original: Reconstructed original command
|
120
|
+
- relay: Command to relay to native tool
|
121
|
+
- relay_base: Just the native tool path
|
122
|
+
"""
|
123
|
+
# Check for special computed values
|
124
|
+
if path == 'original':
|
125
|
+
return self.context._reconstruct_original()
|
126
|
+
elif path == 'relay':
|
127
|
+
return self.context._build_relay_command()
|
128
|
+
elif path == 'relay_base':
|
129
|
+
return self.context.target or self.context.command
|
130
|
+
|
131
|
+
# Normal path lookup
|
132
|
+
value = self.context.get(path)
|
133
|
+
|
134
|
+
# Convert None to empty string
|
135
|
+
if value is None:
|
136
|
+
return ""
|
137
|
+
|
138
|
+
# Convert booleans to shell-friendly strings
|
139
|
+
if isinstance(value, bool):
|
140
|
+
return "true" if value else ""
|
141
|
+
|
142
|
+
return value
|
143
|
+
|
144
|
+
def _apply_filter(self, value: Any, filter_name: str) -> Any:
|
145
|
+
"""Apply a filter to a value."""
|
146
|
+
if filter_name in self.filters:
|
147
|
+
return self.filters[filter_name](value)
|
148
|
+
return value
|
149
|
+
|
150
|
+
def _build_filters(self) -> Dict[str, Callable[[Any], Any]]:
|
151
|
+
"""Build available filters with proper typing."""
|
152
|
+
return {
|
153
|
+
'upper': self._filter_upper,
|
154
|
+
'lower': self._filter_lower,
|
155
|
+
'strip': self._filter_strip,
|
156
|
+
'join': self._filter_join,
|
157
|
+
'json': self._filter_json,
|
158
|
+
'shell_escape': self._filter_shell_escape,
|
159
|
+
'strip_prefix': self._filter_strip_prefix,
|
160
|
+
'length': self._filter_length,
|
161
|
+
'default': self._filter_default,
|
162
|
+
}
|
163
|
+
|
164
|
+
def _filter_upper(self, x: Any) -> str:
|
165
|
+
"""Convert to uppercase."""
|
166
|
+
return str(x).upper()
|
167
|
+
|
168
|
+
def _filter_lower(self, x: Any) -> str:
|
169
|
+
"""Convert to lowercase."""
|
170
|
+
return str(x).lower()
|
171
|
+
|
172
|
+
def _filter_strip(self, x: Any) -> str:
|
173
|
+
"""Strip whitespace."""
|
174
|
+
return str(x).strip()
|
175
|
+
|
176
|
+
def _filter_join(self, x: Any, delimiter: str = ', ') -> str:
|
177
|
+
"""Join list items or convert to string."""
|
178
|
+
if isinstance(x, list):
|
179
|
+
return delimiter.join(str(i) for i in x)
|
180
|
+
return str(x)
|
181
|
+
|
182
|
+
def _filter_json(self, x: Any) -> str:
|
183
|
+
"""Convert to JSON string."""
|
184
|
+
import json
|
185
|
+
return json.dumps(x) if x else "{}"
|
186
|
+
|
187
|
+
def _filter_shell_escape(self, x: Any) -> str:
|
188
|
+
"""Escape for shell usage."""
|
189
|
+
import shlex
|
190
|
+
return shlex.quote(str(x))
|
191
|
+
|
192
|
+
def _filter_strip_prefix(self, x: Any, prefix: str = 'v') -> str:
|
193
|
+
"""Strip prefix from string."""
|
194
|
+
s = str(x)
|
195
|
+
return s.lstrip(prefix) if isinstance(x, str) else s
|
196
|
+
|
197
|
+
def _filter_length(self, x: Any) -> int:
|
198
|
+
"""Get length of object."""
|
199
|
+
return len(x) if hasattr(x, '__len__') else 0
|
200
|
+
|
201
|
+
def _filter_default(self, x: Any, default_val: Any = '') -> Any:
|
202
|
+
"""Return default if value is falsy."""
|
203
|
+
return x if x else default_val
|
204
|
+
|
205
|
+
def evaluate_condition(self, condition: str) -> bool:
|
206
|
+
"""
|
207
|
+
Evaluate a conditional expression safely.
|
208
|
+
|
209
|
+
Examples:
|
210
|
+
flags.force
|
211
|
+
not flags.dry_run
|
212
|
+
flags.bump == "major"
|
213
|
+
arguments.branch in ["main", "master"]
|
214
|
+
"""
|
215
|
+
# Process any template variables first
|
216
|
+
processed_condition = self.process(condition)
|
217
|
+
|
218
|
+
# Use safe evaluation with restricted builtins
|
219
|
+
return self._safe_eval(processed_condition)
|
220
|
+
|
221
|
+
def _safe_eval(self, expression: str) -> bool:
|
222
|
+
"""Safely evaluate boolean expressions."""
|
223
|
+
# Create safe evaluation context
|
224
|
+
eval_context = self.context.to_dict()
|
225
|
+
|
226
|
+
# Allowed builtins for safe evaluation
|
227
|
+
safe_builtins = {
|
228
|
+
'__builtins__': {
|
229
|
+
'len': len,
|
230
|
+
'str': str,
|
231
|
+
'int': int,
|
232
|
+
'float': float,
|
233
|
+
'bool': bool,
|
234
|
+
'list': list,
|
235
|
+
'dict': dict,
|
236
|
+
'True': True,
|
237
|
+
'False': False,
|
238
|
+
'None': None,
|
239
|
+
}
|
240
|
+
}
|
241
|
+
|
242
|
+
try:
|
243
|
+
result = eval(expression, safe_builtins, eval_context)
|
244
|
+
return bool(result)
|
245
|
+
except Exception:
|
246
|
+
# Fallback to string comparison for simple cases
|
247
|
+
return self._fallback_condition_eval(expression, eval_context)
|
248
|
+
|
249
|
+
def _fallback_condition_eval(self, expression: str, context: Dict[str, Any]) -> bool:
|
250
|
+
"""Fallback condition evaluation for simple cases."""
|
251
|
+
# Handle simple true/false checks
|
252
|
+
if expression.lower() in ['true', '1', 'yes']:
|
253
|
+
return True
|
254
|
+
if expression.lower() in ['false', '0', 'no', '']:
|
255
|
+
return False
|
256
|
+
|
257
|
+
# Check if it's a simple variable lookup
|
258
|
+
if expression in context:
|
259
|
+
return bool(context[expression])
|
260
|
+
|
261
|
+
# Default to False for unknown expressions
|
262
|
+
return False
|
263
|
+
|
264
|
+
def process_modifications(self, step: Dict[str, Any]) -> Dict[str, Any]:
|
265
|
+
"""
|
266
|
+
Process modification directives in a step.
|
267
|
+
|
268
|
+
Supports explicit modification syntax for cleaner YAML:
|
269
|
+
modify:
|
270
|
+
flags.message: "cleaned message"
|
271
|
+
env.TOKEN: "{{captured.token}}"
|
272
|
+
|
273
|
+
Args:
|
274
|
+
step: Step dictionary potentially containing 'modify' directive
|
275
|
+
|
276
|
+
Returns:
|
277
|
+
Dictionary of modifications to apply to context
|
278
|
+
"""
|
279
|
+
if 'modify' not in step:
|
280
|
+
return {}
|
281
|
+
|
282
|
+
modifications = {}
|
283
|
+
modify_spec = step['modify']
|
284
|
+
|
285
|
+
for path, value in modify_spec.items():
|
286
|
+
# Process template in value
|
287
|
+
processed_value = self.process(value) if isinstance(value, str) else value
|
288
|
+
|
289
|
+
# Parse the path (e.g., "flags.message" -> modify flags)
|
290
|
+
parts = path.split('.')
|
291
|
+
if len(parts) == 2:
|
292
|
+
category, key = parts
|
293
|
+
if category not in modifications:
|
294
|
+
modifications[category] = {}
|
295
|
+
if isinstance(modifications[category], dict):
|
296
|
+
modifications[category][key] = processed_value
|
297
|
+
elif len(parts) == 1:
|
298
|
+
# Direct modification (e.g., "positionals")
|
299
|
+
modifications[parts[0]] = processed_value
|
300
|
+
|
301
|
+
return modifications
|
302
|
+
|
303
|
+
|
304
|
+
# These are now imported at module level where needed
|
305
|
+
|
306
|
+
|