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.
- execsql/cli/__init__.py +14 -0
- execsql/cli/dsn.py +2 -0
- execsql/cli/help.py +2 -0
- execsql/cli/run.py +4 -2
- execsql/constants.py +11 -0
- execsql/db/access.py +20 -0
- execsql/db/base.py +4 -0
- execsql/db/dsn.py +11 -8
- execsql/db/duckdb.py +12 -8
- execsql/db/firebird.py +17 -8
- execsql/db/mysql.py +13 -8
- execsql/db/oracle.py +22 -8
- execsql/db/postgres.py +21 -9
- execsql/db/sqlite.py +18 -8
- execsql/db/sqlserver.py +14 -8
- execsql/exporters/__init__.py +6 -1
- execsql/exporters/base.py +2 -0
- execsql/exporters/delimited.py +10 -0
- execsql/exporters/protocol.py +128 -0
- execsql/exporters/xls.py +8 -0
- execsql/format.py +3 -1
- execsql/gui/__init__.py +2 -0
- execsql/gui/base.py +2 -0
- execsql/gui/console.py +2 -0
- execsql/gui/desktop.py +1 -0
- execsql/gui/tui.py +134 -0
- execsql/importers/base.py +1 -0
- execsql/importers/csv.py +2 -0
- execsql/importers/feather.py +2 -0
- execsql/importers/ods.py +1 -0
- execsql/importers/xls.py +1 -0
- execsql/metacommands/__init__.py +386 -180
- execsql/metacommands/dispatch.py +2 -0
- execsql/metacommands/io.py +41 -0
- execsql/models.py +17 -0
- execsql/parser.py +41 -0
- execsql/script/control.py +2 -0
- execsql/script/engine.py +19 -0
- execsql/script/variables.py +9 -5
- execsql/state.py +312 -199
- execsql/types.py +46 -0
- {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/METADATA +2 -2
- {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/RECORD +62 -61
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/execsql.conf +0 -0
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.4.5.data → execsql2-2.6.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/WHEEL +0 -0
- {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/entry_points.txt +0 -0
- {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.4.5.dist-info → execsql2-2.6.0.dist-info}/licenses/NOTICE +0 -0
execsql/models.py
CHANGED
|
@@ -49,13 +49,18 @@ __all__ = [
|
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
class Column:
|
|
52
|
+
"""Compile data-type match statistics for a single column of imported data."""
|
|
53
|
+
|
|
52
54
|
# Column objects are used to compile information about the data types that a set of data
|
|
53
55
|
# values may match. A Column object is intended to be used to identify the data type of a column
|
|
54
56
|
# when scanning a data stream (such as a CSV file) to create a new data table.
|
|
55
57
|
|
|
56
58
|
class Accum:
|
|
59
|
+
"""Accumulate match counts and length statistics for a single data type."""
|
|
60
|
+
|
|
57
61
|
# Accumulates the count of matches for each data type, plus the maximum length if appropriate.
|
|
58
62
|
def __init__(self, data_type_obj: DataType) -> None:
|
|
63
|
+
"""Initialise the accumulator for the given data type."""
|
|
59
64
|
self.dt = data_type_obj
|
|
60
65
|
self.failed = False
|
|
61
66
|
self.count = 0
|
|
@@ -72,6 +77,7 @@ class Column:
|
|
|
72
77
|
)
|
|
73
78
|
|
|
74
79
|
def check(self, datavalue: Any) -> None:
|
|
80
|
+
"""Test whether a non-null value matches this data type and update statistics."""
|
|
75
81
|
# datavalue must be non-null
|
|
76
82
|
if not self.failed:
|
|
77
83
|
is_match = self.dt.matches(datavalue)
|
|
@@ -105,6 +111,7 @@ class Column:
|
|
|
105
111
|
self.failed = True
|
|
106
112
|
|
|
107
113
|
def __init__(self, colname: str) -> None:
|
|
114
|
+
"""Create a column characteriser for the named column."""
|
|
108
115
|
from execsql.exceptions import ErrInfo
|
|
109
116
|
import execsql.state as _state
|
|
110
117
|
|
|
@@ -150,6 +157,7 @@ class Column:
|
|
|
150
157
|
return f"Column({self.name!r})"
|
|
151
158
|
|
|
152
159
|
def eval_types(self, column_value: Any) -> None:
|
|
160
|
+
"""Evaluate which data types the value matches and update counters."""
|
|
153
161
|
# Evaluate which data type(s) the value matches, and increment the appropriate counter(s).
|
|
154
162
|
import execsql.state as _state
|
|
155
163
|
|
|
@@ -170,6 +178,7 @@ class Column:
|
|
|
170
178
|
dt.check(column_value)
|
|
171
179
|
|
|
172
180
|
def column_type(self) -> tuple:
|
|
181
|
+
"""Return the inferred type of this column as a 6-tuple."""
|
|
173
182
|
# Return the type of this column as a tuple of:
|
|
174
183
|
# column name, data type class, max length or None, bool for null values,
|
|
175
184
|
# precision or None, scale or None.
|
|
@@ -212,7 +221,10 @@ class Column:
|
|
|
212
221
|
|
|
213
222
|
|
|
214
223
|
class DataTable:
|
|
224
|
+
"""Scan a row source and infer column types for CREATE TABLE generation."""
|
|
225
|
+
|
|
215
226
|
def __init__(self, column_names: list[str], rowsource: Any) -> None:
|
|
227
|
+
"""Scan all rows from the source and infer a column type for each column."""
|
|
216
228
|
import execsql.state as _state
|
|
217
229
|
|
|
218
230
|
self.inputrows = 0 # Total number of rows in the row source.
|
|
@@ -264,6 +276,7 @@ class DataTable:
|
|
|
264
276
|
return f"DataTable({[col.name for col in self.cols]!r}, rowsource)"
|
|
265
277
|
|
|
266
278
|
def column_declarations(self, database_type: DbType) -> list[str]:
|
|
279
|
+
"""Return a list of SQL column-declaration strings for the given DBMS."""
|
|
267
280
|
# Returns a list of column specifications.
|
|
268
281
|
spec = []
|
|
269
282
|
for col in self.cols:
|
|
@@ -277,6 +290,7 @@ class DataTable:
|
|
|
277
290
|
tablename: str,
|
|
278
291
|
pretty: bool = False,
|
|
279
292
|
) -> str:
|
|
293
|
+
"""Generate a CREATE TABLE statement for the given DBMS and table name."""
|
|
280
294
|
tb = (
|
|
281
295
|
f"{database_type.quoted(schemaname)}.{database_type.quoted(tablename)}"
|
|
282
296
|
if schemaname
|
|
@@ -292,7 +306,10 @@ class DataTable:
|
|
|
292
306
|
|
|
293
307
|
|
|
294
308
|
class JsonDatatype:
|
|
309
|
+
"""Namespace mapping Python DataType subclasses to JSON Schema type strings."""
|
|
310
|
+
|
|
295
311
|
def __init__(self) -> None:
|
|
312
|
+
"""Create an empty JsonDatatype namespace instance."""
|
|
296
313
|
pass
|
|
297
314
|
|
|
298
315
|
|
execsql/parser.py
CHANGED
|
@@ -39,20 +39,26 @@ __all__ = [
|
|
|
39
39
|
|
|
40
40
|
|
|
41
41
|
class SourceString:
|
|
42
|
+
"""Cursor-based scanner over a raw string for token matching."""
|
|
43
|
+
|
|
42
44
|
def __init__(self, source_string: str) -> None:
|
|
45
|
+
"""Initialise the scanner at position zero of the given string."""
|
|
43
46
|
self.str = source_string
|
|
44
47
|
self.currpos = 0
|
|
45
48
|
|
|
46
49
|
def eoi(self) -> bool:
|
|
50
|
+
"""Return True if the entire source string has been consumed."""
|
|
47
51
|
# Returns True or False indicating whether or not there is any of
|
|
48
52
|
# the source string left to be consumed.
|
|
49
53
|
return self.currpos >= len(self.str)
|
|
50
54
|
|
|
51
55
|
def eat_whitespace(self) -> None:
|
|
56
|
+
"""Advance the cursor past any whitespace at the current position."""
|
|
52
57
|
while not self.eoi() and self.str[self.currpos] in [" ", "\t", "\n"]:
|
|
53
58
|
self.currpos += 1
|
|
54
59
|
|
|
55
60
|
def match_str(self, str: str) -> str | None:
|
|
61
|
+
"""Match a string case-insensitively at the current position and advance."""
|
|
56
62
|
# Tries to match the 'str' argument at the current position in the
|
|
57
63
|
# source string. Matching is case-insensitive. If matching succeeds,
|
|
58
64
|
# the matched string is returned and the internal pointer is incremented.
|
|
@@ -70,6 +76,7 @@ class SourceString:
|
|
|
70
76
|
return None
|
|
71
77
|
|
|
72
78
|
def match_regex(self, regex: Any) -> dict | None:
|
|
79
|
+
"""Match a compiled regex at the current position and return named groups."""
|
|
73
80
|
# Tries to match the 'regex' argument at the current position in the
|
|
74
81
|
# source string. If it succeeds, a dictionary of all of the named
|
|
75
82
|
# groups is returned, and the internal pointer is incremented.
|
|
@@ -85,6 +92,7 @@ class SourceString:
|
|
|
85
92
|
return None
|
|
86
93
|
|
|
87
94
|
def match_metacommand(self, commandlist: Any) -> tuple | None:
|
|
95
|
+
"""Match a metacommand from the command list at the current position."""
|
|
88
96
|
# Tries to match text at the current position to any metacommand
|
|
89
97
|
# in the specified commandlist.
|
|
90
98
|
# If it succeeds, the return value is a tuple of the MetaCommand object
|
|
@@ -102,6 +110,7 @@ class SourceString:
|
|
|
102
110
|
return None
|
|
103
111
|
|
|
104
112
|
def remainder(self) -> str:
|
|
113
|
+
"""Return the unconsumed portion of the source string."""
|
|
105
114
|
return self.str[self.currpos :]
|
|
106
115
|
|
|
107
116
|
|
|
@@ -109,10 +118,14 @@ class SourceString:
|
|
|
109
118
|
|
|
110
119
|
|
|
111
120
|
class CondTokens:
|
|
121
|
+
"""Integer constants for conditional-expression AST node types."""
|
|
122
|
+
|
|
112
123
|
AND, OR, NOT, CONDITIONAL = range(4)
|
|
113
124
|
|
|
114
125
|
|
|
115
126
|
class NumTokens:
|
|
127
|
+
"""Integer constants for numeric-expression AST node types."""
|
|
128
|
+
|
|
116
129
|
MUL, DIV, ADD, SUB, NUMBER = range(5)
|
|
117
130
|
|
|
118
131
|
|
|
@@ -120,7 +133,10 @@ class NumTokens:
|
|
|
120
133
|
|
|
121
134
|
|
|
122
135
|
class CondAstNode(CondTokens):
|
|
136
|
+
"""AST node for boolean expressions supporting AND, OR, NOT, and leaf conditionals."""
|
|
137
|
+
|
|
123
138
|
def __init__(self, type: int, cond1: Any, cond2: Any) -> None:
|
|
139
|
+
"""Create a conditional AST node with the given operator type and children."""
|
|
124
140
|
# 'type' should be one of the constants AND, OR, NOT, CONDITIONAL.
|
|
125
141
|
# For AND and OR types, 'cond1' and 'cond2' should be a subtree (a CondAstNode)
|
|
126
142
|
# For NOT type, 'cond1' should be a CondAstNode and 'cond2' should be None
|
|
@@ -134,6 +150,7 @@ class CondAstNode(CondTokens):
|
|
|
134
150
|
self.right = None
|
|
135
151
|
|
|
136
152
|
def eval(self) -> bool:
|
|
153
|
+
"""Evaluate this subtree and return a boolean result."""
|
|
137
154
|
# Evaluates the subtrees and/or conditional value for this node,
|
|
138
155
|
# returning True or False.
|
|
139
156
|
if self.type == self.CONDITIONAL:
|
|
@@ -157,7 +174,10 @@ class CondAstNode(CondTokens):
|
|
|
157
174
|
|
|
158
175
|
|
|
159
176
|
class NumericAstNode(NumTokens):
|
|
177
|
+
"""AST node for arithmetic expressions supporting MUL, DIV, ADD, SUB, and NUMBER."""
|
|
178
|
+
|
|
160
179
|
def __init__(self, type: int, value1: Any, value2: Any) -> None:
|
|
180
|
+
"""Create a numeric AST node with the given operator type and operands."""
|
|
161
181
|
# 'type' should be one of the constants MUL, DIV, ADD, SUB, OR NUMBER.
|
|
162
182
|
# 'value1' and 'value2' should each be either a subtree (a
|
|
163
183
|
# NumericAstNode) or (only 'value1' should be) a number.
|
|
@@ -169,6 +189,7 @@ class NumericAstNode(NumTokens):
|
|
|
169
189
|
self.right = None
|
|
170
190
|
|
|
171
191
|
def eval(self) -> Any:
|
|
192
|
+
"""Evaluate this subtree and return a numeric result."""
|
|
172
193
|
# Evaluates the subtrees and/or numeric value for this node,
|
|
173
194
|
# returning a numeric value.
|
|
174
195
|
if self.type == self.NUMBER:
|
|
@@ -190,12 +211,16 @@ class NumericAstNode(NumTokens):
|
|
|
190
211
|
|
|
191
212
|
|
|
192
213
|
class CondParser(CondTokens):
|
|
214
|
+
"""Recursive-descent parser for boolean conditional expressions."""
|
|
215
|
+
|
|
193
216
|
# Takes a conditional expression string.
|
|
194
217
|
def __init__(self, condexpr: str) -> None:
|
|
218
|
+
"""Initialise the parser with the conditional expression string."""
|
|
195
219
|
self.condexpr = condexpr
|
|
196
220
|
self.cond_expr = SourceString(condexpr)
|
|
197
221
|
|
|
198
222
|
def match_not(self) -> int | None:
|
|
223
|
+
"""Match a NOT operator and return its token type, or None."""
|
|
199
224
|
# Try to match 'NOT' operator. If not found, return None
|
|
200
225
|
m1 = self.cond_expr.match_str("NOT")
|
|
201
226
|
if m1 is not None:
|
|
@@ -203,6 +228,7 @@ class CondParser(CondTokens):
|
|
|
203
228
|
return None
|
|
204
229
|
|
|
205
230
|
def match_andop(self) -> int | None:
|
|
231
|
+
"""Match an AND operator and return its token type, or None."""
|
|
206
232
|
# Try to match 'AND' operator. If not found, return None
|
|
207
233
|
m1 = self.cond_expr.match_str("AND")
|
|
208
234
|
if m1 is not None:
|
|
@@ -210,6 +236,7 @@ class CondParser(CondTokens):
|
|
|
210
236
|
return None
|
|
211
237
|
|
|
212
238
|
def match_orop(self) -> int | None:
|
|
239
|
+
"""Match an OR operator and return its token type, or None."""
|
|
213
240
|
# Try to match 'OR' operator. If not found, return None
|
|
214
241
|
m1 = self.cond_expr.match_str("OR")
|
|
215
242
|
if m1 is not None:
|
|
@@ -217,6 +244,7 @@ class CondParser(CondTokens):
|
|
|
217
244
|
return None
|
|
218
245
|
|
|
219
246
|
def factor(self) -> Any:
|
|
247
|
+
"""Parse a factor: NOT, a parenthesised expression, or a metacommand leaf."""
|
|
220
248
|
m1 = self.match_not()
|
|
221
249
|
if m1 is not None:
|
|
222
250
|
m1 = self.factor()
|
|
@@ -244,6 +272,7 @@ class CondParser(CondTokens):
|
|
|
244
272
|
)
|
|
245
273
|
|
|
246
274
|
def term(self) -> Any:
|
|
275
|
+
"""Parse a term: a factor optionally followed by AND and another term."""
|
|
247
276
|
m1 = self.factor()
|
|
248
277
|
andop = self.match_andop()
|
|
249
278
|
if andop is not None:
|
|
@@ -253,6 +282,7 @@ class CondParser(CondTokens):
|
|
|
253
282
|
return m1
|
|
254
283
|
|
|
255
284
|
def expression(self) -> Any:
|
|
285
|
+
"""Parse an expression: a term optionally followed by OR and another expression."""
|
|
256
286
|
e1 = self.term()
|
|
257
287
|
orop = self.match_orop()
|
|
258
288
|
if orop is not None:
|
|
@@ -262,6 +292,7 @@ class CondParser(CondTokens):
|
|
|
262
292
|
return e1
|
|
263
293
|
|
|
264
294
|
def parse(self) -> Any:
|
|
295
|
+
"""Parse the entire conditional expression and return the AST root."""
|
|
265
296
|
exp = self.expression()
|
|
266
297
|
if not self.cond_expr.eoi():
|
|
267
298
|
raise CondParserError(
|
|
@@ -274,13 +305,17 @@ class CondParser(CondTokens):
|
|
|
274
305
|
|
|
275
306
|
|
|
276
307
|
class NumericParser(NumTokens):
|
|
308
|
+
"""Recursive-descent parser for arithmetic numeric expressions."""
|
|
309
|
+
|
|
277
310
|
# Takes a numeric expression string
|
|
278
311
|
def __init__(self, numexpr: str) -> None:
|
|
312
|
+
"""Initialise the parser with the numeric expression string."""
|
|
279
313
|
self.num_expr = SourceString(numexpr)
|
|
280
314
|
self.rxint = re.compile(r"(?P<int_num>[+-]?[0-9]+)")
|
|
281
315
|
self.rxfloat = re.compile(r"(?P<float_num>[+-]?(?:(?:[0-9]*\.[0-9]+)|(?:[0-9]+\.[0-9]*)))")
|
|
282
316
|
|
|
283
317
|
def match_number(self) -> Any | None:
|
|
318
|
+
"""Match a float or integer literal and return its numeric value, or None."""
|
|
284
319
|
# Try to match a number in the source string.
|
|
285
320
|
# Return it if matched, return None if unmatched.
|
|
286
321
|
m1 = self.num_expr.match_regex(self.rxfloat)
|
|
@@ -293,6 +328,7 @@ class NumericParser(NumTokens):
|
|
|
293
328
|
return None
|
|
294
329
|
|
|
295
330
|
def match_mulop(self) -> int | None:
|
|
331
|
+
"""Match a multiplication or division operator and return its token type, or None."""
|
|
296
332
|
# Try to match a multiplication or division operator in the source string.
|
|
297
333
|
# if found, return the matching operator type. If not found, return None.
|
|
298
334
|
m1 = self.num_expr.match_str("*")
|
|
@@ -305,6 +341,7 @@ class NumericParser(NumTokens):
|
|
|
305
341
|
return None
|
|
306
342
|
|
|
307
343
|
def match_addop(self) -> int | None:
|
|
344
|
+
"""Match an addition or subtraction operator and return its token type, or None."""
|
|
308
345
|
# Try to match an addition or subtraction operator in the source string.
|
|
309
346
|
# if found, return the matching operator type. If not found, return None.
|
|
310
347
|
m1 = self.num_expr.match_str("+")
|
|
@@ -317,6 +354,7 @@ class NumericParser(NumTokens):
|
|
|
317
354
|
return None
|
|
318
355
|
|
|
319
356
|
def factor(self) -> Any:
|
|
357
|
+
"""Parse a numeric factor: a number literal or a parenthesised expression."""
|
|
320
358
|
# Parses a factor out of the source string and returns the
|
|
321
359
|
# AST node that is created.
|
|
322
360
|
m1 = self.match_number()
|
|
@@ -338,6 +376,7 @@ class NumericParser(NumTokens):
|
|
|
338
376
|
)
|
|
339
377
|
|
|
340
378
|
def term(self) -> Any:
|
|
379
|
+
"""Parse a term: a factor optionally followed by MUL/DIV and another term."""
|
|
341
380
|
# Parses a term out of the source string and returns the
|
|
342
381
|
# AST node that is created.
|
|
343
382
|
m1 = self.factor()
|
|
@@ -349,6 +388,7 @@ class NumericParser(NumTokens):
|
|
|
349
388
|
return m1
|
|
350
389
|
|
|
351
390
|
def expression(self) -> Any:
|
|
391
|
+
"""Parse an expression: a term optionally followed by ADD/SUB and another expression."""
|
|
352
392
|
# Parses an expression out of the source string and returns the
|
|
353
393
|
# AST node that is created.
|
|
354
394
|
e1 = self.term()
|
|
@@ -362,6 +402,7 @@ class NumericParser(NumTokens):
|
|
|
362
402
|
return e1
|
|
363
403
|
|
|
364
404
|
def parse(self) -> Any:
|
|
405
|
+
"""Parse the entire numeric expression and return the AST root."""
|
|
365
406
|
exp = self.expression()
|
|
366
407
|
if not self.num_expr.eoi():
|
|
367
408
|
raise NumericParserError(
|
execsql/script/control.py
CHANGED
execsql/script/engine.py
CHANGED
|
@@ -40,6 +40,25 @@ from execsql.script.variables import LocalSubVarSet, ScriptArgSubVarSet, SubVarS
|
|
|
40
40
|
from execsql.utils.errors import exception_desc
|
|
41
41
|
from execsql.utils.fileio import EncodedFile
|
|
42
42
|
|
|
43
|
+
__all__ = [
|
|
44
|
+
"MetaCommand",
|
|
45
|
+
"MetaCommandList",
|
|
46
|
+
"SqlStmt",
|
|
47
|
+
"MetacommandStmt",
|
|
48
|
+
"ScriptCmd",
|
|
49
|
+
"CommandList",
|
|
50
|
+
"CommandListWhileLoop",
|
|
51
|
+
"CommandListUntilLoop",
|
|
52
|
+
"ScriptFile",
|
|
53
|
+
"ScriptExecSpec",
|
|
54
|
+
"set_system_vars",
|
|
55
|
+
"substitute_vars",
|
|
56
|
+
"runscripts",
|
|
57
|
+
"current_script_line",
|
|
58
|
+
"read_sqlfile",
|
|
59
|
+
"read_sqlstring",
|
|
60
|
+
]
|
|
61
|
+
|
|
43
62
|
|
|
44
63
|
# ---------------------------------------------------------------------------
|
|
45
64
|
# MetaCommand / MetaCommandList
|
execsql/script/variables.py
CHANGED
|
@@ -15,6 +15,8 @@ from typing import Any
|
|
|
15
15
|
|
|
16
16
|
from execsql.exceptions import ErrInfo
|
|
17
17
|
|
|
18
|
+
__all__ = ["CounterVars", "SubVarSet", "LocalSubVarSet", "ScriptArgSubVarSet"]
|
|
19
|
+
|
|
18
20
|
|
|
19
21
|
# ---------------------------------------------------------------------------
|
|
20
22
|
# CounterVars
|
|
@@ -184,15 +186,17 @@ class SubVarSet:
|
|
|
184
186
|
return template_str.lower() in self._subs_dict
|
|
185
187
|
|
|
186
188
|
def merge(self, other_subvars: SubVarSet | None) -> SubVarSet:
|
|
187
|
-
"""Return a new SubVarSet with this object's variables merged with other_subvars.
|
|
189
|
+
"""Return a new SubVarSet with this object's variables merged with other_subvars.
|
|
190
|
+
|
|
191
|
+
Copies dictionaries and pre-compiled patterns directly instead of
|
|
192
|
+
re-adding variables one at a time, avoiding O(V) regex recompilation.
|
|
193
|
+
"""
|
|
188
194
|
if other_subvars is not None:
|
|
189
195
|
newsubs = SubVarSet()
|
|
190
|
-
newsubs._subs_dict =
|
|
191
|
-
newsubs._compiled_patterns =
|
|
196
|
+
newsubs._subs_dict = {**self._subs_dict, **other_subvars._subs_dict}
|
|
197
|
+
newsubs._compiled_patterns = {**self._compiled_patterns, **other_subvars._compiled_patterns}
|
|
192
198
|
newsubs.prefix_list = list(set(self.prefix_list + other_subvars.prefix_list))
|
|
193
199
|
newsubs.compile_var_rx()
|
|
194
|
-
for varname, value in other_subvars._subs_dict.items():
|
|
195
|
-
newsubs.add_substitution(varname, value)
|
|
196
200
|
return newsubs
|
|
197
201
|
return self
|
|
198
202
|
|