chemql-python 1.2.1__tar.gz → 1.2.3__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.1/src/chemql_python.egg-info → chemql_python-1.2.3}/PKG-INFO +6 -6
- {chemql_python-1.2.1 → chemql_python-1.2.3}/README.md +5 -5
- {chemql_python-1.2.1 → chemql_python-1.2.3}/pyproject.toml +1 -1
- {chemql_python-1.2.1 → chemql_python-1.2.3}/src/chemql/__init__.py +273 -73
- {chemql_python-1.2.1 → chemql_python-1.2.3/src/chemql_python.egg-info}/PKG-INFO +6 -6
- {chemql_python-1.2.1 → chemql_python-1.2.3}/LICENSE +0 -0
- {chemql_python-1.2.1 → chemql_python-1.2.3}/setup.cfg +0 -0
- {chemql_python-1.2.1 → chemql_python-1.2.3}/src/chemql/__main__.py +0 -0
- {chemql_python-1.2.1 → chemql_python-1.2.3}/src/chemql/data/BondEnergies.json +0 -0
- {chemql_python-1.2.1 → chemql_python-1.2.3}/src/chemql/data/MoleculeBonds.json +0 -0
- {chemql_python-1.2.1 → chemql_python-1.2.3}/src/chemql/data/Molecules.json +0 -0
- {chemql_python-1.2.1 → chemql_python-1.2.3}/src/chemql/data/PeriodicTableJSON.json +0 -0
- {chemql_python-1.2.1 → chemql_python-1.2.3}/src/chemql/data/Reactions.json +0 -0
- {chemql_python-1.2.1 → chemql_python-1.2.3}/src/chemql/data/THIRD_PARTY_LICENSES/PeriodicTableJSON.txt +0 -0
- {chemql_python-1.2.1 → chemql_python-1.2.3}/src/chemql/data/THIRD_PARTY_LICENSES/RHEA.txt +0 -0
- {chemql_python-1.2.1 → chemql_python-1.2.3}/src/chemql/html/periodic_table.html +0 -0
- {chemql_python-1.2.1 → chemql_python-1.2.3}/src/chemql_python.egg-info/SOURCES.txt +0 -0
- {chemql_python-1.2.1 → chemql_python-1.2.3}/src/chemql_python.egg-info/dependency_links.txt +0 -0
- {chemql_python-1.2.1 → chemql_python-1.2.3}/src/chemql_python.egg-info/entry_points.txt +0 -0
- {chemql_python-1.2.1 → chemql_python-1.2.3}/src/chemql_python.egg-info/requires.txt +0 -0
- {chemql_python-1.2.1 → chemql_python-1.2.3}/src/chemql_python.egg-info/top_level.txt +0 -0
- {chemql_python-1.2.1 → chemql_python-1.2.3}/tests/test_external_molecule_lookup.py +0 -0
- {chemql_python-1.2.1 → chemql_python-1.2.3}/tests/test_public_api.py +0 -0
- {chemql_python-1.2.1 → chemql_python-1.2.3}/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.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.
|
|
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.
|
|
28
|
-
> The examples below describe the ChemQL 1.2.
|
|
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.
|
|
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.
|
|
836
|
+
chemql --upgrade 1.2.3
|
|
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.3 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.3\
|
|
15
|
+
> The examples below describe the ChemQL 1.2.3 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.3 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.3
|
|
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.3"
|
|
49
51
|
|
|
50
52
|
# Persistent preprocessor/function state for the current ChemQL session.
|
|
51
53
|
DEFINITIONS = {}
|
|
@@ -53,6 +55,7 @@ FUNCTIONS = {}
|
|
|
53
55
|
MACROS = {}
|
|
54
56
|
EXECUTION_HISTORY = []
|
|
55
57
|
_DEFINITION_NAME = r"[A-Za-z_][A-Za-z0-9_]*"
|
|
58
|
+
_NO_RETURN = object()
|
|
56
59
|
|
|
57
60
|
def _expand_definitions(line):
|
|
58
61
|
"""Replace @NAME literally, including inside strings, using current definitions."""
|
|
@@ -107,111 +110,203 @@ def _split_function_arguments(source):
|
|
|
107
110
|
return args
|
|
108
111
|
|
|
109
112
|
def _call_function(name, arguments):
|
|
110
|
-
"""Call a ChemQL function, including
|
|
113
|
+
"""Call a ChemQL function, including let assignments and block syntax."""
|
|
111
114
|
if name not in FUNCTIONS:
|
|
112
115
|
raise NameError(f"Unknown ChemQL function `{name}`")
|
|
113
116
|
|
|
114
117
|
values = [evaluate_python(arg) for arg in arguments]
|
|
115
118
|
definition = FUNCTIONS[name]
|
|
119
|
+
|
|
116
120
|
saved_globals = python_globals.copy()
|
|
117
121
|
saved_chemql_variables = CHEMQL_VARIABLES.copy()
|
|
122
|
+
|
|
118
123
|
had_arguments = "arguments" in python_globals
|
|
119
124
|
saved_arguments = python_globals.get("arguments")
|
|
120
|
-
|
|
125
|
+
|
|
126
|
+
# Expose only the supplied arguments.
|
|
121
127
|
python_globals["arguments"] = list(values)
|
|
122
128
|
|
|
123
129
|
def substitute_parameters(source):
|
|
124
|
-
|
|
130
|
+
"""Replace \\1, \\2, etc. without corrupting \\10 or longer indices."""
|
|
125
131
|
return re.sub(
|
|
126
132
|
r"\\(\d+)",
|
|
127
|
-
lambda match:
|
|
128
|
-
|
|
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
|
+
),
|
|
129
138
|
source,
|
|
130
139
|
)
|
|
131
140
|
|
|
132
141
|
def normalize_let_line(source):
|
|
133
|
-
"""Translate
|
|
134
|
-
match = re.fullmatch(
|
|
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
|
+
|
|
135
148
|
if not match:
|
|
136
149
|
return source
|
|
150
|
+
|
|
137
151
|
variable, expression = match.groups()
|
|
138
152
|
expression = substitute_parameters(expression)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
#
|
|
153
|
+
expression = _expand_definitions(expression)
|
|
154
|
+
|
|
155
|
+
# Translate simple ChemQL [variable] expressions into (variable).
|
|
142
156
|
expression = re.sub(
|
|
143
|
-
r"\[\s*([A-Za-z_]
|
|
157
|
+
r"\[\s*([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)\s*\]",
|
|
144
158
|
r"(\1)",
|
|
145
159
|
expression,
|
|
146
160
|
)
|
|
161
|
+
|
|
147
162
|
return f"[{variable} = {expression}]"
|
|
148
163
|
|
|
149
|
-
|
|
150
|
-
|
|
164
|
+
def execute_function_lines(lines):
|
|
165
|
+
"""Execute function statements and propagate return values."""
|
|
151
166
|
i = 0
|
|
152
|
-
|
|
153
|
-
|
|
167
|
+
|
|
168
|
+
while i < len(lines):
|
|
169
|
+
line = lines[i].strip()
|
|
170
|
+
|
|
154
171
|
if not line:
|
|
155
172
|
i += 1
|
|
156
173
|
continue
|
|
157
174
|
|
|
158
|
-
#
|
|
175
|
+
# Multiline Python block.
|
|
159
176
|
if line == "[[[":
|
|
160
177
|
block = []
|
|
161
178
|
i += 1
|
|
162
|
-
|
|
163
|
-
|
|
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)
|
|
164
184
|
i += 1
|
|
165
|
-
|
|
166
|
-
|
|
185
|
+
|
|
186
|
+
if i >= len(lines):
|
|
187
|
+
raise SyntaxError(
|
|
188
|
+
f"Missing `]]]` in function `{name}`"
|
|
189
|
+
)
|
|
190
|
+
|
|
167
191
|
evaluate_python("\n".join(block))
|
|
168
192
|
i += 1
|
|
169
193
|
continue
|
|
170
194
|
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
|
|
174
|
-
|
|
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
|
+
|
|
175
206
|
block_body = [
|
|
176
207
|
normalize_let_line(substitute_parameters(source_line))
|
|
177
|
-
for source_line in
|
|
208
|
+
for source_line in lines[i + 1:end]
|
|
178
209
|
]
|
|
210
|
+
|
|
179
211
|
code = line[1:-1].strip()
|
|
212
|
+
|
|
180
213
|
if code.startswith("for "):
|
|
181
214
|
execute_for_block(line, block_body)
|
|
215
|
+
|
|
182
216
|
elif code.startswith("while "):
|
|
183
217
|
execute_while_block(line, block_body)
|
|
218
|
+
|
|
184
219
|
elif code.startswith("if "):
|
|
185
|
-
|
|
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
|
+
|
|
186
255
|
else:
|
|
187
|
-
raise SyntaxError(
|
|
256
|
+
raise SyntaxError(
|
|
257
|
+
f"Unsupported function block `{code}`"
|
|
258
|
+
)
|
|
259
|
+
|
|
188
260
|
i = end + 1
|
|
189
261
|
continue
|
|
190
262
|
|
|
191
263
|
if line == "[endblock]":
|
|
192
|
-
raise SyntaxError(
|
|
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()
|
|
193
271
|
|
|
194
|
-
if line.startswith("return"):
|
|
195
|
-
expression = line[len("return"):].strip()
|
|
196
272
|
if expression.startswith("[") and expression.endswith("]"):
|
|
197
|
-
expression = expression[1:-1]
|
|
273
|
+
expression = expression[1:-1].strip()
|
|
274
|
+
|
|
198
275
|
expression = substitute_parameters(expression)
|
|
276
|
+
expression = _expand_definitions(expression)
|
|
277
|
+
|
|
199
278
|
return evaluate_python(expression) if expression else None
|
|
200
279
|
|
|
201
280
|
normalized = normalize_let_line(line)
|
|
281
|
+
|
|
202
282
|
if normalized != line:
|
|
203
|
-
# The wrapper is ChemQL syntax; pass the assignment itself to Python.
|
|
204
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)
|
|
205
289
|
else:
|
|
206
|
-
|
|
290
|
+
expression = substitute_parameters(line)
|
|
291
|
+
expression = _expand_definitions(expression)
|
|
292
|
+
evaluate_python(expression)
|
|
293
|
+
|
|
207
294
|
i += 1
|
|
208
|
-
|
|
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
|
+
|
|
209
302
|
finally:
|
|
210
|
-
#
|
|
303
|
+
# Restore the caller's namespaces.
|
|
211
304
|
python_globals.clear()
|
|
212
305
|
python_globals.update(saved_globals)
|
|
306
|
+
|
|
213
307
|
CHEMQL_VARIABLES.clear()
|
|
214
308
|
CHEMQL_VARIABLES.update(saved_chemql_variables)
|
|
309
|
+
|
|
215
310
|
if had_arguments:
|
|
216
311
|
python_globals["arguments"] = saved_arguments
|
|
217
312
|
else:
|
|
@@ -258,17 +353,15 @@ _shell_process = None
|
|
|
258
353
|
_shell_master_fd = None
|
|
259
354
|
_shell_prompt_ready = False
|
|
260
355
|
_shell_output_streamed = False
|
|
356
|
+
_shell_output_queue = None
|
|
357
|
+
_shell_reader_thread = None
|
|
261
358
|
_shell_prompt = f"__CHEMQL_PROMPT_{uuid.uuid4().hex}__"
|
|
262
359
|
|
|
263
360
|
|
|
264
361
|
def get_shell():
|
|
265
|
-
"""Return the persistent shell process.
|
|
266
|
-
|
|
267
|
-
On POSIX, use a pseudo-terminal so foreground commands such as `read`,
|
|
268
|
-
`read -p`, and interactive CLIs can receive keyboard input. Windows keeps
|
|
269
|
-
the pipe-based implementation until a ConPTY backend is added.
|
|
270
|
-
"""
|
|
362
|
+
"""Return the persistent OS shell process for the current platform."""
|
|
271
363
|
global _shell_process, _shell_master_fd, _shell_prompt_ready
|
|
364
|
+
global _shell_output_queue, _shell_reader_thread
|
|
272
365
|
|
|
273
366
|
if _shell_process is not None and _shell_process.poll() is None:
|
|
274
367
|
return _shell_process
|
|
@@ -277,14 +370,38 @@ def get_shell():
|
|
|
277
370
|
close_shell()
|
|
278
371
|
|
|
279
372
|
if os.name == "nt":
|
|
373
|
+
# Use PowerShell as ChemQL's native Windows shell. Its prompt function
|
|
374
|
+
# emits a private marker when a command finishes, so we do not append a
|
|
375
|
+
# marker to stdin (which would accidentally become Read-Host input).
|
|
376
|
+
setup = (
|
|
377
|
+
f"function global:prompt {{ [Console]::Out.WriteLine('{_shell_prompt}'); return '' }}"
|
|
378
|
+
)
|
|
280
379
|
_shell_process = subprocess.Popen(
|
|
281
|
-
["
|
|
380
|
+
["powershell.exe", "-NoLogo", "-NoProfile", "-NoExit", "-Command", setup],
|
|
282
381
|
stdin=subprocess.PIPE,
|
|
283
382
|
stdout=subprocess.PIPE,
|
|
284
383
|
stderr=subprocess.STDOUT,
|
|
285
384
|
text=True,
|
|
286
385
|
bufsize=1,
|
|
287
386
|
)
|
|
387
|
+
output_queue = queue.Queue()
|
|
388
|
+
_shell_output_queue = output_queue
|
|
389
|
+
process = _shell_process
|
|
390
|
+
|
|
391
|
+
def read_output():
|
|
392
|
+
try:
|
|
393
|
+
while True:
|
|
394
|
+
character = process.stdout.read(1) # type: ignore
|
|
395
|
+
if not character:
|
|
396
|
+
break
|
|
397
|
+
output_queue.put(character)
|
|
398
|
+
except (OSError, ValueError):
|
|
399
|
+
pass
|
|
400
|
+
finally:
|
|
401
|
+
output_queue.put(None)
|
|
402
|
+
|
|
403
|
+
_shell_reader_thread = threading.Thread(target=read_output, daemon=True)
|
|
404
|
+
_shell_reader_thread.start()
|
|
288
405
|
else:
|
|
289
406
|
import fcntl
|
|
290
407
|
import pty
|
|
@@ -434,47 +551,128 @@ def _run_shell_command_posix(command, marker=None):
|
|
|
434
551
|
return f"Shell error: {exc}"
|
|
435
552
|
|
|
436
553
|
|
|
437
|
-
def
|
|
554
|
+
def _run_shell_command_windows(command):
|
|
555
|
+
"""Run a command in persistent PowerShell and forward line input interactively."""
|
|
556
|
+
global _shell_prompt_ready, _shell_output_streamed
|
|
438
557
|
shell = get_shell()
|
|
439
|
-
|
|
440
|
-
|
|
558
|
+
output_queue = _shell_output_queue
|
|
559
|
+
if output_queue is None:
|
|
560
|
+
return "Shell error: PowerShell output reader was not initialized."
|
|
441
561
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
562
|
+
import msvcrt
|
|
563
|
+
|
|
564
|
+
def wait_for_prompt(stream_output):
|
|
565
|
+
collected = ""
|
|
566
|
+
streamed_pos = 0
|
|
567
|
+
marker = _shell_prompt
|
|
568
|
+
marker_len = len(marker)
|
|
569
|
+
stdin_is_tty = False
|
|
570
|
+
try:
|
|
571
|
+
stdin_is_tty = sys.stdin is not None and sys.stdin.isatty()
|
|
572
|
+
except (AttributeError, OSError, ValueError):
|
|
573
|
+
pass
|
|
574
|
+
pending_input = ""
|
|
445
575
|
|
|
446
|
-
# Windows pipe mode supports persistent commands, but not fully
|
|
447
|
-
# interactive programs yet; a ConPTY backend is needed for that.
|
|
448
|
-
shell.stdin.write(command + "\r\n" + f"echo {marker}\r\n") # type: ignore
|
|
449
|
-
shell.stdin.flush() # type: ignore
|
|
450
|
-
output = []
|
|
451
576
|
while True:
|
|
577
|
+
# While a shell command is running, pass complete lines typed by the
|
|
578
|
+
# user to PowerShell. This lets Read-Host receive real input rather
|
|
579
|
+
# than consuming a completion marker injected by ChemQL.
|
|
580
|
+
if stdin_is_tty:
|
|
581
|
+
try:
|
|
582
|
+
while msvcrt.kbhit(): #type:ignore
|
|
583
|
+
key = msvcrt.getwch() #type:ignore
|
|
584
|
+
if key in ("\x00", "\xe0"): #type:ignore
|
|
585
|
+
# Consume the second half of a Windows special key.
|
|
586
|
+
if msvcrt.kbhit(): #type:ignore
|
|
587
|
+
msvcrt.getwch() #type:ignore
|
|
588
|
+
elif key in ("\r", "\n"):
|
|
589
|
+
shell.stdin.write(pending_input + "\r\n") # type: ignore
|
|
590
|
+
shell.stdin.flush() # type: ignore
|
|
591
|
+
pending_input = ""
|
|
592
|
+
elif key == "\b":
|
|
593
|
+
pending_input = pending_input[:-1]
|
|
594
|
+
elif key == "\x03":
|
|
595
|
+
# Ctrl-C is sent as a line input; reset is safer than
|
|
596
|
+
# leaving ChemQL stuck in a nested interactive prompt.
|
|
597
|
+
close_shell()
|
|
598
|
+
return None
|
|
599
|
+
elif key >= " " and key != "\x7f":
|
|
600
|
+
pending_input += key
|
|
601
|
+
except (OSError, ValueError):
|
|
602
|
+
stdin_is_tty = False
|
|
603
|
+
|
|
452
604
|
try:
|
|
453
|
-
|
|
454
|
-
except
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
return "Shell command interrupted. Shell session was reset."
|
|
459
|
-
if not line:
|
|
460
|
-
return "\n".join(output) or "Shell session terminated."
|
|
461
|
-
line = line.rstrip("\r\n")
|
|
462
|
-
if marker in line:
|
|
463
|
-
break
|
|
464
|
-
output.append(line)
|
|
465
|
-
return "\n".join(output).strip()
|
|
605
|
+
character = output_queue.get(timeout=0.04)
|
|
606
|
+
except queue.Empty:
|
|
607
|
+
continue
|
|
608
|
+
if character is None:
|
|
609
|
+
return None
|
|
466
610
|
|
|
611
|
+
collected += character
|
|
612
|
+
marker_pos = collected.find(marker)
|
|
613
|
+
if marker_pos >= 0:
|
|
614
|
+
visible = collected[:marker_pos]
|
|
615
|
+
if stream_output and marker_pos > streamed_pos:
|
|
616
|
+
sys.stdout.write(collected[streamed_pos:marker_pos])
|
|
617
|
+
sys.stdout.flush()
|
|
618
|
+
_shell_output_streamed = True
|
|
619
|
+
return visible.replace("\r\n", "\n").rstrip("\r\n ")
|
|
620
|
+
|
|
621
|
+
if stream_output:
|
|
622
|
+
# Stream immediately, retaining the full output for history and
|
|
623
|
+
# only withholding a suffix that could begin the prompt marker.
|
|
624
|
+
overlap = 0
|
|
625
|
+
max_overlap = min(marker_len - 1, len(collected))
|
|
626
|
+
for size in range(max_overlap, 0, -1):
|
|
627
|
+
if collected.endswith(marker[:size]):
|
|
628
|
+
overlap = size
|
|
629
|
+
break
|
|
630
|
+
safe_end = len(collected) - overlap
|
|
631
|
+
if safe_end > streamed_pos:
|
|
632
|
+
sys.stdout.write(collected[streamed_pos:safe_end])
|
|
633
|
+
sys.stdout.flush()
|
|
634
|
+
_shell_output_streamed = True
|
|
635
|
+
streamed_pos = safe_end
|
|
636
|
+
|
|
637
|
+
if not _shell_prompt_ready:
|
|
638
|
+
initial = wait_for_prompt(stream_output=False)
|
|
639
|
+
if initial is None:
|
|
640
|
+
close_shell()
|
|
641
|
+
return "Shell session terminated."
|
|
642
|
+
_shell_prompt_ready = True
|
|
643
|
+
|
|
644
|
+
try:
|
|
645
|
+
shell.stdin.write(command.replace("\n", "\r\n") + "\r\n") # type: ignore
|
|
646
|
+
shell.stdin.flush() # type: ignore
|
|
647
|
+
result = wait_for_prompt(stream_output=True)
|
|
648
|
+
if result is None:
|
|
649
|
+
close_shell()
|
|
650
|
+
return "Shell session terminated."
|
|
651
|
+
return result
|
|
652
|
+
except (BrokenPipeError, OSError, ValueError) as exc:
|
|
653
|
+
close_shell()
|
|
654
|
+
return f"Shell error: {exc}"
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def run_shell_command(command):
|
|
658
|
+
global _shell_output_streamed
|
|
659
|
+
command = command.replace("\r\n", "\n").replace("\r", "\n").rstrip("\n")
|
|
660
|
+
try:
|
|
661
|
+
if os.name == "nt":
|
|
662
|
+
if command.strip().casefold() in {"powershell", "powershell.exe", "pwsh", "pwsh.exe"}:
|
|
663
|
+
return "ChemQL already uses PowerShell on Windows; run the command directly instead of starting a nested shell."
|
|
664
|
+
_shell_output_streamed = False
|
|
665
|
+
return _run_shell_command_windows(command)
|
|
666
|
+
return _run_shell_command_posix(command)
|
|
467
667
|
except KeyboardInterrupt:
|
|
468
668
|
close_shell()
|
|
469
669
|
return "Shell command interrupted. Shell session was reset."
|
|
470
|
-
except (BrokenPipeError, OSError) as exc:
|
|
471
|
-
close_shell()
|
|
472
|
-
return f"Shell error: {exc}"
|
|
473
670
|
|
|
474
671
|
|
|
475
672
|
def close_shell():
|
|
476
673
|
"""Clean up the persistent shell and its pseudo-terminal on exit."""
|
|
477
674
|
global _shell_process, _shell_master_fd, _shell_prompt_ready
|
|
675
|
+
global _shell_output_queue, _shell_reader_thread
|
|
478
676
|
|
|
479
677
|
if _shell_process is not None:
|
|
480
678
|
if _shell_process.poll() is None:
|
|
@@ -496,6 +694,8 @@ def close_shell():
|
|
|
496
694
|
pass
|
|
497
695
|
_shell_master_fd = None
|
|
498
696
|
_shell_prompt_ready = False
|
|
697
|
+
_shell_output_queue = None
|
|
698
|
+
_shell_reader_thread = None
|
|
499
699
|
|
|
500
700
|
|
|
501
701
|
atexit.register(close_shell)
|
|
@@ -2486,7 +2686,7 @@ def execute_if_block(header, body):
|
|
|
2486
2686
|
)
|
|
2487
2687
|
|
|
2488
2688
|
for section_header, section_body in sections:
|
|
2489
|
-
code = section_header[1:-1].strip()
|
|
2689
|
+
code = _expand_definitions(section_header[1:-1].strip())
|
|
2490
2690
|
|
|
2491
2691
|
if code.startswith("if "):
|
|
2492
2692
|
condition = code[3:].strip()
|
|
@@ -3229,7 +3429,7 @@ def execute(arguments, query_text=None, positions=None):
|
|
|
3229
3429
|
|
|
3230
3430
|
if arguments[0].lower() == "macros":
|
|
3231
3431
|
return "\n".join([f"@{name} = {value}" for name, value in MACROS.items()]) if MACROS else "No macros."
|
|
3232
|
-
|
|
3432
|
+
|
|
3233
3433
|
if arguments[0] == "conditions":
|
|
3234
3434
|
if len(arguments) != 1:
|
|
3235
3435
|
return "Usage: conditions"
|
|
@@ -3584,7 +3784,7 @@ class ChemqlLexer(Lexer):
|
|
|
3584
3784
|
"findmol", "findre", "set", "add", "remove", "react",
|
|
3585
3785
|
"balance", "conditions", "view", "help", "dump", "version",
|
|
3586
3786
|
"return", "union", "intersection", "let", "unset", "variables",
|
|
3587
|
-
"macros", "definitions", "arguments"
|
|
3787
|
+
"macros", "definitions", "arguments",
|
|
3588
3788
|
}
|
|
3589
3789
|
|
|
3590
3790
|
OPERATORS = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chemql-python
|
|
3
|
-
Version: 1.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.
|
|
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.
|
|
28
|
-
> The examples below describe the ChemQL 1.2.
|
|
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.
|
|
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.
|
|
836
|
+
chemql --upgrade 1.2.3
|
|
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
|