chemql-python 1.2.2__py3-none-any.whl → 1.2.3__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.
chemql/__init__.py CHANGED
@@ -47,7 +47,7 @@ __all__ = [
47
47
  "__version__"
48
48
  ]
49
49
 
50
- __version__ = "1.2.2"
50
+ __version__ = "1.2.3"
51
51
 
52
52
  # Persistent preprocessor/function state for the current ChemQL session.
53
53
  DEFINITIONS = {}
@@ -55,6 +55,7 @@ FUNCTIONS = {}
55
55
  MACROS = {}
56
56
  EXECUTION_HISTORY = []
57
57
  _DEFINITION_NAME = r"[A-Za-z_][A-Za-z0-9_]*"
58
+ _NO_RETURN = object()
58
59
 
59
60
  def _expand_definitions(line):
60
61
  """Replace @NAME literally, including inside strings, using current definitions."""
@@ -109,111 +110,203 @@ def _split_function_arguments(source):
109
110
  return args
110
111
 
111
112
  def _call_function(name, arguments):
112
- """Call a ChemQL function, including `let` assignments and block syntax."""
113
+ """Call a ChemQL function, including let assignments and block syntax."""
113
114
  if name not in FUNCTIONS:
114
115
  raise NameError(f"Unknown ChemQL function `{name}`")
115
116
 
116
117
  values = [evaluate_python(arg) for arg in arguments]
117
118
  definition = FUNCTIONS[name]
119
+
118
120
  saved_globals = python_globals.copy()
119
121
  saved_chemql_variables = CHEMQL_VARIABLES.copy()
122
+
120
123
  had_arguments = "arguments" in python_globals
121
124
  saved_arguments = python_globals.get("arguments")
122
- # `arguments` contains only the supplied arguments, not the function name.
125
+
126
+ # Expose only the supplied arguments.
123
127
  python_globals["arguments"] = list(values)
124
128
 
125
129
  def substitute_parameters(source):
126
- # Replace positional placeholders as tokens, so \1 does not corrupt \10.
130
+ """Replace \\1, \\2, etc. without corrupting \\10 or longer indices."""
127
131
  return re.sub(
128
132
  r"\\(\d+)",
129
- lambda match: repr(values[int(match.group(1)) - 1])
130
- if 0 < int(match.group(1)) <= len(values) else match.group(0),
133
+ lambda match: (
134
+ repr(values[int(match.group(1)) - 1])
135
+ if 0 < int(match.group(1)) <= len(values)
136
+ else match.group(0)
137
+ ),
131
138
  source,
132
139
  )
133
140
 
134
141
  def normalize_let_line(source):
135
- """Translate `let name = expression` to a Python assignment block."""
136
- match = re.fullmatch(r"\s*let\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*", source)
142
+ """Translate let name = expression into a Python assignment."""
143
+ match = re.fullmatch(
144
+ r"\s*let\s+([A-Za-z_]\w*)\s*=\s*(.*?)\s*",
145
+ source,
146
+ )
147
+
137
148
  if not match:
138
149
  return source
150
+
139
151
  variable, expression = match.groups()
140
152
  expression = substitute_parameters(expression)
141
- # In ChemQL function expressions, [i] means evaluate i, rather than
142
- # create a Python list containing i. Keep this deliberately simple and
143
- # avoid changing arbitrary Python list literals.
153
+ expression = _expand_definitions(expression)
154
+
155
+ # Translate simple ChemQL [variable] expressions into (variable).
144
156
  expression = re.sub(
145
- r"\[\s*([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)\s*\]",
157
+ r"\[\s*([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*\]",
146
158
  r"(\1)",
147
159
  expression,
148
160
  )
161
+
149
162
  return f"[{variable} = {expression}]"
150
163
 
151
- try:
152
- body = definition["body"]
164
+ def execute_function_lines(lines):
165
+ """Execute function statements and propagate return values."""
153
166
  i = 0
154
- while i < len(body):
155
- line = body[i].strip()
167
+
168
+ while i < len(lines):
169
+ line = lines[i].strip()
170
+
156
171
  if not line:
157
172
  i += 1
158
173
  continue
159
174
 
160
- # A function may contain a multiline [[[ Python block ]]].
175
+ # Multiline Python block.
161
176
  if line == "[[[":
162
177
  block = []
163
178
  i += 1
164
- while i < len(body) and body[i].strip() != "]]]":
165
- block.append(substitute_parameters(body[i]))
179
+
180
+ while i < len(lines) and lines[i].strip() != "]]]":
181
+ source_line = substitute_parameters(lines[i])
182
+ source_line = _expand_definitions(source_line)
183
+ block.append(source_line)
166
184
  i += 1
167
- if i >= len(body):
168
- raise SyntaxError(f"Missing `]]]` in function `{name}`")
185
+
186
+ if i >= len(lines):
187
+ raise SyntaxError(
188
+ f"Missing `]]]` in function `{name}`"
189
+ )
190
+
169
191
  evaluate_python("\n".join(block))
170
192
  i += 1
171
193
  continue
172
194
 
173
- # Support ChemQL's [for ...], [if ...], and [while ...] blocks
174
- # inside functions, using the same block executors as the REPL.
175
- if is_python_block_header(line):
176
- end = find_endblock(body, i + 1)
195
+ # Recognize block headers generically, while excluding
196
+ # closing and branch markers from being treated as headers.
197
+ generic_header = re.match(
198
+ r"^\[\s*(?!endblock\b|else\b|elif\b)"
199
+ r"[A-Za-z_]\w*\b.*\]\s*$",
200
+ line,
201
+ )
202
+
203
+ if is_python_block_header(line) or generic_header:
204
+ end = find_endblock(lines, i + 1)
205
+
177
206
  block_body = [
178
207
  normalize_let_line(substitute_parameters(source_line))
179
- for source_line in body[i + 1:end]
208
+ for source_line in lines[i + 1:end]
180
209
  ]
210
+
181
211
  code = line[1:-1].strip()
212
+
182
213
  if code.startswith("for "):
183
214
  execute_for_block(line, block_body)
215
+
184
216
  elif code.startswith("while "):
185
217
  execute_while_block(line, block_body)
218
+
186
219
  elif code.startswith("if "):
187
- execute_if_block(line, block_body)
220
+ # Expand parameters and definitions before evaluating.
221
+ condition = code[3:].strip()
222
+ condition = substitute_parameters(condition)
223
+ condition = _expand_definitions(condition)
224
+
225
+ # Find the top-level else branch.
226
+ else_index = next(
227
+ (
228
+ j
229
+ for j, source_line in enumerate(block_body)
230
+ if source_line.strip() == "[else]"
231
+ ),
232
+ None,
233
+ )
234
+
235
+ if evaluate_python(condition):
236
+ selected_body = (
237
+ block_body
238
+ if else_index is None
239
+ else block_body[:else_index]
240
+ )
241
+ else:
242
+ selected_body = (
243
+ []
244
+ if else_index is None
245
+ else block_body[else_index + 1:]
246
+ )
247
+
248
+ # Execute only the selected branch. A return exits
249
+ # this function immediately.
250
+ result = execute_function_lines(selected_body)
251
+
252
+ if result is not _NO_RETURN:
253
+ return result
254
+
188
255
  else:
189
- raise SyntaxError(f"Unsupported function block `{code}`")
256
+ raise SyntaxError(
257
+ f"Unsupported function block `{code}`"
258
+ )
259
+
190
260
  i = end + 1
191
261
  continue
192
262
 
193
263
  if line == "[endblock]":
194
- raise SyntaxError(f"Unexpected `[endblock]` in function `{name}`")
264
+ raise SyntaxError(
265
+ f"Unexpected `[endblock]` in function `{name}`"
266
+ )
267
+
268
+ # Handle return statements inside the function.
269
+ if re.match(r"^return\b", line):
270
+ expression = re.sub(r"^return\b", "", line, count=1).strip()
195
271
 
196
- if line.startswith("return"):
197
- expression = line[len("return"):].strip()
198
272
  if expression.startswith("[") and expression.endswith("]"):
199
- expression = expression[1:-1]
273
+ expression = expression[1:-1].strip()
274
+
200
275
  expression = substitute_parameters(expression)
276
+ expression = _expand_definitions(expression)
277
+
201
278
  return evaluate_python(expression) if expression else None
202
279
 
203
280
  normalized = normalize_let_line(line)
281
+
204
282
  if normalized != line:
205
- # The wrapper is ChemQL syntax; pass the assignment itself to Python.
206
283
  evaluate_python(normalized[1:-1])
284
+ elif line.startswith("[") and line.endswith("]"):
285
+ expression = line[1:-1].strip()
286
+ expression = substitute_parameters(expression)
287
+ expression = _expand_definitions(expression)
288
+ evaluate_python(expression)
207
289
  else:
208
- evaluate_python(substitute_parameters(line))
290
+ expression = substitute_parameters(line)
291
+ expression = _expand_definitions(expression)
292
+ evaluate_python(expression)
293
+
209
294
  i += 1
210
- return None
295
+
296
+ return _NO_RETURN
297
+
298
+ try:
299
+ result = execute_function_lines(definition["body"])
300
+ return None if result is _NO_RETURN else result
301
+
211
302
  finally:
212
- # Keep function-local assignments from leaking into the caller's namespace.
303
+ # Restore the caller's namespaces.
213
304
  python_globals.clear()
214
305
  python_globals.update(saved_globals)
306
+
215
307
  CHEMQL_VARIABLES.clear()
216
308
  CHEMQL_VARIABLES.update(saved_chemql_variables)
309
+
217
310
  if had_arguments:
218
311
  python_globals["arguments"] = saved_arguments
219
312
  else:
@@ -2593,7 +2686,7 @@ def execute_if_block(header, body):
2593
2686
  )
2594
2687
 
2595
2688
  for section_header, section_body in sections:
2596
- code = section_header[1:-1].strip()
2689
+ code = _expand_definitions(section_header[1:-1].strip())
2597
2690
 
2598
2691
  if code.startswith("if "):
2599
2692
  condition = code[3:].strip()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chemql-python
3
- Version: 1.2.2
3
+ Version: 1.2.3
4
4
  Summary: A chemistry-focused query language and REPL
5
5
  Author-email: Mohammad Rakibul Islam <rakibulofficial1017@gmail.com>
6
6
  Requires-Python: >=3.10
@@ -17,15 +17,15 @@ Dynamic: license-file
17
17
  shell commands in one interactive environment.**
18
18
 
19
19
  ChemQL is a command-line chemistry query language for exploring chemical
20
- elements, molecules, and reactions. Version 1.2.2 combines database-style
20
+ elements, molecules, and reactions. Version 1.2.3 combines database-style
21
21
  chemistry queries, reaction tools, Python interoperability, reusable functions
22
22
  and macros, and a persistent shell in one scriptable environment. It combines a small SQL-inspired
23
23
  query syntax with a persistent Python environment, reaction utilities,
24
24
  optional external molecule lookup, and tools for running repeatable
25
25
  scripts.
26
26
 
27
- > **Version covered by this README:** 1.2.2\
28
- > The examples below describe the ChemQL 1.2.2 source. Dataset
27
+ > **Version covered by this README:** 1.2.3\
28
+ > The examples below describe the ChemQL 1.2.3 source. Dataset
29
29
  > contents and external-service availability can affect individual
30
30
  > lookup results.
31
31
 
@@ -538,7 +538,7 @@ queried or inspected from Python.
538
538
 
539
539
  ## Preprocessor, functions, and macros
540
540
 
541
- ChemQL 1.2.2 adds three ways to reuse values and logic. These declarations
541
+ ChemQL 1.2.3 adds three ways to reuse values and logic. These declarations
542
542
  are processed in source order: definitions affect later lines, not earlier
543
543
  ones. They persist during the current ChemQL process/session; they are not
544
544
  saved automatically between separate launches.
@@ -833,7 +833,7 @@ redirect a file into the command.
833
833
  ``` bash
834
834
  chemql --version
835
835
  chemql --upgrade
836
- chemql --upgrade 1.2.2
836
+ chemql --upgrade 1.2.3
837
837
  ```
838
838
 
839
839
  `--upgrade` installs the latest published `chemql-python` release; supplying a
@@ -1,4 +1,4 @@
1
- chemql/__init__.py,sha256=r2d-6_Co77jFd0rGL1ipVvMLyi6G5R5BhYvHC0F8NyY,157189
1
+ chemql/__init__.py,sha256=buPnjHRRx32NB8I6hXUgKoKLzy3R_FkdNYqm1mmptLA,159524
2
2
  chemql/__main__.py,sha256=3Q52s0hyuouhvSSqFEQFDr6mtJ-azkCwI2AD4c7f7A8,28
3
3
  chemql/data/BondEnergies.json,sha256=QZN3iWZrD8125jV41EG1GiRohBtP0CzAP_EDQTfaEKc,8269
4
4
  chemql/data/MoleculeBonds.json,sha256=Iw2shYxJScH_tI80IytBYoqVDSbgT7OSqPJUIgKZme4,84262
@@ -8,9 +8,9 @@ chemql/data/Reactions.json,sha256=BFpkKEWwLzG54ot96BVOWwU3btpimRJlOM57yGclGgc,15
8
8
  chemql/data/THIRD_PARTY_LICENSES/PeriodicTableJSON.txt,sha256=9wih5q8s58qR3vJEGpooa4ESyAGZHcmGvzwMMkMHNKk,322
9
9
  chemql/data/THIRD_PARTY_LICENSES/RHEA.txt,sha256=nXTeCP6vJBW5B_mtaVGFLr0w7vnjrSTBnv6_eOk3KKo,634
10
10
  chemql/html/periodic_table.html,sha256=ftVgXWjRO2AsTAGEekTrvgE5ZvcwRfoby9UWoixc9Y8,361560
11
- chemql_python-1.2.2.dist-info/licenses/LICENSE,sha256=o4ywHC0EwRy2zgIASiRD2sK3Vcsf7LBcTHjDvoJgRE0,1079
12
- chemql_python-1.2.2.dist-info/METADATA,sha256=n7DufnwFppatPS4HweoL9dmw8tKzi3y_uuMBpoLQTiA,30529
13
- chemql_python-1.2.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
14
- chemql_python-1.2.2.dist-info/entry_points.txt,sha256=j98ObVWR743an524z24AiWeRsppag-py5cc7l0D0HkI,39
15
- chemql_python-1.2.2.dist-info/top_level.txt,sha256=9UzHwnk_F6W19Q9nayaFgVyAELQXXwoYjeBgB6MWNr8,7
16
- chemql_python-1.2.2.dist-info/RECORD,,
11
+ chemql_python-1.2.3.dist-info/licenses/LICENSE,sha256=o4ywHC0EwRy2zgIASiRD2sK3Vcsf7LBcTHjDvoJgRE0,1079
12
+ chemql_python-1.2.3.dist-info/METADATA,sha256=HMq0gKLpGQrzdy9Ycj5evg8E0D57_Hfr28TSNOQHuXM,30529
13
+ chemql_python-1.2.3.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
14
+ chemql_python-1.2.3.dist-info/entry_points.txt,sha256=j98ObVWR743an524z24AiWeRsppag-py5cc7l0D0HkI,39
15
+ chemql_python-1.2.3.dist-info/top_level.txt,sha256=9UzHwnk_F6W19Q9nayaFgVyAELQXXwoYjeBgB6MWNr8,7
16
+ chemql_python-1.2.3.dist-info/RECORD,,