chemql-python 1.2.0__tar.gz → 1.2.2__tar.gz
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_python-1.2.0/src/chemql_python.egg-info → chemql_python-1.2.2}/PKG-INFO +6 -6
- {chemql_python-1.2.0 → chemql_python-1.2.2}/README.md +5 -5
- {chemql_python-1.2.0 → chemql_python-1.2.2}/pyproject.toml +1 -1
- {chemql_python-1.2.0 → chemql_python-1.2.2}/src/chemql/__init__.py +209 -39
- {chemql_python-1.2.0 → chemql_python-1.2.2/src/chemql_python.egg-info}/PKG-INFO +6 -6
- {chemql_python-1.2.0 → chemql_python-1.2.2}/LICENSE +0 -0
- {chemql_python-1.2.0 → chemql_python-1.2.2}/setup.cfg +0 -0
- {chemql_python-1.2.0 → chemql_python-1.2.2}/src/chemql/__main__.py +0 -0
- {chemql_python-1.2.0 → chemql_python-1.2.2}/src/chemql/data/BondEnergies.json +0 -0
- {chemql_python-1.2.0 → chemql_python-1.2.2}/src/chemql/data/MoleculeBonds.json +0 -0
- {chemql_python-1.2.0 → chemql_python-1.2.2}/src/chemql/data/Molecules.json +0 -0
- {chemql_python-1.2.0 → chemql_python-1.2.2}/src/chemql/data/PeriodicTableJSON.json +0 -0
- {chemql_python-1.2.0 → chemql_python-1.2.2}/src/chemql/data/Reactions.json +0 -0
- {chemql_python-1.2.0 → chemql_python-1.2.2}/src/chemql/data/THIRD_PARTY_LICENSES/PeriodicTableJSON.txt +0 -0
- {chemql_python-1.2.0 → chemql_python-1.2.2}/src/chemql/data/THIRD_PARTY_LICENSES/RHEA.txt +0 -0
- {chemql_python-1.2.0 → chemql_python-1.2.2}/src/chemql/html/periodic_table.html +0 -0
- {chemql_python-1.2.0 → chemql_python-1.2.2}/src/chemql_python.egg-info/SOURCES.txt +0 -0
- {chemql_python-1.2.0 → chemql_python-1.2.2}/src/chemql_python.egg-info/dependency_links.txt +0 -0
- {chemql_python-1.2.0 → chemql_python-1.2.2}/src/chemql_python.egg-info/entry_points.txt +0 -0
- {chemql_python-1.2.0 → chemql_python-1.2.2}/src/chemql_python.egg-info/requires.txt +0 -0
- {chemql_python-1.2.0 → chemql_python-1.2.2}/src/chemql_python.egg-info/top_level.txt +0 -0
- {chemql_python-1.2.0 → chemql_python-1.2.2}/tests/test_external_molecule_lookup.py +0 -0
- {chemql_python-1.2.0 → chemql_python-1.2.2}/tests/test_public_api.py +0 -0
- {chemql_python-1.2.0 → chemql_python-1.2.2}/tests/test_reactions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chemql-python
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
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.
|
|
20
|
+
elements, molecules, and reactions. Version 1.2.2 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.
|
|
28
|
-
> The examples below describe the ChemQL 1.2.
|
|
27
|
+
> **Version covered by this README:** 1.2.2\
|
|
28
|
+
> The examples below describe the ChemQL 1.2.2 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.
|
|
541
|
+
ChemQL 1.2.2 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.
|
|
836
|
+
chemql --upgrade 1.2.2
|
|
837
837
|
```
|
|
838
838
|
|
|
839
839
|
`--upgrade` installs the latest published `chemql-python` release; supplying a
|
|
@@ -4,15 +4,15 @@
|
|
|
4
4
|
shell commands in one interactive environment.**
|
|
5
5
|
|
|
6
6
|
ChemQL is a command-line chemistry query language for exploring chemical
|
|
7
|
-
elements, molecules, and reactions. Version 1.2.
|
|
7
|
+
elements, molecules, and reactions. Version 1.2.2 combines database-style
|
|
8
8
|
chemistry queries, reaction tools, Python interoperability, reusable functions
|
|
9
9
|
and macros, and a persistent shell in one scriptable environment. It combines a small SQL-inspired
|
|
10
10
|
query syntax with a persistent Python environment, reaction utilities,
|
|
11
11
|
optional external molecule lookup, and tools for running repeatable
|
|
12
12
|
scripts.
|
|
13
13
|
|
|
14
|
-
> **Version covered by this README:** 1.2.
|
|
15
|
-
> The examples below describe the ChemQL 1.2.
|
|
14
|
+
> **Version covered by this README:** 1.2.2\
|
|
15
|
+
> The examples below describe the ChemQL 1.2.2 source. Dataset
|
|
16
16
|
> contents and external-service availability can affect individual
|
|
17
17
|
> lookup results.
|
|
18
18
|
|
|
@@ -525,7 +525,7 @@ queried or inspected from Python.
|
|
|
525
525
|
|
|
526
526
|
## Preprocessor, functions, and macros
|
|
527
527
|
|
|
528
|
-
ChemQL 1.2.
|
|
528
|
+
ChemQL 1.2.2 adds three ways to reuse values and logic. These declarations
|
|
529
529
|
are processed in source order: definitions affect later lines, not earlier
|
|
530
530
|
ones. They persist during the current ChemQL process/session; they are not
|
|
531
531
|
saved automatically between separate launches.
|
|
@@ -820,7 +820,7 @@ redirect a file into the command.
|
|
|
820
820
|
``` bash
|
|
821
821
|
chemql --version
|
|
822
822
|
chemql --upgrade
|
|
823
|
-
chemql --upgrade 1.2.
|
|
823
|
+
chemql --upgrade 1.2.2
|
|
824
824
|
```
|
|
825
825
|
|
|
826
826
|
`--upgrade` installs the latest published `chemql-python` release; supplying a
|
|
@@ -18,6 +18,8 @@ from prompt_toolkit.lexers import Lexer
|
|
|
18
18
|
import atexit
|
|
19
19
|
import uuid
|
|
20
20
|
import select
|
|
21
|
+
import queue
|
|
22
|
+
import threading
|
|
21
23
|
from prompt_toolkit.styles import Style
|
|
22
24
|
from prompt_toolkit.completion import Completer, Completion
|
|
23
25
|
import urllib.parse
|
|
@@ -45,7 +47,7 @@ __all__ = [
|
|
|
45
47
|
"__version__"
|
|
46
48
|
]
|
|
47
49
|
|
|
48
|
-
__version__ = "1.2.
|
|
50
|
+
__version__ = "1.2.2"
|
|
49
51
|
|
|
50
52
|
# Persistent preprocessor/function state for the current ChemQL session.
|
|
51
53
|
DEFINITIONS = {}
|
|
@@ -107,16 +109,18 @@ def _split_function_arguments(source):
|
|
|
107
109
|
return args
|
|
108
110
|
|
|
109
111
|
def _call_function(name, arguments):
|
|
110
|
-
"""Call a ChemQL function
|
|
112
|
+
"""Call a ChemQL function, including `let` assignments and block syntax."""
|
|
111
113
|
if name not in FUNCTIONS:
|
|
112
114
|
raise NameError(f"Unknown ChemQL function `{name}`")
|
|
115
|
+
|
|
113
116
|
values = [evaluate_python(arg) for arg in arguments]
|
|
114
117
|
definition = FUNCTIONS[name]
|
|
115
118
|
saved_globals = python_globals.copy()
|
|
116
119
|
saved_chemql_variables = CHEMQL_VARIABLES.copy()
|
|
117
120
|
had_arguments = "arguments" in python_globals
|
|
118
121
|
saved_arguments = python_globals.get("arguments")
|
|
119
|
-
|
|
122
|
+
# `arguments` contains only the supplied arguments, not the function name.
|
|
123
|
+
python_globals["arguments"] = list(values)
|
|
120
124
|
|
|
121
125
|
def substitute_parameters(source):
|
|
122
126
|
# Replace positional placeholders as tokens, so \1 does not corrupt \10.
|
|
@@ -127,6 +131,23 @@ def _call_function(name, arguments):
|
|
|
127
131
|
source,
|
|
128
132
|
)
|
|
129
133
|
|
|
134
|
+
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)
|
|
137
|
+
if not match:
|
|
138
|
+
return source
|
|
139
|
+
variable, expression = match.groups()
|
|
140
|
+
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.
|
|
144
|
+
expression = re.sub(
|
|
145
|
+
r"\[\s*([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)\s*\]",
|
|
146
|
+
r"(\1)",
|
|
147
|
+
expression,
|
|
148
|
+
)
|
|
149
|
+
return f"[{variable} = {expression}]"
|
|
150
|
+
|
|
130
151
|
try:
|
|
131
152
|
body = definition["body"]
|
|
132
153
|
i = 0
|
|
@@ -149,6 +170,29 @@ def _call_function(name, arguments):
|
|
|
149
170
|
i += 1
|
|
150
171
|
continue
|
|
151
172
|
|
|
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)
|
|
177
|
+
block_body = [
|
|
178
|
+
normalize_let_line(substitute_parameters(source_line))
|
|
179
|
+
for source_line in body[i + 1:end]
|
|
180
|
+
]
|
|
181
|
+
code = line[1:-1].strip()
|
|
182
|
+
if code.startswith("for "):
|
|
183
|
+
execute_for_block(line, block_body)
|
|
184
|
+
elif code.startswith("while "):
|
|
185
|
+
execute_while_block(line, block_body)
|
|
186
|
+
elif code.startswith("if "):
|
|
187
|
+
execute_if_block(line, block_body)
|
|
188
|
+
else:
|
|
189
|
+
raise SyntaxError(f"Unsupported function block `{code}`")
|
|
190
|
+
i = end + 1
|
|
191
|
+
continue
|
|
192
|
+
|
|
193
|
+
if line == "[endblock]":
|
|
194
|
+
raise SyntaxError(f"Unexpected `[endblock]` in function `{name}`")
|
|
195
|
+
|
|
152
196
|
if line.startswith("return"):
|
|
153
197
|
expression = line[len("return"):].strip()
|
|
154
198
|
if expression.startswith("[") and expression.endswith("]"):
|
|
@@ -156,7 +200,12 @@ def _call_function(name, arguments):
|
|
|
156
200
|
expression = substitute_parameters(expression)
|
|
157
201
|
return evaluate_python(expression) if expression else None
|
|
158
202
|
|
|
159
|
-
|
|
203
|
+
normalized = normalize_let_line(line)
|
|
204
|
+
if normalized != line:
|
|
205
|
+
# The wrapper is ChemQL syntax; pass the assignment itself to Python.
|
|
206
|
+
evaluate_python(normalized[1:-1])
|
|
207
|
+
else:
|
|
208
|
+
evaluate_python(substitute_parameters(line))
|
|
160
209
|
i += 1
|
|
161
210
|
return None
|
|
162
211
|
finally:
|
|
@@ -211,17 +260,15 @@ _shell_process = None
|
|
|
211
260
|
_shell_master_fd = None
|
|
212
261
|
_shell_prompt_ready = False
|
|
213
262
|
_shell_output_streamed = False
|
|
263
|
+
_shell_output_queue = None
|
|
264
|
+
_shell_reader_thread = None
|
|
214
265
|
_shell_prompt = f"__CHEMQL_PROMPT_{uuid.uuid4().hex}__"
|
|
215
266
|
|
|
216
267
|
|
|
217
268
|
def get_shell():
|
|
218
|
-
"""Return the persistent shell process.
|
|
219
|
-
|
|
220
|
-
On POSIX, use a pseudo-terminal so foreground commands such as `read`,
|
|
221
|
-
`read -p`, and interactive CLIs can receive keyboard input. Windows keeps
|
|
222
|
-
the pipe-based implementation until a ConPTY backend is added.
|
|
223
|
-
"""
|
|
269
|
+
"""Return the persistent OS shell process for the current platform."""
|
|
224
270
|
global _shell_process, _shell_master_fd, _shell_prompt_ready
|
|
271
|
+
global _shell_output_queue, _shell_reader_thread
|
|
225
272
|
|
|
226
273
|
if _shell_process is not None and _shell_process.poll() is None:
|
|
227
274
|
return _shell_process
|
|
@@ -230,14 +277,38 @@ def get_shell():
|
|
|
230
277
|
close_shell()
|
|
231
278
|
|
|
232
279
|
if os.name == "nt":
|
|
280
|
+
# Use PowerShell as ChemQL's native Windows shell. Its prompt function
|
|
281
|
+
# emits a private marker when a command finishes, so we do not append a
|
|
282
|
+
# marker to stdin (which would accidentally become Read-Host input).
|
|
283
|
+
setup = (
|
|
284
|
+
f"function global:prompt {{ [Console]::Out.WriteLine('{_shell_prompt}'); return '' }}"
|
|
285
|
+
)
|
|
233
286
|
_shell_process = subprocess.Popen(
|
|
234
|
-
["
|
|
287
|
+
["powershell.exe", "-NoLogo", "-NoProfile", "-NoExit", "-Command", setup],
|
|
235
288
|
stdin=subprocess.PIPE,
|
|
236
289
|
stdout=subprocess.PIPE,
|
|
237
290
|
stderr=subprocess.STDOUT,
|
|
238
291
|
text=True,
|
|
239
292
|
bufsize=1,
|
|
240
293
|
)
|
|
294
|
+
output_queue = queue.Queue()
|
|
295
|
+
_shell_output_queue = output_queue
|
|
296
|
+
process = _shell_process
|
|
297
|
+
|
|
298
|
+
def read_output():
|
|
299
|
+
try:
|
|
300
|
+
while True:
|
|
301
|
+
character = process.stdout.read(1) # type: ignore
|
|
302
|
+
if not character:
|
|
303
|
+
break
|
|
304
|
+
output_queue.put(character)
|
|
305
|
+
except (OSError, ValueError):
|
|
306
|
+
pass
|
|
307
|
+
finally:
|
|
308
|
+
output_queue.put(None)
|
|
309
|
+
|
|
310
|
+
_shell_reader_thread = threading.Thread(target=read_output, daemon=True)
|
|
311
|
+
_shell_reader_thread.start()
|
|
241
312
|
else:
|
|
242
313
|
import fcntl
|
|
243
314
|
import pty
|
|
@@ -387,47 +458,128 @@ def _run_shell_command_posix(command, marker=None):
|
|
|
387
458
|
return f"Shell error: {exc}"
|
|
388
459
|
|
|
389
460
|
|
|
390
|
-
def
|
|
461
|
+
def _run_shell_command_windows(command):
|
|
462
|
+
"""Run a command in persistent PowerShell and forward line input interactively."""
|
|
463
|
+
global _shell_prompt_ready, _shell_output_streamed
|
|
391
464
|
shell = get_shell()
|
|
392
|
-
|
|
393
|
-
|
|
465
|
+
output_queue = _shell_output_queue
|
|
466
|
+
if output_queue is None:
|
|
467
|
+
return "Shell error: PowerShell output reader was not initialized."
|
|
394
468
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
469
|
+
import msvcrt
|
|
470
|
+
|
|
471
|
+
def wait_for_prompt(stream_output):
|
|
472
|
+
collected = ""
|
|
473
|
+
streamed_pos = 0
|
|
474
|
+
marker = _shell_prompt
|
|
475
|
+
marker_len = len(marker)
|
|
476
|
+
stdin_is_tty = False
|
|
477
|
+
try:
|
|
478
|
+
stdin_is_tty = sys.stdin is not None and sys.stdin.isatty()
|
|
479
|
+
except (AttributeError, OSError, ValueError):
|
|
480
|
+
pass
|
|
481
|
+
pending_input = ""
|
|
398
482
|
|
|
399
|
-
# Windows pipe mode supports persistent commands, but not fully
|
|
400
|
-
# interactive programs yet; a ConPTY backend is needed for that.
|
|
401
|
-
shell.stdin.write(command + "\r\n" + f"echo {marker}\r\n") # type: ignore
|
|
402
|
-
shell.stdin.flush() # type: ignore
|
|
403
|
-
output = []
|
|
404
483
|
while True:
|
|
484
|
+
# While a shell command is running, pass complete lines typed by the
|
|
485
|
+
# user to PowerShell. This lets Read-Host receive real input rather
|
|
486
|
+
# than consuming a completion marker injected by ChemQL.
|
|
487
|
+
if stdin_is_tty:
|
|
488
|
+
try:
|
|
489
|
+
while msvcrt.kbhit(): #type:ignore
|
|
490
|
+
key = msvcrt.getwch() #type:ignore
|
|
491
|
+
if key in ("\x00", "\xe0"): #type:ignore
|
|
492
|
+
# Consume the second half of a Windows special key.
|
|
493
|
+
if msvcrt.kbhit(): #type:ignore
|
|
494
|
+
msvcrt.getwch() #type:ignore
|
|
495
|
+
elif key in ("\r", "\n"):
|
|
496
|
+
shell.stdin.write(pending_input + "\r\n") # type: ignore
|
|
497
|
+
shell.stdin.flush() # type: ignore
|
|
498
|
+
pending_input = ""
|
|
499
|
+
elif key == "\b":
|
|
500
|
+
pending_input = pending_input[:-1]
|
|
501
|
+
elif key == "\x03":
|
|
502
|
+
# Ctrl-C is sent as a line input; reset is safer than
|
|
503
|
+
# leaving ChemQL stuck in a nested interactive prompt.
|
|
504
|
+
close_shell()
|
|
505
|
+
return None
|
|
506
|
+
elif key >= " " and key != "\x7f":
|
|
507
|
+
pending_input += key
|
|
508
|
+
except (OSError, ValueError):
|
|
509
|
+
stdin_is_tty = False
|
|
510
|
+
|
|
405
511
|
try:
|
|
406
|
-
|
|
407
|
-
except
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
512
|
+
character = output_queue.get(timeout=0.04)
|
|
513
|
+
except queue.Empty:
|
|
514
|
+
continue
|
|
515
|
+
if character is None:
|
|
516
|
+
return None
|
|
517
|
+
|
|
518
|
+
collected += character
|
|
519
|
+
marker_pos = collected.find(marker)
|
|
520
|
+
if marker_pos >= 0:
|
|
521
|
+
visible = collected[:marker_pos]
|
|
522
|
+
if stream_output and marker_pos > streamed_pos:
|
|
523
|
+
sys.stdout.write(collected[streamed_pos:marker_pos])
|
|
524
|
+
sys.stdout.flush()
|
|
525
|
+
_shell_output_streamed = True
|
|
526
|
+
return visible.replace("\r\n", "\n").rstrip("\r\n ")
|
|
527
|
+
|
|
528
|
+
if stream_output:
|
|
529
|
+
# Stream immediately, retaining the full output for history and
|
|
530
|
+
# only withholding a suffix that could begin the prompt marker.
|
|
531
|
+
overlap = 0
|
|
532
|
+
max_overlap = min(marker_len - 1, len(collected))
|
|
533
|
+
for size in range(max_overlap, 0, -1):
|
|
534
|
+
if collected.endswith(marker[:size]):
|
|
535
|
+
overlap = size
|
|
536
|
+
break
|
|
537
|
+
safe_end = len(collected) - overlap
|
|
538
|
+
if safe_end > streamed_pos:
|
|
539
|
+
sys.stdout.write(collected[streamed_pos:safe_end])
|
|
540
|
+
sys.stdout.flush()
|
|
541
|
+
_shell_output_streamed = True
|
|
542
|
+
streamed_pos = safe_end
|
|
543
|
+
|
|
544
|
+
if not _shell_prompt_ready:
|
|
545
|
+
initial = wait_for_prompt(stream_output=False)
|
|
546
|
+
if initial is None:
|
|
547
|
+
close_shell()
|
|
548
|
+
return "Shell session terminated."
|
|
549
|
+
_shell_prompt_ready = True
|
|
550
|
+
|
|
551
|
+
try:
|
|
552
|
+
shell.stdin.write(command.replace("\n", "\r\n") + "\r\n") # type: ignore
|
|
553
|
+
shell.stdin.flush() # type: ignore
|
|
554
|
+
result = wait_for_prompt(stream_output=True)
|
|
555
|
+
if result is None:
|
|
556
|
+
close_shell()
|
|
557
|
+
return "Shell session terminated."
|
|
558
|
+
return result
|
|
559
|
+
except (BrokenPipeError, OSError, ValueError) as exc:
|
|
560
|
+
close_shell()
|
|
561
|
+
return f"Shell error: {exc}"
|
|
562
|
+
|
|
419
563
|
|
|
564
|
+
def run_shell_command(command):
|
|
565
|
+
global _shell_output_streamed
|
|
566
|
+
command = command.replace("\r\n", "\n").replace("\r", "\n").rstrip("\n")
|
|
567
|
+
try:
|
|
568
|
+
if os.name == "nt":
|
|
569
|
+
if command.strip().casefold() in {"powershell", "powershell.exe", "pwsh", "pwsh.exe"}:
|
|
570
|
+
return "ChemQL already uses PowerShell on Windows; run the command directly instead of starting a nested shell."
|
|
571
|
+
_shell_output_streamed = False
|
|
572
|
+
return _run_shell_command_windows(command)
|
|
573
|
+
return _run_shell_command_posix(command)
|
|
420
574
|
except KeyboardInterrupt:
|
|
421
575
|
close_shell()
|
|
422
576
|
return "Shell command interrupted. Shell session was reset."
|
|
423
|
-
except (BrokenPipeError, OSError) as exc:
|
|
424
|
-
close_shell()
|
|
425
|
-
return f"Shell error: {exc}"
|
|
426
577
|
|
|
427
578
|
|
|
428
579
|
def close_shell():
|
|
429
580
|
"""Clean up the persistent shell and its pseudo-terminal on exit."""
|
|
430
581
|
global _shell_process, _shell_master_fd, _shell_prompt_ready
|
|
582
|
+
global _shell_output_queue, _shell_reader_thread
|
|
431
583
|
|
|
432
584
|
if _shell_process is not None:
|
|
433
585
|
if _shell_process.poll() is None:
|
|
@@ -449,6 +601,8 @@ def close_shell():
|
|
|
449
601
|
pass
|
|
450
602
|
_shell_master_fd = None
|
|
451
603
|
_shell_prompt_ready = False
|
|
604
|
+
_shell_output_queue = None
|
|
605
|
+
_shell_reader_thread = None
|
|
452
606
|
|
|
453
607
|
|
|
454
608
|
atexit.register(close_shell)
|
|
@@ -2719,6 +2873,14 @@ def process_lines(lines):
|
|
|
2719
2873
|
print("No definitions.")
|
|
2720
2874
|
i += 1
|
|
2721
2875
|
continue
|
|
2876
|
+
if stripped.casefold() == "macros":
|
|
2877
|
+
if MACROS:
|
|
2878
|
+
for name, value in MACROS.items():
|
|
2879
|
+
print(f"@{name} = {value}")
|
|
2880
|
+
else:
|
|
2881
|
+
print("No macros.")
|
|
2882
|
+
i += 1
|
|
2883
|
+
continue
|
|
2722
2884
|
|
|
2723
2885
|
# Function definitions use `func NAME ... endfunc`.
|
|
2724
2886
|
function_match = re.fullmatch(r"func\s+(" + _DEFINITION_NAME + r")", stripped, re.IGNORECASE)
|
|
@@ -3167,6 +3329,13 @@ def execute(arguments, query_text=None, positions=None):
|
|
|
3167
3329
|
|
|
3168
3330
|
if arguments[0].lower() == "version":
|
|
3169
3331
|
return __version__
|
|
3332
|
+
if arguments[0].lower() == "exit":
|
|
3333
|
+
return sys.exit(0)
|
|
3334
|
+
if arguments[0].lower() == "definitions":
|
|
3335
|
+
return "\n".join([f"@{name} = {value}" for name, value in DEFINITIONS.items()]) if DEFINITIONS else "No definitions."
|
|
3336
|
+
|
|
3337
|
+
if arguments[0].lower() == "macros":
|
|
3338
|
+
return "\n".join([f"@{name} = {value}" for name, value in MACROS.items()]) if MACROS else "No macros."
|
|
3170
3339
|
|
|
3171
3340
|
if arguments[0] == "conditions":
|
|
3172
3341
|
if len(arguments) != 1:
|
|
@@ -3522,6 +3691,7 @@ class ChemqlLexer(Lexer):
|
|
|
3522
3691
|
"findmol", "findre", "set", "add", "remove", "react",
|
|
3523
3692
|
"balance", "conditions", "view", "help", "dump", "version",
|
|
3524
3693
|
"return", "union", "intersection", "let", "unset", "variables",
|
|
3694
|
+
"macros", "definitions", "arguments",
|
|
3525
3695
|
}
|
|
3526
3696
|
|
|
3527
3697
|
OPERATORS = {
|
|
@@ -3736,7 +3906,7 @@ class ChemqlCompleter(Completer):
|
|
|
3736
3906
|
"findel", "findmol", "findre", "set", "add", "remove",
|
|
3737
3907
|
"react", "balance", "conditions", "version", "help", "view",
|
|
3738
3908
|
"dump", "let", "unset", "variables", "func", "define",
|
|
3739
|
-
"import",
|
|
3909
|
+
"import", "definitions", "macros",
|
|
3740
3910
|
)
|
|
3741
3911
|
|
|
3742
3912
|
QUERY_OPERATORS = [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chemql-python
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.2
|
|
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.
|
|
20
|
+
elements, molecules, and reactions. Version 1.2.2 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.
|
|
28
|
-
> The examples below describe the ChemQL 1.2.
|
|
27
|
+
> **Version covered by this README:** 1.2.2\
|
|
28
|
+
> The examples below describe the ChemQL 1.2.2 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.
|
|
541
|
+
ChemQL 1.2.2 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.
|
|
836
|
+
chemql --upgrade 1.2.2
|
|
837
837
|
```
|
|
838
838
|
|
|
839
839
|
`--upgrade` installs the latest published `chemql-python` release; supplying a
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|