execsql2 2.4.5__py3-none-any.whl → 2.6.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.
Files changed (62) hide show
  1. execsql/cli/__init__.py +14 -0
  2. execsql/cli/dsn.py +2 -0
  3. execsql/cli/help.py +2 -0
  4. execsql/cli/run.py +4 -2
  5. execsql/constants.py +11 -0
  6. execsql/db/access.py +20 -0
  7. execsql/db/base.py +4 -0
  8. execsql/db/dsn.py +11 -8
  9. execsql/db/duckdb.py +12 -8
  10. execsql/db/firebird.py +17 -8
  11. execsql/db/mysql.py +13 -8
  12. execsql/db/oracle.py +22 -8
  13. execsql/db/postgres.py +21 -9
  14. execsql/db/sqlite.py +18 -8
  15. execsql/db/sqlserver.py +14 -8
  16. execsql/exporters/__init__.py +6 -1
  17. execsql/exporters/base.py +2 -0
  18. execsql/exporters/delimited.py +10 -0
  19. execsql/exporters/protocol.py +128 -0
  20. execsql/exporters/xls.py +8 -0
  21. execsql/format.py +3 -1
  22. execsql/gui/__init__.py +2 -0
  23. execsql/gui/base.py +2 -0
  24. execsql/gui/console.py +2 -0
  25. execsql/gui/desktop.py +1 -0
  26. execsql/gui/tui.py +134 -0
  27. execsql/importers/base.py +1 -0
  28. execsql/importers/csv.py +2 -0
  29. execsql/importers/feather.py +2 -0
  30. execsql/importers/ods.py +1 -0
  31. execsql/importers/xls.py +1 -0
  32. execsql/metacommands/__init__.py +386 -180
  33. execsql/metacommands/dispatch.py +2 -0
  34. execsql/metacommands/io.py +41 -0
  35. execsql/models.py +17 -0
  36. execsql/parser.py +41 -0
  37. execsql/script/control.py +2 -0
  38. execsql/script/engine.py +19 -0
  39. execsql/script/variables.py +9 -5
  40. execsql/state.py +312 -199
  41. execsql/types.py +46 -0
  42. {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/METADATA +2 -2
  43. {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/RECORD +62 -61
  44. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/README.md +0 -0
  45. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
  46. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
  47. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/execsql.conf +0 -0
  48. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
  49. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/md_compare.sql +0 -0
  50. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
  51. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
  52. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
  53. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
  54. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
  55. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/script_template.sql +0 -0
  56. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
  57. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
  58. {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
  59. {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/WHEEL +0 -0
  60. {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/entry_points.txt +0 -0
  61. {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/licenses/LICENSE.txt +0 -0
  62. {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/licenses/NOTICE +0 -0
execsql/state.py CHANGED
@@ -12,27 +12,25 @@ and then access attributes (``_state.conf``, ``_state.subvars``, etc.)
12
12
  inside function/method bodies — never at class-definition time — to avoid
13
13
  circular-import issues at load time.
14
14
 
15
- Variable groups defined here:
16
-
17
- - **Configuration** — ``conf`` (:class:`execsql.config.ConfigData`),
18
- ``logfile_encoding``.
19
- - **Runtime flags** — ``last_command``, ``upass``, ``varlike`` regex,
20
- halt/cancel write-spec and mail-spec objects.
21
- - **Execution stack** — ``commandliststack``, ``savedscripts``,
22
- ``loopcommandstack``, ``compiling_loop``, loop regexes and nesting counter.
23
- - **Lazy singletons** ``if_stack``, ``counters``, ``timer``, ``output``,
24
- ``dbs``, ``tempfiles``, ``export_metadata``, ``metacommandlist``,
25
- ``filewriter``, ``gui_console``, GUI queue/thread.
26
- - **Version** — ``primary_vno``, ``secondary_vno``, ``tertiary_vno`` parsed
27
- from ``__version__``.
28
- - **Utility functions** — ``xcmd_test()`` (evaluate a conditional string),
29
- ``endloop()`` (finalise a compiled loop).
30
-
31
- All re-exports have been removed. Each module now imports directly from
32
- its source module rather than accessing names via ``_state``.
15
+ Internally, all mutable state lives on a :class:`RuntimeContext` instance
16
+ (``_ctx``). The module's ``__class__`` is swapped to a custom
17
+ :class:`types.ModuleType` subclass that transparently proxies attribute
18
+ reads and writes to the active context. This means:
19
+
20
+ - External code continues to use ``_state.conf``, ``_state.subvars = ...``,
21
+ etc. with zero changes.
22
+ - Functions *within* this module use ``_ctx.conf``, ``_ctx.subvars``, etc.
23
+ directly, because Python's ``LOAD_GLOBAL`` / ``STORE_GLOBAL`` bytecodes
24
+ access ``__dict__`` directly and do not trigger ``__getattr__`` /
25
+ ``__setattr__`` on the module class.
26
+
27
+ Use :func:`get_context` / :func:`set_context` to obtain or replace the
28
+ active context programmatically.
33
29
  """
34
30
 
35
31
  import re
32
+ import sys
33
+ import types
36
34
  from typing import TYPE_CHECKING, Any
37
35
 
38
36
  if TYPE_CHECKING:
@@ -55,154 +53,288 @@ if TYPE_CHECKING:
55
53
  from execsql.utils.mail import MailSpec
56
54
  from execsql.utils.timer import Timer
57
55
 
58
- # ---------------------------------------------------------------------------
59
- # Configuration / encoding
60
- # ---------------------------------------------------------------------------
61
-
62
- # Configuration data, initialized in main()
63
- conf: ConfigData | None = None
64
-
65
- # Default encodings
66
- logfile_encoding: str = "utf8" # Should never be changed; is not configurable.
56
+ __all__ = [
57
+ # Configuration / encoding
58
+ "conf",
59
+ "logfile_encoding",
60
+ # Runtime state
61
+ "last_command",
62
+ "upass",
63
+ "varlike",
64
+ "err_halt_writespec",
65
+ "err_halt_email",
66
+ "err_halt_exec",
67
+ "cancel_halt_writespec",
68
+ "cancel_halt_mailspec",
69
+ "cancel_halt_exec",
70
+ "commandliststack",
71
+ "savedscripts",
72
+ "loopcommandstack",
73
+ "compiling_loop",
74
+ "endloop_rx",
75
+ "loop_rx",
76
+ "loop_nest_level",
77
+ "cmds_run",
78
+ "defer_rx",
79
+ "stringtypes",
80
+ "exec_log",
81
+ "subvars",
82
+ "status",
83
+ # Lazy singletons
84
+ "if_stack",
85
+ "counters",
86
+ "timer",
87
+ "output",
88
+ "dbs",
89
+ "tempfiles",
90
+ "export_metadata",
91
+ "metacommandlist",
92
+ "conditionallist",
93
+ "filewriter",
94
+ "gui_console",
95
+ "gui_manager_queue",
96
+ "gui_manager_thread",
97
+ # Version
98
+ "primary_vno",
99
+ "secondary_vno",
100
+ "tertiary_vno",
101
+ # Functions
102
+ "xcmd_test",
103
+ "endloop",
104
+ "reset",
105
+ "initialize",
106
+ # New public API
107
+ "RuntimeContext",
108
+ "get_context",
109
+ "set_context",
110
+ ]
67
111
 
68
112
  # ---------------------------------------------------------------------------
69
- # Runtime state variables
113
+ # Compile-time constants — immutable after module load, stay in __dict__
70
114
  # ---------------------------------------------------------------------------
71
115
 
72
- # The last command run. This should be a ScriptCmd object.
73
- last_command: ScriptCmd | None = None
74
-
75
- # The last user password entered via 'get_password()'
76
- upass: str | None = None
77
-
78
116
  # A compiled regex to match prefixed regular expressions, used to check
79
117
  # for unsubstituted variables.
80
118
  varlike = re.compile(r"!![$@&~#]?\w+!!", re.I)
81
119
 
82
- # A WriteSpec object for messages to be written when the program halts due to an error.
83
- err_halt_writespec: WriteSpec | None = None
84
-
85
- # A MailSpec object for email to be sent when the program halts due to an error.
86
- err_halt_email: MailSpec | None = None
87
-
88
- # A ScriptExecSpec object for a script to be executed when the program halts due to an error.
89
- err_halt_exec: ScriptExecSpec | None = None
90
-
91
- # A WriteSpec object for messages to be written when the program halts due to user cancellation.
92
- cancel_halt_writespec: WriteSpec | None = None
93
-
94
- # A MailSpec object for email to be sent when the program halts due to user cancellation.
95
- cancel_halt_mailspec: MailSpec | None = None
96
-
97
- # A ScriptExecSpec object for a script to be executed when the program halts due to user cancellation.
98
- cancel_halt_exec: ScriptExecSpec | None = None
99
-
100
- # A stack of the CommandList objects currently in the queue to be executed.
101
- commandliststack: list[CommandList] = []
102
-
103
- # A dictionary of CommandList objects (ordinarily created by BEGIN/END SCRIPT metacommands).
104
- savedscripts: dict[str, CommandList] = {}
105
-
106
- # A stack of CommandList objects used when compiling the statements within a loop.
107
- loopcommandstack: list[CommandList] = []
108
-
109
- # A global flag to indicate that commands should be compiled into the topmost entry
110
- # in the loopcommandstack rather than executed.
111
- compiling_loop: bool = False
112
-
113
120
  # Compiled regex for END LOOP metacommand, which is immediate.
114
121
  endloop_rx = re.compile(r"^\s*END\s+LOOP\s*$", re.I)
115
122
 
116
- # Compiled regex for *start of* LOOP metacommand, for testing while compiling commands within a loop.
123
+ # Compiled regex for *start of* LOOP metacommand, for testing while compiling
124
+ # commands within a loop.
117
125
  loop_rx = re.compile(r"\s*LOOP\s+", re.I)
118
126
 
119
- # Nesting counter, to ensure loops are only ended when nesting level is zero.
120
- loop_nest_level: int = 0
121
-
122
- # A count of all of the commands run.
123
- cmds_run: int = 0
124
-
125
127
  # Pattern for deferred substitution, e.g.: "!{somevar}!"
126
128
  defer_rx = re.compile(r"(!{([$@&~#]?[a-z0-9_]+)}!)", re.I)
127
129
 
128
130
  # The string type (str in Python 3).
129
131
  stringtypes: type = str
130
132
 
131
- # The execution log object; set at startup.
132
- exec_log: Logger | None = None
133
-
134
- # The substitution variable set; set at startup.
135
- subvars: SubVarSet | None = None
136
-
137
- # The program execution status tracker; set at startup.
138
- status: StatObj | None = None
139
-
140
133
  # ---------------------------------------------------------------------------
141
- # Runtime objects initialized in main() to avoid circular imports at load time.
134
+ # Version numbers (parsed from package __version__)
142
135
  # ---------------------------------------------------------------------------
143
136
 
144
- # Stack-based conditional state (IfLevels instance).
145
- if_stack: IfLevels | None = None
137
+ try:
138
+ from execsql import __version__ as _pkg_version
146
139
 
147
- # Global counter variables (CounterVars instance).
148
- counters: CounterVars | None = None
140
+ _vparts = _pkg_version.split(".")
141
+ primary_vno: int = int(_vparts[0]) if len(_vparts) > 0 else 0
142
+ secondary_vno: int = int(_vparts[1]) if len(_vparts) > 1 else 0
143
+ tertiary_vno: int = int(_vparts[2]) if len(_vparts) > 2 else 0
144
+ except Exception:
145
+ primary_vno = 0
146
+ secondary_vno = 0
147
+ tertiary_vno = 0
149
148
 
150
- # Elapsed-time tracker (Timer instance).
151
- timer: Timer | None = None
152
149
 
153
- # Redirectable output (WriteHooks instance).
154
- output: WriteHooks | None = None
150
+ # ---------------------------------------------------------------------------
151
+ # RuntimeContext holds all mutable state for a single execsql session
152
+ # ---------------------------------------------------------------------------
155
153
 
156
- # Database connection pool (DatabasePool instance).
157
- dbs: DatabasePool | None = None
154
+ _CONTEXT_ATTRS: frozenset[str] = frozenset(
155
+ {
156
+ # Configuration
157
+ "conf",
158
+ "logfile_encoding",
159
+ # Runtime flags
160
+ "last_command",
161
+ "upass",
162
+ "err_halt_writespec",
163
+ "err_halt_email",
164
+ "err_halt_exec",
165
+ "cancel_halt_writespec",
166
+ "cancel_halt_mailspec",
167
+ "cancel_halt_exec",
168
+ # Execution stack
169
+ "commandliststack",
170
+ "savedscripts",
171
+ "loopcommandstack",
172
+ "compiling_loop",
173
+ "loop_nest_level",
174
+ "cmds_run",
175
+ # I/O
176
+ "exec_log",
177
+ "subvars",
178
+ "status",
179
+ "output",
180
+ "filewriter",
181
+ # Lazy singletons
182
+ "if_stack",
183
+ "counters",
184
+ "timer",
185
+ "dbs",
186
+ "tempfiles",
187
+ "export_metadata",
188
+ "metacommandlist",
189
+ "conditionallist",
190
+ # GUI
191
+ "gui_console",
192
+ "gui_manager_queue",
193
+ "gui_manager_thread",
194
+ },
195
+ )
196
+
197
+
198
+ class RuntimeContext:
199
+ """All mutable runtime state for a single execsql execution session.
200
+
201
+ A fresh instance provides clean default values identical to the original
202
+ module-level declarations. Use :func:`get_context` to obtain the active
203
+ instance, or :func:`set_context` to replace it.
204
+ """
158
205
 
159
- # Temporary file manager (TempFileMgr instance).
160
- tempfiles: TempFileMgr | None = None
206
+ __slots__ = (
207
+ # Configuration
208
+ "conf",
209
+ "logfile_encoding",
210
+ # Runtime flags
211
+ "last_command",
212
+ "upass",
213
+ "err_halt_writespec",
214
+ "err_halt_email",
215
+ "err_halt_exec",
216
+ "cancel_halt_writespec",
217
+ "cancel_halt_mailspec",
218
+ "cancel_halt_exec",
219
+ # Execution stack
220
+ "commandliststack",
221
+ "savedscripts",
222
+ "loopcommandstack",
223
+ "compiling_loop",
224
+ "loop_nest_level",
225
+ "cmds_run",
226
+ # I/O
227
+ "exec_log",
228
+ "subvars",
229
+ "status",
230
+ "output",
231
+ "filewriter",
232
+ # Lazy singletons
233
+ "if_stack",
234
+ "counters",
235
+ "timer",
236
+ "dbs",
237
+ "tempfiles",
238
+ "export_metadata",
239
+ "metacommandlist",
240
+ "conditionallist",
241
+ # GUI
242
+ "gui_console",
243
+ "gui_manager_queue",
244
+ "gui_manager_thread",
245
+ )
161
246
 
162
- # Export metadata tracker (ExportMetadata instance).
163
- export_metadata: ExportMetadata | None = None
247
+ def __init__(self) -> None:
248
+ # Configuration
249
+ self.conf: ConfigData | None = None
250
+ self.logfile_encoding: str = "utf8"
251
+
252
+ # Runtime flags
253
+ self.last_command: ScriptCmd | None = None
254
+ self.upass: str | None = None
255
+ self.err_halt_writespec: WriteSpec | None = None
256
+ self.err_halt_email: MailSpec | None = None
257
+ self.err_halt_exec: ScriptExecSpec | None = None
258
+ self.cancel_halt_writespec: WriteSpec | None = None
259
+ self.cancel_halt_mailspec: MailSpec | None = None
260
+ self.cancel_halt_exec: ScriptExecSpec | None = None
261
+
262
+ # Execution stack
263
+ self.commandliststack: list[CommandList] = []
264
+ self.savedscripts: dict[str, CommandList] = {}
265
+ self.loopcommandstack: list[CommandList] = []
266
+ self.compiling_loop: bool = False
267
+ self.loop_nest_level: int = 0
268
+ self.cmds_run: int = 0
269
+
270
+ # I/O
271
+ self.exec_log: Logger | None = None
272
+ self.subvars: SubVarSet | None = None
273
+ self.status: StatObj | None = None
274
+ self.output: WriteHooks | None = None
275
+ self.filewriter: FileWriter | None = None
276
+
277
+ # Lazy singletons
278
+ self.if_stack: IfLevels | None = None
279
+ self.counters: CounterVars | None = None
280
+ self.timer: Timer | None = None
281
+ self.dbs: DatabasePool | None = None
282
+ self.tempfiles: TempFileMgr | None = None
283
+ self.export_metadata: ExportMetadata | None = None
284
+ self.metacommandlist: MetaCommandList | None = None
285
+ self.conditionallist: MetaCommandList | None = None
286
+
287
+ # GUI
288
+ self.gui_console: Any = None
289
+ self.gui_manager_queue: _mp.Queue | None = None
290
+ self.gui_manager_thread: _threading.Thread | None = None
164
291
 
165
- # Metacommand dispatch table (MetaCommandList instance).
166
- metacommandlist: MetaCommandList | None = None
167
292
 
168
- # Conditional predicate dispatch table (MetaCommandList instance).
169
- conditionallist: MetaCommandList | None = None
293
+ # ---------------------------------------------------------------------------
294
+ # Module proxy transparently delegates context attr access to _ctx
295
+ # ---------------------------------------------------------------------------
170
296
 
171
- # Asynchronous file-writer subprocess (FileWriter instance).
172
- filewriter: FileWriter | None = None
173
297
 
174
- # GUI console object.
175
- gui_console: Any = None # Varies by backend (ConsoleUI, DesktopUI, TextualUI).
298
+ class _StateModule(types.ModuleType):
299
+ """Module subclass that proxies mutable state attributes to the active RuntimeContext."""
176
300
 
177
- # Queue and thread used to communicate with the GUI manager.
178
- gui_manager_queue: _mp.Queue | None = None
179
- gui_manager_thread: _threading.Thread | None = None
301
+ def __getattr__(self, name: str) -> Any:
302
+ if name in _CONTEXT_ATTRS:
303
+ return getattr(self.__dict__["_ctx"], name)
304
+ raise AttributeError(f"module {self.__name__!r} has no attribute {name!r}")
180
305
 
181
- # ---------------------------------------------------------------------------
182
- # Version numbers (parsed from package __version__)
183
- # ---------------------------------------------------------------------------
306
+ def __setattr__(self, name: str, value: Any) -> None:
307
+ if name in _CONTEXT_ATTRS:
308
+ setattr(self.__dict__["_ctx"], name, value)
309
+ else:
310
+ super().__setattr__(name, value)
184
311
 
185
- try:
186
- from execsql import __version__ as _pkg_version
312
+ def __delattr__(self, name: str) -> None:
313
+ if name in _CONTEXT_ATTRS:
314
+ # Reset to the fresh-context default. Needed for
315
+ # unittest.mock.patch compatibility: patch checks
316
+ # ``name in target.__dict__`` to decide whether to restore
317
+ # via setattr (local) or delattr (non-local). Since context
318
+ # attrs live on _ctx, not __dict__, patch takes the delattr
319
+ # path. We reset to the default rather than truly deleting.
320
+ _defaults = RuntimeContext()
321
+ setattr(self.__dict__["_ctx"], name, getattr(_defaults, name))
322
+ else:
323
+ super().__delattr__(name)
324
+
325
+ def __dir__(self) -> list[str]:
326
+ return sorted(set(super().__dir__()) | _CONTEXT_ATTRS)
187
327
 
188
- _vparts = _pkg_version.split(".")
189
- primary_vno: int = int(_vparts[0]) if len(_vparts) > 0 else 0
190
- secondary_vno: int = int(_vparts[1]) if len(_vparts) > 1 else 0
191
- tertiary_vno: int = int(_vparts[2]) if len(_vparts) > 2 else 0
192
- except Exception:
193
- primary_vno = 0
194
- secondary_vno = 0
195
- tertiary_vno = 0
196
328
 
197
329
  # ---------------------------------------------------------------------------
198
- # Utility functions defined directly here to avoid circular imports.
330
+ # Utility functions use _ctx directly (LOAD_GLOBAL bypasses the proxy)
199
331
  # ---------------------------------------------------------------------------
200
332
 
201
333
 
202
334
  def xcmd_test(teststr: str) -> bool:
203
335
  """Evaluate a conditional test string and return a boolean result."""
204
- import execsql.parser as _parser
205
336
  import execsql.exceptions as _exc
337
+ import execsql.parser as _parser
206
338
 
207
339
  result = _parser.CondParser(teststr).parse().eval()
208
340
  if result is not None:
@@ -214,80 +346,60 @@ def endloop() -> None:
214
346
  """Complete the current loop being compiled and push it onto the command stack."""
215
347
  import execsql.exceptions as _exc
216
348
 
217
- global compiling_loop
218
- if len(loopcommandstack) == 0:
349
+ if len(_ctx.loopcommandstack) == 0:
219
350
  raise _exc.ErrInfo("error", other_msg="END LOOP metacommand without a matching preceding LOOP metacommand.")
220
- compiling_loop = False
221
- commandliststack.append(loopcommandstack[-1])
222
- loopcommandstack.pop()
351
+ _ctx.compiling_loop = False
352
+ _ctx.commandliststack.append(_ctx.loopcommandstack[-1])
353
+ _ctx.loopcommandstack.pop()
223
354
 
224
355
 
225
356
  # ---------------------------------------------------------------------------
226
- # Test-support utilities
357
+ # Context management
358
+ # ---------------------------------------------------------------------------
359
+
360
+
361
+ def get_context() -> RuntimeContext:
362
+ """Return the active :class:`RuntimeContext`."""
363
+ return _ctx
364
+
365
+
366
+ def set_context(ctx: RuntimeContext) -> None:
367
+ """Replace the active :class:`RuntimeContext`.
368
+
369
+ Args:
370
+ ctx: The new context to install. All subsequent ``_state.foo``
371
+ accesses will resolve against this instance.
372
+ """
373
+ global _ctx
374
+ _ctx = ctx
375
+
376
+
377
+ # ---------------------------------------------------------------------------
378
+ # Initialization and reset
227
379
  # ---------------------------------------------------------------------------
228
380
 
229
381
 
230
382
  def reset() -> None:
231
- """Reset all module-level state to initial values.
383
+ """Reset all mutable state to initial values.
232
384
 
233
- Intended for use in tests. Clears mutable containers, resets counters,
234
- and sets all lazy singletons back to ``None`` so that each test starts
235
- from a clean slate.
385
+ Intended for use in tests. Creates a fresh :class:`RuntimeContext`,
386
+ preserving only the ``filewriter`` subprocess (which is ``atexit``-managed
387
+ and must not be discarded while alive).
236
388
  """
237
- global compiling_loop, loop_nest_level, cmds_run
238
- global conf, last_command, upass
239
- global err_halt_writespec, err_halt_email, err_halt_exec
240
- global cancel_halt_writespec, cancel_halt_mailspec, cancel_halt_exec
241
- global exec_log, subvars, status, if_stack, counters, timer
242
- global output, dbs, tempfiles, export_metadata
243
- global metacommandlist, conditionallist, filewriter
244
- global gui_console, gui_manager_queue, gui_manager_thread
245
-
246
- # Mutable containers — clear in-place (no rebind needed)
247
- commandliststack.clear()
248
- loopcommandstack.clear()
249
- savedscripts.clear()
250
-
251
- # Scalar flags and counters
252
- compiling_loop = False
253
- loop_nest_level = 0
254
- cmds_run = 0
389
+ global _ctx
390
+
391
+ # Preserve filewriter — it's atexit-managed and must survive resets.
392
+ old_fw = _ctx.filewriter
255
393
 
256
394
  # Close open database connections before discarding the pool.
257
- if dbs is not None:
395
+ if _ctx.dbs is not None:
258
396
  try:
259
- dbs.closeall()
397
+ _ctx.dbs.closeall()
260
398
  except Exception:
261
399
  pass
262
400
 
263
- # Lazy singletons — reset to None
264
- conf = None
265
- last_command = None
266
- upass = None
267
- err_halt_writespec = None
268
- err_halt_email = None
269
- err_halt_exec = None
270
- cancel_halt_writespec = None
271
- cancel_halt_mailspec = None
272
- cancel_halt_exec = None
273
- exec_log = None
274
- subvars = None
275
- status = None
276
- if_stack = None
277
- counters = None
278
- timer = None
279
- output = None
280
- dbs = None
281
- tempfiles = None
282
- export_metadata = None
283
- metacommandlist = None
284
- conditionallist = None
285
- # filewriter is a multiprocessing.Process managed by atexit — do NOT null
286
- # it here. Nulling it while the subprocess is alive creates two competing
287
- # consumers on the shared fw_input queue, causing test-to-test races.
288
- gui_console = None
289
- gui_manager_queue = None
290
- gui_manager_thread = None
401
+ _ctx = RuntimeContext()
402
+ _ctx.filewriter = old_fw
291
403
 
292
404
 
293
405
  def initialize(
@@ -315,25 +427,26 @@ def initialize(
315
427
  (script path, subprocess queues, local class definitions). Those are
316
428
  assigned directly in ``_run()`` before and after this call.
317
429
  """
318
- global conf, if_stack, counters, timer, dbs, tempfiles
319
- global export_metadata, metacommandlist, conditionallist
320
-
321
- # These names are re-exported at the bottom of this module (after this
322
- # function definition), so they are guaranteed to be available by the time
323
- # initialize() is called from cli._run(). Using the module-level names
324
- # avoids F811 "redefinition of unused name" from local imports.
325
- import execsql.script as _script
326
- import execsql.utils.timer as _timer_mod
327
430
  import execsql.db.base as _db_base
328
- import execsql.utils.fileio as _fileio_mod
329
431
  import execsql.exporters.base as _exporters_base
432
+ import execsql.script as _script
433
+ import execsql.utils.fileio as _fileio_mod
434
+ import execsql.utils.timer as _timer_mod
435
+
436
+ _ctx.conf = config
437
+ _ctx.if_stack = _script.IfLevels()
438
+ _ctx.counters = _script.CounterVars()
439
+ _ctx.timer = _timer_mod.Timer()
440
+ _ctx.dbs = _db_base.DatabasePool()
441
+ _ctx.tempfiles = _fileio_mod.TempFileMgr()
442
+ _ctx.export_metadata = _exporters_base.ExportMetadata()
443
+ _ctx.metacommandlist = dispatch_table
444
+ _ctx.conditionallist = conditional_table
445
+
446
+
447
+ # ---------------------------------------------------------------------------
448
+ # Bootstrap — create the initial context and swap the module class
449
+ # ---------------------------------------------------------------------------
330
450
 
331
- conf = config
332
- if_stack = _script.IfLevels()
333
- counters = _script.CounterVars()
334
- timer = _timer_mod.Timer()
335
- dbs = _db_base.DatabasePool()
336
- tempfiles = _fileio_mod.TempFileMgr()
337
- export_metadata = _exporters_base.ExportMetadata()
338
- metacommandlist = dispatch_table
339
- conditionallist = conditional_table
451
+ _ctx = RuntimeContext()
452
+ sys.modules[__name__].__class__ = _StateModule