scitex-session 0.1.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.
- scitex_session/__init__.py +79 -0
- scitex_session/_decorator.py +640 -0
- scitex_session/_lifecycle/__init__.py +23 -0
- scitex_session/_lifecycle/_close.py +246 -0
- scitex_session/_lifecycle/_config.py +112 -0
- scitex_session/_lifecycle/_matplotlib.py +93 -0
- scitex_session/_lifecycle/_start.py +262 -0
- scitex_session/_lifecycle/_utils.py +186 -0
- scitex_session/_manager.py +143 -0
- scitex_session/template.py +28 -0
- scitex_session-0.1.0.dist-info/METADATA +91 -0
- scitex_session-0.1.0.dist-info/RECORD +15 -0
- scitex_session-0.1.0.dist-info/WHEEL +5 -0
- scitex_session-0.1.0.dist-info/licenses/LICENSE +661 -0
- scitex_session-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Timestamp: "2025-08-21 20:36:45 (ywatanabe)"
|
|
3
|
+
# File: /home/ywatanabe/proj/SciTeX-Code/src/scitex/session/__init__.py
|
|
4
|
+
# ----------------------------------------
|
|
5
|
+
"""scitex-session — @session decorator + lifecycle management (standalone)."""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
__FILE__ = __file__
|
|
14
|
+
__DIR__ = os.path.dirname(__FILE__)
|
|
15
|
+
# ----------------------------------------
|
|
16
|
+
|
|
17
|
+
"""Experiment session management for SciTeX.
|
|
18
|
+
|
|
19
|
+
This module provides session lifecycle management functionality that was previously
|
|
20
|
+
in scitex.session.start and scitex.session.close, now as a dedicated session management system.
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
# Session management (replaces scitex.session.start/close)
|
|
24
|
+
import sys
|
|
25
|
+
import matplotlib.pyplot as plt
|
|
26
|
+
from scitex import session
|
|
27
|
+
|
|
28
|
+
# Start a session
|
|
29
|
+
CONFIG, sys.stdout, sys.stderr, plt, COLORS, rng = session.start(sys, plt)
|
|
30
|
+
|
|
31
|
+
# Your experiment code here
|
|
32
|
+
|
|
33
|
+
# Close the session
|
|
34
|
+
session.close(CONFIG)
|
|
35
|
+
|
|
36
|
+
# Session manager for advanced use cases
|
|
37
|
+
manager = session.SessionManager()
|
|
38
|
+
active_sessions = manager.get_active_sessions()
|
|
39
|
+
|
|
40
|
+
# Using INJECTED sentinel for decorator parameters
|
|
41
|
+
@stx.session
|
|
42
|
+
def main(CONFIG=stx.session.INJECTED, plt=stx.session.INJECTED):
|
|
43
|
+
...
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Sentinel object for decorator-injected parameters
|
|
48
|
+
class _InjectedSentinel:
|
|
49
|
+
"""Sentinel value indicating a parameter will be injected by a decorator."""
|
|
50
|
+
|
|
51
|
+
def __repr__(self):
|
|
52
|
+
return "<INJECTED>"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
INJECTED = _InjectedSentinel()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Import session management functionality
|
|
59
|
+
# Use refactored _lifecycle subpackage (verification hooks included)
|
|
60
|
+
from ._decorator import run, session
|
|
61
|
+
from ._lifecycle import close, running2finished, start
|
|
62
|
+
from ._manager import SessionManager
|
|
63
|
+
|
|
64
|
+
# Export public API
|
|
65
|
+
__all__ = [
|
|
66
|
+
# Sentinel for injected parameters
|
|
67
|
+
"INJECTED",
|
|
68
|
+
# Session lifecycle (main functions)
|
|
69
|
+
"start",
|
|
70
|
+
"close",
|
|
71
|
+
"running2finished",
|
|
72
|
+
# Session decorator (new simplified API)
|
|
73
|
+
"session",
|
|
74
|
+
"run",
|
|
75
|
+
# Advanced session management
|
|
76
|
+
"SessionManager",
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
# EOF
|
|
@@ -0,0 +1,640 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
# Timestamp: "2026-02-01 08:38:19 (ywatanabe)"
|
|
4
|
+
# File: /home/ywatanabe/proj/scitex-python/src/scitex/session/_decorator.py
|
|
5
|
+
|
|
6
|
+
# Timestamp: "2025-11-05"
|
|
7
|
+
"""Session decorator for scitex.
|
|
8
|
+
|
|
9
|
+
Provides @stx.session decorator that automatically:
|
|
10
|
+
- Generates CLI from function signature
|
|
11
|
+
- Manages session lifecycle
|
|
12
|
+
- Handles errors
|
|
13
|
+
- Organizes outputs
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import functools
|
|
18
|
+
import inspect
|
|
19
|
+
import sys as sys_module
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any, Callable, get_type_hints
|
|
22
|
+
|
|
23
|
+
from logging import getLogger
|
|
24
|
+
|
|
25
|
+
from . import INJECTED # Use local INJECTED from session module
|
|
26
|
+
from ._lifecycle import close, start
|
|
27
|
+
|
|
28
|
+
# Internal logger for the decorator itself
|
|
29
|
+
_decorator_logger = getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def session(
|
|
33
|
+
func: Callable = None,
|
|
34
|
+
*,
|
|
35
|
+
verbose: bool = False,
|
|
36
|
+
agg: bool = True,
|
|
37
|
+
notify: bool = False,
|
|
38
|
+
sdir_suffix: str = None,
|
|
39
|
+
**session_kwargs,
|
|
40
|
+
) -> Callable:
|
|
41
|
+
"""Decorator to wrap function in scitex session.
|
|
42
|
+
|
|
43
|
+
Automatically handles:
|
|
44
|
+
- CLI argument parsing from function signature
|
|
45
|
+
- Session initialization (logging, output directories)
|
|
46
|
+
- Execution
|
|
47
|
+
- Cleanup
|
|
48
|
+
- Error handling
|
|
49
|
+
|
|
50
|
+
This decorator is designed for script entry points. The decorated function
|
|
51
|
+
should be called without arguments from `if __name__ == '__main__':` to
|
|
52
|
+
trigger CLI parsing and session management.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
func: Function to wrap (set automatically by decorator)
|
|
56
|
+
verbose: Enable verbose logging
|
|
57
|
+
agg: Use matplotlib Agg backend
|
|
58
|
+
notify: Send notification on completion
|
|
59
|
+
sdir_suffix: Suffix for output directory name
|
|
60
|
+
**session_kwargs: Additional session configuration parameters
|
|
61
|
+
|
|
62
|
+
Example:
|
|
63
|
+
@stx.session
|
|
64
|
+
def analyze(data_path: str, threshold: float = 0.5):
|
|
65
|
+
'''Analyze data file.'''
|
|
66
|
+
data = stx.io.load(data_path)
|
|
67
|
+
result = process(data, threshold)
|
|
68
|
+
stx.io.save(result, "output.csv")
|
|
69
|
+
return 0
|
|
70
|
+
|
|
71
|
+
if __name__ == '__main__':
|
|
72
|
+
analyze() # No arguments = CLI mode with session management
|
|
73
|
+
|
|
74
|
+
# CLI: python script.py --data-path data.csv --threshold 0.7
|
|
75
|
+
|
|
76
|
+
Example with options:
|
|
77
|
+
@stx.session(verbose=True, notify=True)
|
|
78
|
+
def train_model(model_name: str, epochs: int = 10):
|
|
79
|
+
'''Train ML model.'''
|
|
80
|
+
# These are automatically available as globals:
|
|
81
|
+
# - CONFIG: Session configuration dict
|
|
82
|
+
# - plt: Matplotlib pyplot (configured for session)
|
|
83
|
+
# - COLORS: Custom Colors
|
|
84
|
+
# - rngg: RandomStateManager (fixes seeds, creates named generators)
|
|
85
|
+
logger.info(f"Session ID: {CONFIG['ID']}")
|
|
86
|
+
logger.info(f"Output directory: {CONFIG['SDIR_RUN']}")
|
|
87
|
+
# ... training code ...
|
|
88
|
+
return 0
|
|
89
|
+
|
|
90
|
+
if __name__ == '__main__':
|
|
91
|
+
train_model()
|
|
92
|
+
|
|
93
|
+
Notes:
|
|
94
|
+
- Function name can be anything (not just 'main')
|
|
95
|
+
- Calling with arguments bypasses session management: analyze('/path', 0.5)
|
|
96
|
+
- Only one session-managed function per script
|
|
97
|
+
- Do NOT call multiple @session decorated functions from one script
|
|
98
|
+
- Do NOT nest session-decorated function calls without arguments
|
|
99
|
+
|
|
100
|
+
Injected Global Variables:
|
|
101
|
+
When called without arguments (CLI mode), these are injected into globals:
|
|
102
|
+
- CONFIG (dict): Session configuration with ID, SDIR, paths, etc.
|
|
103
|
+
- plt (module): matplotlib.pyplot configured with session settings
|
|
104
|
+
- COLORS (CustomColors): Custom Colors for consistent plotting
|
|
105
|
+
- rngg (RandomStateManager): Manages reproducibility by fixing global seeds
|
|
106
|
+
and creating named generators via rngg("name")
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
def decorator(func: Callable) -> Callable:
|
|
110
|
+
@functools.wraps(func)
|
|
111
|
+
def wrapper(*args, **kwargs):
|
|
112
|
+
# If called with arguments (not CLI), run directly
|
|
113
|
+
if args or kwargs:
|
|
114
|
+
return func(*args, **kwargs)
|
|
115
|
+
|
|
116
|
+
# Otherwise, parse CLI and run with session management
|
|
117
|
+
return _run_with_session(
|
|
118
|
+
func,
|
|
119
|
+
verbose=verbose,
|
|
120
|
+
agg=agg,
|
|
121
|
+
notify=notify,
|
|
122
|
+
sdir_suffix=sdir_suffix,
|
|
123
|
+
**session_kwargs,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Store original function for direct access
|
|
127
|
+
wrapper._func = func
|
|
128
|
+
wrapper._is_session_wrapped = True
|
|
129
|
+
|
|
130
|
+
return wrapper
|
|
131
|
+
|
|
132
|
+
# Handle @stx.session vs @stx.session()
|
|
133
|
+
if func is None:
|
|
134
|
+
# Called with arguments: @stx.session(verbose=True)
|
|
135
|
+
return decorator
|
|
136
|
+
else:
|
|
137
|
+
# Called without arguments: @stx.session
|
|
138
|
+
return decorator(func)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _run_with_session(
|
|
142
|
+
func: Callable,
|
|
143
|
+
verbose: bool,
|
|
144
|
+
agg: bool,
|
|
145
|
+
notify: bool,
|
|
146
|
+
sdir_suffix: str,
|
|
147
|
+
**session_kwargs,
|
|
148
|
+
) -> Any:
|
|
149
|
+
"""Run function with full session management."""
|
|
150
|
+
|
|
151
|
+
# Get calling file
|
|
152
|
+
frame = inspect.currentframe()
|
|
153
|
+
caller_frame = frame.f_back.f_back # Go up two levels
|
|
154
|
+
caller_file = caller_frame.f_globals.get("__file__", "unknown.py")
|
|
155
|
+
|
|
156
|
+
# Generate argparse from function signature
|
|
157
|
+
parser = _create_parser(func)
|
|
158
|
+
args = parser.parse_args()
|
|
159
|
+
|
|
160
|
+
# Clean up INJECTED sentinels from args before passing to session
|
|
161
|
+
cleaned_args = argparse.Namespace(
|
|
162
|
+
**{k: v for k, v in vars(args).items() if not isinstance(v, type(INJECTED))}
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Start session
|
|
166
|
+
import matplotlib.pyplot as plt
|
|
167
|
+
|
|
168
|
+
CONFIG, stdout, stderr, plt, COLORS, rngg = start(
|
|
169
|
+
sys=sys_module,
|
|
170
|
+
plt=plt,
|
|
171
|
+
args=cleaned_args,
|
|
172
|
+
file=caller_file,
|
|
173
|
+
sdir_suffix=sdir_suffix or func.__name__,
|
|
174
|
+
verbose=verbose,
|
|
175
|
+
agg=agg,
|
|
176
|
+
**session_kwargs,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Create a logger for the user's script
|
|
180
|
+
script_logger = getLogger(func.__module__)
|
|
181
|
+
|
|
182
|
+
# Store session variables in function globals
|
|
183
|
+
func_globals = func.__globals__
|
|
184
|
+
func_globals["CONFIG"] = CONFIG
|
|
185
|
+
func_globals["plt"] = plt
|
|
186
|
+
func_globals["COLORS"] = COLORS
|
|
187
|
+
func_globals["rngg"] = rngg
|
|
188
|
+
func_globals["logger"] = script_logger
|
|
189
|
+
|
|
190
|
+
# Log injected globals for user awareness (only in verbose mode)
|
|
191
|
+
if verbose:
|
|
192
|
+
_decorator_logger.info("=" * 60)
|
|
193
|
+
_decorator_logger.info(
|
|
194
|
+
"Injected Global Variables (available in your function):"
|
|
195
|
+
)
|
|
196
|
+
_decorator_logger.info(" • CONFIG - Session configuration dict")
|
|
197
|
+
_decorator_logger.info(f" - CONFIG['ID']: {CONFIG['ID']}")
|
|
198
|
+
_decorator_logger.info(f" - CONFIG['SDIR_RUN']: {CONFIG['SDIR_RUN']}")
|
|
199
|
+
_decorator_logger.info(f" - CONFIG['PID']: {CONFIG['PID']}")
|
|
200
|
+
_decorator_logger.info(" • plt - matplotlib.pyplot (configured for session)")
|
|
201
|
+
_decorator_logger.info(" • COLORS - CustomColors (for consistent plotting)")
|
|
202
|
+
_decorator_logger.info(" • rngg - RandomStateManager (for reproducibility)")
|
|
203
|
+
_decorator_logger.info(
|
|
204
|
+
" • logger - SciTeX logger (configured for your script)"
|
|
205
|
+
)
|
|
206
|
+
_decorator_logger.info("=" * 60)
|
|
207
|
+
|
|
208
|
+
# Run function
|
|
209
|
+
exit_status = 0
|
|
210
|
+
result = None
|
|
211
|
+
|
|
212
|
+
try:
|
|
213
|
+
# Convert args namespace to kwargs
|
|
214
|
+
kwargs = vars(args)
|
|
215
|
+
|
|
216
|
+
# Get function parameters
|
|
217
|
+
sig = inspect.signature(func)
|
|
218
|
+
func_params = set(sig.parameters.keys())
|
|
219
|
+
|
|
220
|
+
# Map of injected variable names to their actual objects
|
|
221
|
+
injection_map = {
|
|
222
|
+
"CONFIG": CONFIG,
|
|
223
|
+
"plt": plt,
|
|
224
|
+
"COLORS": COLORS,
|
|
225
|
+
"rngg": rngg,
|
|
226
|
+
"logger": script_logger,
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
# Build filtered_kwargs with user args and injected values
|
|
230
|
+
filtered_kwargs = {}
|
|
231
|
+
|
|
232
|
+
# First, add all parsed CLI arguments
|
|
233
|
+
for k, v in kwargs.items():
|
|
234
|
+
if k in func_params:
|
|
235
|
+
filtered_kwargs[k] = v
|
|
236
|
+
|
|
237
|
+
# Then, inject parameters that have INJECTED as default
|
|
238
|
+
for param_name, param in sig.parameters.items():
|
|
239
|
+
if param.default != inspect.Parameter.empty:
|
|
240
|
+
if isinstance(param.default, type(INJECTED)):
|
|
241
|
+
# This parameter should be injected
|
|
242
|
+
if param_name in injection_map:
|
|
243
|
+
filtered_kwargs[param_name] = injection_map[param_name]
|
|
244
|
+
|
|
245
|
+
# Log injected arguments summary (only in verbose mode)
|
|
246
|
+
if verbose:
|
|
247
|
+
args_summary = {k: type(v).__name__ for k, v in filtered_kwargs.items()}
|
|
248
|
+
_decorator_logger.info(f"Running {func.__name__} with injected parameters:")
|
|
249
|
+
_decorator_logger.info(args_summary, pprint=True, indent=2)
|
|
250
|
+
|
|
251
|
+
# Execute function
|
|
252
|
+
result = func(**filtered_kwargs)
|
|
253
|
+
|
|
254
|
+
# Handle return value
|
|
255
|
+
if isinstance(result, int):
|
|
256
|
+
exit_status = result
|
|
257
|
+
else:
|
|
258
|
+
exit_status = 0
|
|
259
|
+
|
|
260
|
+
except Exception as e:
|
|
261
|
+
_decorator_logger.error(f"Error in {func.__name__}: {e}", exc_info=True)
|
|
262
|
+
exit_status = 1
|
|
263
|
+
raise
|
|
264
|
+
|
|
265
|
+
finally:
|
|
266
|
+
# Close session with error handling
|
|
267
|
+
try:
|
|
268
|
+
close(
|
|
269
|
+
CONFIG=CONFIG,
|
|
270
|
+
verbose=verbose,
|
|
271
|
+
notify=notify,
|
|
272
|
+
message=f"{func.__name__} completed",
|
|
273
|
+
exit_status=exit_status,
|
|
274
|
+
)
|
|
275
|
+
except SystemExit:
|
|
276
|
+
# Allow normal exits
|
|
277
|
+
raise
|
|
278
|
+
except KeyboardInterrupt:
|
|
279
|
+
# Allow Ctrl+C
|
|
280
|
+
raise
|
|
281
|
+
except Exception as e:
|
|
282
|
+
# Log but don't crash on cleanup errors
|
|
283
|
+
try:
|
|
284
|
+
_decorator_logger.error(f"Session cleanup error: {e}")
|
|
285
|
+
except:
|
|
286
|
+
print(f"Session cleanup error: {e}")
|
|
287
|
+
|
|
288
|
+
# Final matplotlib cleanup (belt and suspenders approach)
|
|
289
|
+
try:
|
|
290
|
+
import matplotlib.pyplot as plt
|
|
291
|
+
|
|
292
|
+
plt.close("all")
|
|
293
|
+
except:
|
|
294
|
+
pass
|
|
295
|
+
|
|
296
|
+
return result
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _create_parser(func: Callable) -> argparse.ArgumentParser:
|
|
300
|
+
"""Create ArgumentParser from function signature.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
func: Function to create parser for
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
Configured ArgumentParser
|
|
307
|
+
"""
|
|
308
|
+
|
|
309
|
+
# Get function info
|
|
310
|
+
sig = inspect.signature(func)
|
|
311
|
+
doc = inspect.getdoc(func) or f"Run {func.__name__}"
|
|
312
|
+
|
|
313
|
+
# Try to get type hints
|
|
314
|
+
try:
|
|
315
|
+
type_hints = get_type_hints(func)
|
|
316
|
+
except Exception:
|
|
317
|
+
type_hints = {}
|
|
318
|
+
|
|
319
|
+
# Get actual values for deterministic items
|
|
320
|
+
# Get calling file from the decorated function's module
|
|
321
|
+
caller_file = func.__globals__.get("__file__", "unknown.py")
|
|
322
|
+
|
|
323
|
+
# Calculate SDIR_OUT (base output directory)
|
|
324
|
+
import os
|
|
325
|
+
|
|
326
|
+
sdir_out = Path(os.path.splitext(caller_file)[0] + "_out")
|
|
327
|
+
sdir_run_example = sdir_out / "RUNNING" / "<SESSION_ID>"
|
|
328
|
+
|
|
329
|
+
# Get current PID
|
|
330
|
+
current_pid = os.getpid()
|
|
331
|
+
|
|
332
|
+
# Check for config YAML files and list all variables with values
|
|
333
|
+
config_status = ""
|
|
334
|
+
try:
|
|
335
|
+
config_dir = Path("./config")
|
|
336
|
+
if config_dir.exists():
|
|
337
|
+
yaml_files = sorted(config_dir.glob("*.yaml"))
|
|
338
|
+
if yaml_files:
|
|
339
|
+
config_status = " CONFIG from YAML files:\n"
|
|
340
|
+
|
|
341
|
+
# Load and list all config variables with their values
|
|
342
|
+
try:
|
|
343
|
+
import yaml
|
|
344
|
+
|
|
345
|
+
all_vars = []
|
|
346
|
+
for yaml_file in yaml_files:
|
|
347
|
+
with open(yaml_file, "r") as f:
|
|
348
|
+
data = yaml.safe_load(f)
|
|
349
|
+
if isinstance(data, dict):
|
|
350
|
+
namespace = yaml_file.stem.upper()
|
|
351
|
+
for key, value in data.items():
|
|
352
|
+
# Format value for display (truncate if too long)
|
|
353
|
+
value_str = str(value)
|
|
354
|
+
if len(value_str) > 50:
|
|
355
|
+
value_str = value_str[:47] + "..."
|
|
356
|
+
all_vars.append(
|
|
357
|
+
f" - CONFIG.{namespace}.{key} (from ./config/{yaml_file.name})\n {value_str}"
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
if all_vars:
|
|
361
|
+
config_status += "\n".join(all_vars)
|
|
362
|
+
else:
|
|
363
|
+
config_status = " CONFIG from YAML files:\n (no variables found)"
|
|
364
|
+
except Exception as e:
|
|
365
|
+
# If we can't load the YAML files, just show error
|
|
366
|
+
config_status = " CONFIG from YAML files:\n (unable to load at help-time, will be available at runtime)"
|
|
367
|
+
else:
|
|
368
|
+
config_status = (
|
|
369
|
+
" CONFIG from YAML files:\n (no .yaml files found)"
|
|
370
|
+
)
|
|
371
|
+
else:
|
|
372
|
+
config_status = " CONFIG from YAML files:\n (./config/ directory not found)"
|
|
373
|
+
except:
|
|
374
|
+
config_status = (
|
|
375
|
+
" CONFIG from YAML files:\n (unable to check at help-time)"
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
# Get available color keys
|
|
379
|
+
try:
|
|
380
|
+
import matplotlib.pyplot as plt_temp
|
|
381
|
+
|
|
382
|
+
from scitex.plt.utils._configure_mpl import configure_mpl
|
|
383
|
+
|
|
384
|
+
_, colors_dict = configure_mpl(plt_temp)
|
|
385
|
+
# Show all color keys
|
|
386
|
+
sorted_keys = sorted(colors_dict.keys())
|
|
387
|
+
color_keys = ", ".join(f"'{k}'" for k in sorted_keys)
|
|
388
|
+
except Exception as e:
|
|
389
|
+
# Fallback if configure_mpl fails
|
|
390
|
+
color_keys = "'blue', 'red', 'green', 'yellow', 'purple', 'orange', ..."
|
|
391
|
+
|
|
392
|
+
# Create parser with epilog documenting injected globals with actual values
|
|
393
|
+
epilog = f"""
|
|
394
|
+
Global Variables Injected by @session Decorator:
|
|
395
|
+
|
|
396
|
+
CONFIG (DotDict)
|
|
397
|
+
Session configuration with ID, paths, timestamps
|
|
398
|
+
Access: CONFIG['key'] or CONFIG.key (both work!)
|
|
399
|
+
|
|
400
|
+
- CONFIG.ID
|
|
401
|
+
<SESSION_ID> (created at runtime, e.g., '2025Y-11M-18D-07h53m37s_Z5MR')
|
|
402
|
+
- CONFIG.FILE
|
|
403
|
+
{Path(caller_file)}
|
|
404
|
+
- CONFIG.SDIR_OUT
|
|
405
|
+
{sdir_out}
|
|
406
|
+
- CONFIG.SDIR_RUN
|
|
407
|
+
{sdir_run_example}
|
|
408
|
+
- CONFIG.PID
|
|
409
|
+
{current_pid} (current Python process)
|
|
410
|
+
- CONFIG.ARGS
|
|
411
|
+
{{'arg1': '<value>'}} (parsed from command line)
|
|
412
|
+
|
|
413
|
+
{config_status}
|
|
414
|
+
|
|
415
|
+
plt (module)
|
|
416
|
+
matplotlib.pyplot configured for session
|
|
417
|
+
|
|
418
|
+
COLORS (DotDict)
|
|
419
|
+
Color palette for consistent plotting
|
|
420
|
+
Access: COLORS.blue or COLORS['blue'] (both work!)
|
|
421
|
+
|
|
422
|
+
Available keys:
|
|
423
|
+
{color_keys}
|
|
424
|
+
|
|
425
|
+
Usage:
|
|
426
|
+
plt.plot(x, y, color=COLORS.blue)
|
|
427
|
+
plt.plot(x, y, color=COLORS['blue'])
|
|
428
|
+
|
|
429
|
+
rngg (RandomStateManager)
|
|
430
|
+
Manages reproducible randomness
|
|
431
|
+
|
|
432
|
+
logger (SciTeXLogger)
|
|
433
|
+
Logger configured for your script
|
|
434
|
+
"""
|
|
435
|
+
|
|
436
|
+
parser = argparse.ArgumentParser(
|
|
437
|
+
description=doc,
|
|
438
|
+
epilog=epilog,
|
|
439
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
# Add arguments from function signature (skip injected parameters)
|
|
443
|
+
# Track used short forms to avoid conflicts
|
|
444
|
+
used_short_forms = {"h"} # Reserve -h for help
|
|
445
|
+
|
|
446
|
+
for param_name, param in sig.parameters.items():
|
|
447
|
+
# Skip parameters with INJECTED as default (these are injected by decorator)
|
|
448
|
+
if param.default != inspect.Parameter.empty:
|
|
449
|
+
if isinstance(param.default, type(INJECTED)):
|
|
450
|
+
continue # Skip injected parameters
|
|
451
|
+
|
|
452
|
+
# Generate short form
|
|
453
|
+
short_form = _generate_short_form(param_name, used_short_forms)
|
|
454
|
+
if short_form:
|
|
455
|
+
used_short_forms.add(short_form)
|
|
456
|
+
|
|
457
|
+
_add_argument(parser, param_name, param, type_hints, short_form)
|
|
458
|
+
|
|
459
|
+
return parser
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _generate_short_form(param_name: str, used_short_forms: set) -> str:
|
|
463
|
+
"""Generate a short form for a parameter name avoiding conflicts.
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
param_name: Full parameter name
|
|
467
|
+
used_short_forms: Set of already used short forms
|
|
468
|
+
|
|
469
|
+
Returns:
|
|
470
|
+
Short form character or None if no unique form can be generated
|
|
471
|
+
"""
|
|
472
|
+
# Strategy 1: Try first letter
|
|
473
|
+
first_letter = param_name[0].lower()
|
|
474
|
+
if first_letter not in used_short_forms:
|
|
475
|
+
return first_letter
|
|
476
|
+
|
|
477
|
+
# Strategy 2: Try first letter of each word (for snake_case or camelCase)
|
|
478
|
+
words = param_name.replace("_", " ").replace("-", " ").split()
|
|
479
|
+
if len(words) > 1:
|
|
480
|
+
acronym = "".join(w[0].lower() for w in words)
|
|
481
|
+
if len(acronym) == 1 and acronym not in used_short_forms:
|
|
482
|
+
return acronym
|
|
483
|
+
|
|
484
|
+
# Strategy 3: Try first two letters
|
|
485
|
+
if len(param_name) >= 2:
|
|
486
|
+
two_letters = param_name[:2].lower()
|
|
487
|
+
if two_letters not in used_short_forms:
|
|
488
|
+
return two_letters
|
|
489
|
+
|
|
490
|
+
# Strategy 4: Try each character in sequence
|
|
491
|
+
for char in param_name.lower():
|
|
492
|
+
if char.isalnum() and char not in used_short_forms:
|
|
493
|
+
return char
|
|
494
|
+
|
|
495
|
+
# Give up if no unique short form found
|
|
496
|
+
return None
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _add_argument(
|
|
500
|
+
parser: argparse.ArgumentParser,
|
|
501
|
+
param_name: str,
|
|
502
|
+
param: inspect.Parameter,
|
|
503
|
+
type_hints: dict,
|
|
504
|
+
short_form: str = None,
|
|
505
|
+
):
|
|
506
|
+
"""Add single argument to parser.
|
|
507
|
+
|
|
508
|
+
Args:
|
|
509
|
+
parser: ArgumentParser to add to
|
|
510
|
+
param_name: Parameter name
|
|
511
|
+
param: Parameter object
|
|
512
|
+
type_hints: Type hints dictionary
|
|
513
|
+
short_form: Optional short form (e.g., 'a' for -a)
|
|
514
|
+
"""
|
|
515
|
+
from typing import Literal, get_args, get_origin
|
|
516
|
+
|
|
517
|
+
# Get type
|
|
518
|
+
param_type = type_hints.get(param_name, param.annotation)
|
|
519
|
+
if param_type == inspect.Parameter.empty:
|
|
520
|
+
param_type = str
|
|
521
|
+
|
|
522
|
+
# Get default
|
|
523
|
+
has_default = param.default != inspect.Parameter.empty
|
|
524
|
+
default = param.default if has_default else None
|
|
525
|
+
|
|
526
|
+
# Convert parameter name to CLI format
|
|
527
|
+
arg_name = f"--{param_name.replace('_', '-')}"
|
|
528
|
+
|
|
529
|
+
# Build argument names list (long form, optionally short form)
|
|
530
|
+
arg_names = [arg_name]
|
|
531
|
+
if short_form:
|
|
532
|
+
arg_names.insert(0, f"-{short_form}")
|
|
533
|
+
|
|
534
|
+
# Check for Literal type (choices)
|
|
535
|
+
choices = None
|
|
536
|
+
origin = get_origin(param_type)
|
|
537
|
+
if origin is Literal:
|
|
538
|
+
choices = list(get_args(param_type))
|
|
539
|
+
param_type = type(choices[0]) if choices else str
|
|
540
|
+
|
|
541
|
+
# Handle different types
|
|
542
|
+
if param_type == bool:
|
|
543
|
+
# Boolean flags
|
|
544
|
+
parser.add_argument(
|
|
545
|
+
*arg_names,
|
|
546
|
+
action="store_true" if not default else "store_false",
|
|
547
|
+
default=default,
|
|
548
|
+
help=f"(default: {default})",
|
|
549
|
+
)
|
|
550
|
+
else:
|
|
551
|
+
# Regular arguments
|
|
552
|
+
choices_str = f", choices: {choices}" if choices else ""
|
|
553
|
+
kwargs = {
|
|
554
|
+
"type": param_type,
|
|
555
|
+
"help": (
|
|
556
|
+
f"(default: {default}{choices_str})"
|
|
557
|
+
if has_default
|
|
558
|
+
else f"(required{choices_str})"
|
|
559
|
+
),
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if choices:
|
|
563
|
+
kwargs["choices"] = choices
|
|
564
|
+
|
|
565
|
+
if has_default:
|
|
566
|
+
kwargs["default"] = default
|
|
567
|
+
else:
|
|
568
|
+
kwargs["required"] = True
|
|
569
|
+
|
|
570
|
+
parser.add_argument(*arg_names, **kwargs)
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def run(func: Callable, parse_args: Callable = None, **session_kwargs) -> Any:
|
|
574
|
+
"""Run function with session management.
|
|
575
|
+
|
|
576
|
+
Alternative to decorator for more explicit control.
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
func: Function to run
|
|
580
|
+
parse_args: Optional custom argument parser
|
|
581
|
+
**session_kwargs: Session configuration
|
|
582
|
+
|
|
583
|
+
Example:
|
|
584
|
+
def main(args):
|
|
585
|
+
# Your code
|
|
586
|
+
return 0
|
|
587
|
+
|
|
588
|
+
if __name__ == '__main__':
|
|
589
|
+
stx.session.run(main)
|
|
590
|
+
"""
|
|
591
|
+
|
|
592
|
+
if parse_args is None:
|
|
593
|
+
# Auto-generate parser
|
|
594
|
+
parser = _create_parser(func)
|
|
595
|
+
args = parser.parse_args()
|
|
596
|
+
else:
|
|
597
|
+
# Use custom parser
|
|
598
|
+
args = parse_args()
|
|
599
|
+
|
|
600
|
+
# Get file
|
|
601
|
+
frame = inspect.currentframe()
|
|
602
|
+
caller_frame = frame.f_back
|
|
603
|
+
caller_file = caller_frame.f_globals.get("__file__", "unknown.py")
|
|
604
|
+
|
|
605
|
+
# Start session
|
|
606
|
+
import matplotlib.pyplot as plt
|
|
607
|
+
|
|
608
|
+
CONFIG, stdout, stderr, plt, COLORS, rngg = start(
|
|
609
|
+
sys=sys_module,
|
|
610
|
+
plt=plt,
|
|
611
|
+
args=args,
|
|
612
|
+
file=caller_file,
|
|
613
|
+
**session_kwargs,
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
# Run
|
|
617
|
+
try:
|
|
618
|
+
if hasattr(args, "__dict__"):
|
|
619
|
+
exit_status = func(args)
|
|
620
|
+
else:
|
|
621
|
+
exit_status = func()
|
|
622
|
+
|
|
623
|
+
exit_status = exit_status or 0
|
|
624
|
+
|
|
625
|
+
except Exception as e:
|
|
626
|
+
_decorator_logger.error(f"Error: {e}", exc_info=True)
|
|
627
|
+
exit_status = 1
|
|
628
|
+
raise
|
|
629
|
+
|
|
630
|
+
finally:
|
|
631
|
+
close(
|
|
632
|
+
CONFIG=CONFIG,
|
|
633
|
+
exit_status=exit_status,
|
|
634
|
+
**session_kwargs,
|
|
635
|
+
)
|
|
636
|
+
|
|
637
|
+
return exit_status
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
# EOF
|