execsql2 2.23.1__py3-none-any.whl → 2.25.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.
- execsql/api.py +169 -160
- execsql/cli/__init__.py +19 -586
- execsql/cli/application.py +309 -0
- execsql/cli/commands/__init__.py +13 -0
- execsql/cli/commands/config.py +205 -0
- execsql/cli/commands/format.py +100 -0
- execsql/cli/commands/init.py +135 -0
- execsql/cli/commands/lint.py +225 -0
- execsql/cli/commands/listing.py +81 -0
- execsql/cli/commands/run.py +459 -0
- execsql/cli/dispatch.py +66 -0
- execsql/cli/dsn.py +11 -4
- execsql/cli/help.py +175 -24
- execsql/cli/lint.py +780 -100
- execsql/cli/options.py +108 -0
- execsql/cli/run.py +96 -32
- execsql/config.py +256 -173
- execsql/config_validation.py +282 -0
- execsql/data/execsql.conf.template +38 -5
- execsql/data/script_header.sql +23 -0
- execsql/db/access.py +29 -8
- execsql/db/base.py +40 -8
- execsql/db/dsn.py +13 -4
- execsql/db/duckdb.py +3 -1
- execsql/db/factory.py +13 -4
- execsql/db/firebird.py +21 -7
- execsql/db/mysql.py +37 -16
- execsql/db/oracle.py +49 -16
- execsql/db/postgres.py +30 -6
- execsql/db/sqlite.py +7 -9
- execsql/db/sqlserver.py +25 -8
- execsql/debug/repl.py +51 -193
- execsql/exceptions.py +9 -0
- execsql/exporters/delimited.py +87 -28
- execsql/exporters/xlsx.py +24 -9
- execsql/exporters/zip.py +1 -2
- execsql/format.py +115 -76
- execsql/gui/desktop.py +1 -3
- execsql/gui/tui.py +2 -2
- execsql/importers/base.py +35 -1
- execsql/importers/csv.py +3 -1
- execsql/interactive.py +232 -0
- execsql/manifest.py +200 -0
- execsql/metacommands/conditions.py +7 -8
- execsql/metacommands/control.py +2 -3
- execsql/metacommands/data.py +4 -1
- execsql/metacommands/debug.py +2 -4
- execsql/metacommands/effects.py +124 -0
- execsql/metacommands/io_fileops.py +62 -37
- execsql/metacommands/io_import.py +0 -1
- execsql/metacommands/prompt.py +2 -0
- execsql/metacommands/upsert.py +5 -2
- execsql/models.py +3 -2
- execsql/parser.py +14 -5
- execsql/plugins.py +24 -0
- execsql/script/__init__.py +0 -9
- execsql/script/ast.py +1 -1
- execsql/script/engine.py +46 -104
- execsql/script/executor.py +162 -39
- execsql/script/parser.py +167 -118
- execsql/state.py +34 -11
- execsql/state.pyi +11 -0
- execsql/types.py +22 -13
- execsql/utils/auth.py +169 -24
- execsql/utils/color.py +18 -0
- execsql/utils/datetime.py +55 -17
- execsql/utils/errors.py +82 -10
- execsql/utils/fileio.py +404 -117
- execsql/utils/gui.py +3 -0
- execsql/utils/mail.py +37 -12
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/README.md +1 -1
- {execsql2-2.23.1.dist-info → execsql2-2.25.0.dist-info}/METADATA +122 -63
- execsql2-2.25.0.dist-info/RECORD +140 -0
- execsql2-2.25.0.dist-info/entry_points.txt +2 -0
- execsql2-2.23.1.dist-info/RECORD +0 -124
- execsql2-2.23.1.dist-info/entry_points.txt +0 -3
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.23.1.data → execsql2-2.25.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.23.1.dist-info → execsql2-2.25.0.dist-info}/WHEEL +0 -0
- {execsql2-2.23.1.dist-info → execsql2-2.25.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.23.1.dist-info → execsql2-2.25.0.dist-info}/licenses/NOTICE +0 -0
execsql/script/executor.py
CHANGED
|
@@ -48,6 +48,7 @@ import re
|
|
|
48
48
|
import time as _time
|
|
49
49
|
from pathlib import Path
|
|
50
50
|
from typing import Any, cast
|
|
51
|
+
from collections.abc import Callable
|
|
51
52
|
|
|
52
53
|
from execsql.exceptions import ErrInfo
|
|
53
54
|
from execsql.script.ast import (
|
|
@@ -64,12 +65,13 @@ from execsql.script.ast import (
|
|
|
64
65
|
SqlBlock,
|
|
65
66
|
SqlStatement,
|
|
66
67
|
)
|
|
68
|
+
from execsql import manifest as _manifest
|
|
67
69
|
from execsql.script.engine import set_dynamic_system_vars, set_static_system_vars, substitute_vars
|
|
68
70
|
from execsql.script.variables import SubVarSet
|
|
69
71
|
from execsql.state import ExecFrame, RuntimeContext, active_context, get_context, xcmd_test
|
|
70
|
-
from execsql.utils.errors import exception_desc, exit_now, stamp_errinfo
|
|
72
|
+
from execsql.utils.errors import exception_desc, exit_now, logged_error, stamp_errinfo
|
|
71
73
|
|
|
72
|
-
__all__ = ["execute"]
|
|
74
|
+
__all__ = ["execute", "execute_input"]
|
|
73
75
|
|
|
74
76
|
|
|
75
77
|
# ---------------------------------------------------------------------------
|
|
@@ -252,35 +254,44 @@ def _exec_sql(
|
|
|
252
254
|
effective_locals = _stack_localvars(ctx) or localvars
|
|
253
255
|
cmd = substitute_vars(text, effective_locals, ctx=ctx)
|
|
254
256
|
if _VARLIKE.search(cmd):
|
|
257
|
+
from execsql.utils.fileio import redact
|
|
258
|
+
|
|
255
259
|
output.write(
|
|
256
|
-
f"Warning: There is a potential un-substituted variable in the command\n {cmd}\n",
|
|
260
|
+
f"Warning: There is a potential un-substituted variable in the command\n {redact(cmd)}\n",
|
|
257
261
|
)
|
|
258
262
|
e = None
|
|
263
|
+
result = None
|
|
259
264
|
try:
|
|
260
265
|
db = dbs.current()
|
|
261
266
|
if conf.log_sql and ctx.exec_log:
|
|
262
267
|
ctx.exec_log.log_sql_query(cmd, db.name(), line_no)
|
|
263
|
-
|
|
268
|
+
if ctx.prompt_input is not None:
|
|
269
|
+
result = db.execute(cmd, fetch=True)
|
|
270
|
+
else:
|
|
271
|
+
db.execute(cmd)
|
|
264
272
|
if commit:
|
|
265
273
|
db.commit()
|
|
266
274
|
except ErrInfo as errinfo:
|
|
267
275
|
e = errinfo
|
|
268
276
|
except SystemExit:
|
|
269
277
|
raise
|
|
270
|
-
except Exception:
|
|
278
|
+
except Exception as exc:
|
|
271
279
|
e = ErrInfo(type="exception", exception_msg=exception_desc())
|
|
280
|
+
e.__cause__ = exc # the driver's own message, which a prompt shows
|
|
272
281
|
if e:
|
|
273
282
|
stamp_errinfo(e)
|
|
274
283
|
subvars.add_substitution("$LAST_ERROR", cmd)
|
|
275
284
|
subvars.add_substitution("$ERROR_MESSAGE", e.errmsg())
|
|
276
285
|
status.sql_error = True
|
|
277
286
|
if ctx.exec_log is not None:
|
|
278
|
-
ctx.exec_log.log_status_info(f"SQL error: {e
|
|
287
|
+
ctx.exec_log.log_status_info(f"SQL error: {logged_error(e)}")
|
|
279
288
|
if status.halt_on_err:
|
|
280
289
|
exit_now(1, e)
|
|
281
290
|
status.error_history.append((source, line_no, cmd, e.errmsg()))
|
|
282
291
|
return
|
|
283
292
|
subvars.add_substitution("$LAST_SQL", cmd)
|
|
293
|
+
if ctx.prompt_input is not None:
|
|
294
|
+
ctx.prompt_input(result)
|
|
284
295
|
|
|
285
296
|
|
|
286
297
|
# ---------------------------------------------------------------------------
|
|
@@ -312,27 +323,32 @@ def _exec_metacommand(
|
|
|
312
323
|
assert subvars is not None
|
|
313
324
|
|
|
314
325
|
if _VARLIKE.search(cmd):
|
|
326
|
+
from execsql.utils.fileio import redact
|
|
327
|
+
|
|
315
328
|
output.write(
|
|
316
|
-
f"Warning: There is a potential un-substituted variable in the command\n {cmd}\n",
|
|
329
|
+
f"Warning: There is a potential un-substituted variable in the command\n {redact(cmd)}\n",
|
|
317
330
|
)
|
|
318
331
|
e = None
|
|
319
332
|
try:
|
|
320
333
|
applies, result = metacommandlist.eval(cmd)
|
|
321
334
|
if applies:
|
|
335
|
+
if (manifest := _manifest.current()) is not None:
|
|
336
|
+
manifest.record_metacommand(cmd, source, line_no, metacommandlist)
|
|
322
337
|
return result
|
|
323
338
|
except ErrInfo as errinfo:
|
|
324
339
|
e = errinfo
|
|
325
340
|
except SystemExit:
|
|
326
341
|
raise
|
|
327
|
-
except Exception:
|
|
342
|
+
except Exception as exc:
|
|
328
343
|
e = ErrInfo(type="exception", exception_msg=exception_desc())
|
|
344
|
+
e.__cause__ = exc
|
|
329
345
|
if e:
|
|
330
346
|
stamp_errinfo(e)
|
|
331
347
|
status.metacommand_error = True
|
|
332
348
|
subvars.add_substitution("$LAST_ERROR", cmd)
|
|
333
349
|
subvars.add_substitution("$ERROR_MESSAGE", e.errmsg())
|
|
334
350
|
if ctx.exec_log is not None:
|
|
335
|
-
ctx.exec_log.log_status_info(f"Metacommand error: {e
|
|
351
|
+
ctx.exec_log.log_status_info(f"Metacommand error: {logged_error(e)}")
|
|
336
352
|
if status.halt_on_metacommand_err:
|
|
337
353
|
raise e
|
|
338
354
|
status.error_history.append((source, line_no, cmd, e.errmsg()))
|
|
@@ -402,9 +418,9 @@ def _execute_node(
|
|
|
402
418
|
text = node.text
|
|
403
419
|
if in_loop:
|
|
404
420
|
text = _convert_deferred_vars(text)
|
|
405
|
-
# Deduplicate trailing semicolons
|
|
421
|
+
# Deduplicate trailing semicolons
|
|
406
422
|
text = re.sub(r"\s*;(\s*;\s*)+$", ";", text)
|
|
407
|
-
ctx.last_command =
|
|
423
|
+
ctx.last_command = ExecutingStatement(node)
|
|
408
424
|
_exec_sql(
|
|
409
425
|
ctx,
|
|
410
426
|
text,
|
|
@@ -413,6 +429,8 @@ def _execute_node(
|
|
|
413
429
|
localvars,
|
|
414
430
|
commit=not ctx.status.batch.in_batch(),
|
|
415
431
|
)
|
|
432
|
+
if (manifest := _manifest.current()) is not None:
|
|
433
|
+
manifest.record_sql()
|
|
416
434
|
|
|
417
435
|
elif isinstance(node, MetaCommandStatement):
|
|
418
436
|
command = node.command
|
|
@@ -425,31 +443,31 @@ def _execute_node(
|
|
|
425
443
|
expanded = substitute_vars(command, effective_locals, ctx=ctx)
|
|
426
444
|
if _BREAK_RX.match(expanded):
|
|
427
445
|
raise _BreakLoop
|
|
428
|
-
ctx.last_command =
|
|
446
|
+
ctx.last_command = ExecutingStatement(node)
|
|
429
447
|
_exec_metacommand(ctx, expanded, node.span.file, node.span.start_line)
|
|
430
448
|
|
|
431
449
|
elif isinstance(node, IfBlock):
|
|
432
|
-
ctx.last_command =
|
|
450
|
+
ctx.last_command = ExecutingStatement(node)
|
|
433
451
|
_execute_if(ctx, node, localvars, in_loop=in_loop)
|
|
434
452
|
|
|
435
453
|
elif isinstance(node, LoopBlock):
|
|
436
|
-
ctx.last_command =
|
|
454
|
+
ctx.last_command = ExecutingStatement(node)
|
|
437
455
|
_execute_loop(ctx, node, localvars)
|
|
438
456
|
|
|
439
457
|
elif isinstance(node, BatchBlock):
|
|
440
|
-
ctx.last_command =
|
|
458
|
+
ctx.last_command = ExecutingStatement(node)
|
|
441
459
|
_execute_batch(ctx, node, localvars, in_loop=in_loop)
|
|
442
460
|
|
|
443
461
|
elif isinstance(node, ScriptBlock):
|
|
444
|
-
ctx.last_command =
|
|
462
|
+
ctx.last_command = ExecutingStatement(node)
|
|
445
463
|
_register_script_block(ctx, node)
|
|
446
464
|
|
|
447
465
|
elif isinstance(node, SqlBlock):
|
|
448
|
-
ctx.last_command =
|
|
466
|
+
ctx.last_command = ExecutingStatement(node)
|
|
449
467
|
_execute_sql_block(ctx, node, localvars, in_loop=in_loop)
|
|
450
468
|
|
|
451
469
|
elif isinstance(node, IncludeDirective):
|
|
452
|
-
ctx.last_command =
|
|
470
|
+
ctx.last_command = ExecutingStatement(node)
|
|
453
471
|
_execute_include(ctx, node, localvars)
|
|
454
472
|
|
|
455
473
|
else:
|
|
@@ -861,6 +879,8 @@ def _execute_include_native(
|
|
|
861
879
|
# Parse with AST parser
|
|
862
880
|
encoding = ctx.conf.script_encoding if ctx.conf is not None else "utf-8"
|
|
863
881
|
included_tree = parse_script(target, encoding=encoding)
|
|
882
|
+
if (manifest := _manifest.current()) is not None:
|
|
883
|
+
manifest.record_include(target, node.span.file, node.span.start_line)
|
|
864
884
|
|
|
865
885
|
# Pre-register SCRIPT blocks in the included file so forward references work.
|
|
866
886
|
_pre_register_scripts(ctx, included_tree.body)
|
|
@@ -885,40 +905,104 @@ def _execute_include_native(
|
|
|
885
905
|
# ---------------------------------------------------------------------------
|
|
886
906
|
|
|
887
907
|
|
|
888
|
-
class _BreakLoop(
|
|
889
|
-
"""Raised by BREAK metacommand to exit the innermost loop.
|
|
908
|
+
class _BreakLoop(BaseException):
|
|
909
|
+
"""Raised by BREAK metacommand to exit the innermost loop.
|
|
910
|
+
|
|
911
|
+
A ``BaseException``, like ``KeyboardInterrupt``, so the ``except
|
|
912
|
+
Exception`` handlers it may pass on its way to the loop leave it alone:
|
|
913
|
+
a BREAK typed at a BREAKPOINT prompt travels out through the BREAKPOINT
|
|
914
|
+
metacommand's handler.
|
|
915
|
+
"""
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
def _inside_loop(ctx: RuntimeContext) -> bool:
|
|
919
|
+
"""Whether a BREAK at the current statement would leave a loop within its scope."""
|
|
920
|
+
for frame in reversed(ctx.ast_exec_stack):
|
|
921
|
+
if frame.kind in ("loop_while", "loop_until"):
|
|
922
|
+
return True
|
|
923
|
+
if frame.kind in ("script", "main"):
|
|
924
|
+
return frame.kind == "script" and frame.iteration > 0
|
|
925
|
+
return False
|
|
890
926
|
|
|
891
927
|
|
|
892
928
|
_BREAK_RX = re.compile(r"^\s*BREAK\s*$", re.I)
|
|
893
929
|
|
|
894
930
|
|
|
895
931
|
# ---------------------------------------------------------------------------
|
|
896
|
-
#
|
|
932
|
+
# The statement the executor is currently on
|
|
897
933
|
# ---------------------------------------------------------------------------
|
|
898
934
|
|
|
899
935
|
|
|
900
|
-
class
|
|
901
|
-
"""
|
|
936
|
+
class _StatementText:
|
|
937
|
+
"""The text of one statement, in the two spellings its readers expect.
|
|
938
|
+
|
|
939
|
+
``statement`` is the raw text; ``commandline()`` is the text as it
|
|
940
|
+
appeared in the source, so a metacommand gets its ``-- !x!`` marker back.
|
|
941
|
+
"""
|
|
942
|
+
|
|
943
|
+
__slots__ = ("statement", "_prefix")
|
|
902
944
|
|
|
903
|
-
|
|
945
|
+
def __init__(self, statement: str, prefix: str = "") -> None:
|
|
946
|
+
self.statement = statement
|
|
947
|
+
self._prefix = prefix
|
|
948
|
+
|
|
949
|
+
def commandline(self) -> str:
|
|
950
|
+
return self._prefix + self.statement
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
class ExecutingStatement:
|
|
954
|
+
"""Where the executor is, as ``ctx.last_command`` exposes it.
|
|
955
|
+
|
|
956
|
+
The AST node is the single representation of a statement. This wraps one
|
|
957
|
+
rather than copying fields out of it, so there is nothing to keep in sync
|
|
958
|
+
and no second source of truth about what is executing. Error reporting
|
|
959
|
+
(:mod:`execsql.utils.errors`), the debug REPL, and ``api.run()`` read
|
|
960
|
+
these five attributes; everything is derived from ``node`` on access, so
|
|
961
|
+
constructing one costs a single small object per statement rather than
|
|
962
|
+
the two classes the previous shim built each time.
|
|
963
|
+
"""
|
|
964
|
+
|
|
965
|
+
__slots__ = ("node", "_source_dir")
|
|
904
966
|
|
|
905
967
|
def __init__(self, node: Node) -> None:
|
|
906
|
-
self.
|
|
907
|
-
self.
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
self.
|
|
968
|
+
self.node = node
|
|
969
|
+
self._source_dir: str | None = None
|
|
970
|
+
|
|
971
|
+
@property
|
|
972
|
+
def source(self) -> str:
|
|
973
|
+
return self.node.span.file
|
|
974
|
+
|
|
975
|
+
@property
|
|
976
|
+
def line_no(self) -> int:
|
|
977
|
+
return self.node.span.start_line
|
|
978
|
+
|
|
979
|
+
@property
|
|
980
|
+
def source_name(self) -> str:
|
|
981
|
+
return Path(self.node.span.file).name
|
|
982
|
+
|
|
983
|
+
@property
|
|
984
|
+
def source_dir(self) -> str:
|
|
985
|
+
# resolve() touches the filesystem, so it is done once per statement
|
|
986
|
+
# object rather than on every read.
|
|
987
|
+
if self._source_dir is None:
|
|
988
|
+
self._source_dir = str(Path(self.node.span.file).resolve().parent) + os.sep
|
|
989
|
+
return self._source_dir
|
|
990
|
+
|
|
991
|
+
@property
|
|
992
|
+
def command_type(self) -> str:
|
|
993
|
+
return "sql" if isinstance(self.node, SqlStatement) else "cmd"
|
|
994
|
+
|
|
995
|
+
@property
|
|
996
|
+
def command(self) -> _StatementText:
|
|
997
|
+
node = self.node
|
|
912
998
|
if isinstance(node, SqlStatement):
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
else:
|
|
921
|
-
self.command = type("_cmd", (), {"statement": "", "commandline": lambda self: ""})()
|
|
999
|
+
return _StatementText(node.text)
|
|
1000
|
+
if isinstance(node, MetaCommandStatement):
|
|
1001
|
+
return _StatementText(node.command, "-- !x! ")
|
|
1002
|
+
return _StatementText("")
|
|
1003
|
+
|
|
1004
|
+
def __repr__(self) -> str:
|
|
1005
|
+
return f"ExecutingStatement({self.source}:{self.line_no}, {self.command_type})"
|
|
922
1006
|
|
|
923
1007
|
def current_script_line(self) -> tuple[str, int]:
|
|
924
1008
|
return (self.source, self.line_no)
|
|
@@ -1008,3 +1092,42 @@ def execute(script: Script, *, ctx: RuntimeContext | None = None) -> None:
|
|
|
1008
1092
|
) from exc
|
|
1009
1093
|
finally:
|
|
1010
1094
|
_pop_frame(ctx)
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
# ---------------------------------------------------------------------------
|
|
1098
|
+
# Input typed at a prompt
|
|
1099
|
+
# ---------------------------------------------------------------------------
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
def execute_input(
|
|
1103
|
+
script: Script,
|
|
1104
|
+
show: Callable[[tuple[list[str], list] | None], None],
|
|
1105
|
+
*,
|
|
1106
|
+
ctx: RuntimeContext | None = None,
|
|
1107
|
+
) -> None:
|
|
1108
|
+
"""Run input typed at a BREAKPOINT prompt as the next lines of the paused script.
|
|
1109
|
+
|
|
1110
|
+
The input runs as if it were written where the script paused: in its
|
|
1111
|
+
variable scope, its batch and its loop, so a BREAK leaves that loop.
|
|
1112
|
+
Unlike :func:`execute`, nothing is reset.
|
|
1113
|
+
|
|
1114
|
+
*show* receives the result of each SQL statement run. An error that
|
|
1115
|
+
would halt the run is raised to the caller instead (see ``exit_now``).
|
|
1116
|
+
The paused script's current statement is restored afterwards, so the
|
|
1117
|
+
debug REPL's ``.where`` still shows it.
|
|
1118
|
+
"""
|
|
1119
|
+
if ctx is None:
|
|
1120
|
+
ctx = get_context()
|
|
1121
|
+
with active_context(ctx):
|
|
1122
|
+
_pre_register_scripts(ctx, script.body)
|
|
1123
|
+
last_command, prompt_input = ctx.last_command, ctx.prompt_input
|
|
1124
|
+
ctx.prompt_input = show
|
|
1125
|
+
try:
|
|
1126
|
+
_execute_nodes(ctx, script.body, script.source)
|
|
1127
|
+
except _BreakLoop as exc:
|
|
1128
|
+
if not _inside_loop(ctx):
|
|
1129
|
+
raise ErrInfo(type="cmd", other_msg="BREAK metacommand outside of a LOOP block.") from exc
|
|
1130
|
+
raise
|
|
1131
|
+
finally:
|
|
1132
|
+
ctx.prompt_input = prompt_input
|
|
1133
|
+
ctx.last_command = last_command
|