chemql-python 1.2.4__tar.gz → 1.2.5__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.
Files changed (24) hide show
  1. {chemql_python-1.2.4/src/chemql_python.egg-info → chemql_python-1.2.5}/PKG-INFO +6 -6
  2. {chemql_python-1.2.4 → chemql_python-1.2.5}/README.md +5 -5
  3. {chemql_python-1.2.4 → chemql_python-1.2.5}/pyproject.toml +1 -1
  4. {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql/__init__.py +283 -100
  5. {chemql_python-1.2.4 → chemql_python-1.2.5/src/chemql_python.egg-info}/PKG-INFO +6 -6
  6. {chemql_python-1.2.4 → chemql_python-1.2.5}/LICENSE +0 -0
  7. {chemql_python-1.2.4 → chemql_python-1.2.5}/setup.cfg +0 -0
  8. {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql/__main__.py +0 -0
  9. {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql/data/BondEnergies.json +0 -0
  10. {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql/data/MoleculeBonds.json +0 -0
  11. {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql/data/Molecules.json +0 -0
  12. {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql/data/PeriodicTableJSON.json +0 -0
  13. {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql/data/Reactions.json +0 -0
  14. {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql/data/THIRD_PARTY_LICENSES/PeriodicTableJSON.txt +0 -0
  15. {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql/data/THIRD_PARTY_LICENSES/RHEA.txt +0 -0
  16. {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql/html/periodic_table.html +0 -0
  17. {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql_python.egg-info/SOURCES.txt +0 -0
  18. {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql_python.egg-info/dependency_links.txt +0 -0
  19. {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql_python.egg-info/entry_points.txt +0 -0
  20. {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql_python.egg-info/requires.txt +0 -0
  21. {chemql_python-1.2.4 → chemql_python-1.2.5}/src/chemql_python.egg-info/top_level.txt +0 -0
  22. {chemql_python-1.2.4 → chemql_python-1.2.5}/tests/test_external_molecule_lookup.py +0 -0
  23. {chemql_python-1.2.4 → chemql_python-1.2.5}/tests/test_public_api.py +0 -0
  24. {chemql_python-1.2.4 → chemql_python-1.2.5}/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.4
3
+ Version: 1.2.5
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.3 combines database-style
20
+ elements, molecules, and reactions. Version 1.2.4 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.3\
28
- > The examples below describe the ChemQL 1.2.3 source. Dataset
27
+ > **Version covered by this README:** 1.2.4\
28
+ > The examples below describe the ChemQL 1.2.4 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.3 adds three ways to reuse values and logic. These declarations
541
+ ChemQL 1.2.5 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.3
836
+ chemql --upgrade 1.2.4
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.3 combines database-style
7
+ elements, molecules, and reactions. Version 1.2.4 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.3\
15
- > The examples below describe the ChemQL 1.2.3 source. Dataset
14
+ > **Version covered by this README:** 1.2.4\
15
+ > The examples below describe the ChemQL 1.2.4 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.3 adds three ways to reuse values and logic. These declarations
528
+ ChemQL 1.2.5 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.3
823
+ chemql --upgrade 1.2.4
824
824
  ```
825
825
 
826
826
  `--upgrade` installs the latest published `chemql-python` release; supplying a
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "chemql-python"
7
- version = "1.2.4"
7
+ version = "1.2.5"
8
8
  authors = [
9
9
  { name = "Mohammad Rakibul Islam", email = "rakibulofficial1017@gmail.com" }
10
10
  ]
@@ -1,33 +1,33 @@
1
- import json
2
- import math
3
- import shlex
4
- import os
5
- import re
6
- import ast
7
1
  import argparse
8
- import time
9
- import subprocess
10
- import sys
2
+ import ast
3
+ import atexit
11
4
  import contextlib
5
+ from fractions import Fraction
6
+ from functools import reduce
12
7
  import io
8
+ import json
9
+ import math
10
+ from math import gcd
11
+ import os
13
12
  from pathlib import Path
14
13
  from prompt_toolkit import PromptSession
15
14
  from prompt_toolkit.key_binding import KeyBindings
16
15
  from prompt_toolkit.keys import Keys
17
16
  from prompt_toolkit.lexers import Lexer
18
- import atexit
19
- import uuid
20
- import select
21
- import queue
22
- import threading
23
17
  from prompt_toolkit.styles import Style
24
18
  from prompt_toolkit.completion import Completer, Completion
19
+ import queue
20
+ import re
21
+ import shlex
22
+ import subprocess
23
+ import sys
24
+ import select
25
+ import time
26
+ import threading
27
+ import uuid
25
28
  import urllib.parse
26
29
  import urllib.request
27
30
  import urllib.error
28
- from fractions import Fraction
29
- from functools import reduce
30
- from math import gcd
31
31
 
32
32
 
33
33
  __all__ = [
@@ -47,7 +47,7 @@ __all__ = [
47
47
  "__version__"
48
48
  ]
49
49
 
50
- __version__ = "1.2.4"
50
+ __version__ = "1.2.5"
51
51
 
52
52
  # Persistent preprocessor/function state for the current ChemQL session.
53
53
  DEFINITIONS = {}
@@ -367,13 +367,13 @@ _shell_prompt_ready = False
367
367
  _shell_output_streamed = False
368
368
  _shell_output_queue = None
369
369
  _shell_reader_thread = None
370
+ _shell_script_path = None
370
371
  _shell_prompt = f"__CHEMQL_PROMPT_{uuid.uuid4().hex}__"
371
372
 
372
-
373
373
  def get_shell():
374
374
  """Return the persistent OS shell process for the current platform."""
375
375
  global _shell_process, _shell_master_fd, _shell_prompt_ready
376
- global _shell_output_queue, _shell_reader_thread
376
+ global _shell_output_queue, _shell_reader_thread, _shell_script_path
377
377
 
378
378
  if _shell_process is not None and _shell_process.poll() is None:
379
379
  return _shell_process
@@ -382,20 +382,89 @@ def get_shell():
382
382
  close_shell()
383
383
 
384
384
  if os.name == "nt":
385
- # Use PowerShell as ChemQL's native Windows shell. Its prompt function
386
- # emits a private marker when a command finishes, so we do not append a
387
- # marker to stdin (which would accidentally become Read-Host input).
388
- setup = (
389
- f"function global:prompt {{ [Console]::Out.WriteLine('{_shell_prompt}'); return '' }}"
385
+ # PowerShell reads its program from a file, leaving stdin available for
386
+ # the command/marker protocol and commands such as Read-Host.
387
+ import tempfile
388
+
389
+ setup = r'''$ErrorActionPreference = 'Continue'
390
+ [Console]::InputEncoding = New-Object System.Text.UTF8Encoding($false)
391
+ [Console]::OutputEncoding = New-Object System.Text.UTF8Encoding($false)
392
+ $OutputEncoding = New-Object System.Text.UTF8Encoding($false)
393
+ [Console]::Out.WriteLine('__CHEMQL_READY__')
394
+ [Console]::Out.Flush()
395
+
396
+ while ($true) {
397
+ # One physical input line per request avoids command/marker line desync.
398
+ $request = [Console]::In.ReadLine()
399
+ if ($null -eq $request) { break }
400
+
401
+ $separatorIndex = $request.IndexOf('|')
402
+ if ($separatorIndex -le 0) {
403
+ [Console]::Out.WriteLine('CHEMQL_PROTOCOL_ERROR: malformed request')
404
+ [Console]::Out.Flush()
405
+ continue
406
+ }
407
+
408
+ $marker = $request.Substring(0, $separatorIndex)
409
+ $encodedCommand = $request.Substring($separatorIndex + 1)
410
+
411
+ try {
412
+ $commandText = [System.Text.Encoding]::UTF8.GetString(
413
+ [System.Convert]::FromBase64String($encodedCommand)
390
414
  )
415
+ if ([string]::IsNullOrWhiteSpace($commandText)) {
416
+ [Console]::Out.WriteLine('ChemQL received an empty PowerShell command.')
417
+ }
418
+ else {
419
+ # Invoke-Expression runs in this persistent script scope, preserving
420
+ # PowerShell variables between ChemQL requests. Format pipeline
421
+ # output explicitly for each request; otherwise PowerShell's default
422
+ # formatting subsystem can defer table output until the next request,
423
+ # making commands such as Get-Location appear blank or duplicated.
424
+ $formattedOutput = Invoke-Expression -Command $commandText | Out-String -Width 4096
425
+ if (-not [string]::IsNullOrEmpty($formattedOutput)) {
426
+ [Console]::Out.Write($formattedOutput)
427
+ }
428
+ }
429
+ }
430
+ catch {
431
+ [Console]::Out.WriteLine($_.ToString())
432
+ }
433
+ finally {
434
+ [Console]::Out.WriteLine($marker)
435
+ [Console]::Out.Flush()
436
+ }
437
+ }
438
+ ''' # I have no idea what this is, this is code I wrote today at 4:00AM but it somehow works
439
+ # reminder to future self to not touch this
440
+ fd, _shell_script_path = tempfile.mkstemp(prefix="chemql_shell_", suffix=".ps1")
441
+ try:
442
+ with os.fdopen(fd, "w", encoding="ascii", newline="\n") as script_file:
443
+ script_file.write(setup)
444
+ except Exception:
445
+ try:
446
+ os.close(fd)
447
+ except OSError:
448
+ pass
449
+ try:
450
+ os.unlink(_shell_script_path)
451
+ except OSError:
452
+ pass
453
+ _shell_script_path = None
454
+ raise
455
+
391
456
  _shell_process = subprocess.Popen(
392
- ["powershell.exe", "-NoLogo", "-NoProfile", "-NoExit", "-Command", setup],
457
+ ["powershell.exe", "-NoLogo", "-NoProfile",
458
+ "-ExecutionPolicy", "Bypass", "-File", _shell_script_path],
393
459
  stdin=subprocess.PIPE,
394
460
  stdout=subprocess.PIPE,
395
461
  stderr=subprocess.STDOUT,
396
462
  text=True,
463
+ encoding="utf-8",
464
+ errors="replace",
397
465
  bufsize=1,
398
466
  )
467
+
399
468
  output_queue = queue.Queue()
400
469
  _shell_output_queue = output_queue
401
470
  process = _shell_process
@@ -414,30 +483,67 @@ def get_shell():
414
483
 
415
484
  _shell_reader_thread = threading.Thread(target=read_output, daemon=True)
416
485
  _shell_reader_thread.start()
486
+
487
+ # Wait briefly for the PowerShell script to confirm it started. This
488
+ # prevents the first request from racing process startup and diagnoses
489
+ # startup failures instead of hanging later while waiting for a marker.
490
+ ready_text = ""
491
+ ready_deadline = time.monotonic() + 5
492
+ while "__CHEMQL_READY__" not in ready_text:
493
+ remaining = ready_deadline - time.monotonic()
494
+ if remaining <= 0:
495
+ startup_output = ready_text.replace("\\r\\n", "\\n").strip()
496
+ close_shell()
497
+ raise RuntimeError(
498
+ "PowerShell request loop did not start within 5 seconds."
499
+ + (f" Output: {startup_output}" if startup_output else "")
500
+ )
501
+ try:
502
+ character = output_queue.get(timeout=min(0.1, remaining))
503
+ except queue.Empty:
504
+ if _shell_process.poll() is not None:
505
+ raise RuntimeError(
506
+ "PowerShell exited during startup with code "
507
+ f"{_shell_process.returncode}."
508
+ )
509
+ continue
510
+ if character is None:
511
+ raise RuntimeError("PowerShell closed its output during startup.")
512
+ ready_text += character
513
+ ready_pos = ready_text.find("__CHEMQL_READY__")
514
+ # Any text before the readiness token is startup diagnostics. It is
515
+ # not command output and should not leak into the first request.
516
+ startup_extra = ready_text[:ready_pos]
517
+ if startup_extra.strip():
518
+ # Preserve startup diagnostics for the next request to display.
519
+ for char in startup_extra:
520
+ output_queue.put(char)
521
+
522
+ # Windows PowerShell uses the explicit command/marker protocol above.
523
+ # Do not run the POSIX PTY prompt handshake below: it waits for a shell
524
+ # prompt that this non-interactive PowerShell script intentionally has
525
+ # disabled, causing get_shell() to hang before the first command.
526
+ return _shell_process
527
+
417
528
  else:
529
+ # Leave the existing POSIX implementation unchanged.
418
530
  import fcntl
419
531
  import pty
420
532
  import termios
421
533
 
422
534
  master_fd, slave_fd = pty.openpty()
423
- # Don't echo bytes sent by ChemQL or keyboard input twice. Keep ISIG
424
- # enabled so forwarding Ctrl-C to the PTY interrupts the foreground job.
425
535
  attrs = termios.tcgetattr(slave_fd)
426
536
  attrs[3] &= ~termios.ECHO
427
537
  termios.tcsetattr(slave_fd, termios.TCSANOW, attrs)
428
538
 
429
539
  def attach_controlling_terminal():
430
- # Popen(start_new_session=True) has already called setsid().
431
540
  fcntl.ioctl(slave_fd, termios.TIOCSCTTY, 0)
432
541
 
433
542
  try:
434
543
  _shell_process = subprocess.Popen(
435
544
  ["bash", "--noprofile", "--norc"],
436
- stdin=slave_fd,
437
- stdout=slave_fd,
438
- stderr=slave_fd,
439
- close_fds=True,
440
- start_new_session=True,
545
+ stdin=slave_fd, stdout=slave_fd, stderr=slave_fd,
546
+ close_fds=True, start_new_session=True,
441
547
  preexec_fn=attach_controlling_terminal,
442
548
  env={**os.environ, "PS1": _shell_prompt, "PS2": ""},
443
549
  )
@@ -451,7 +557,6 @@ def get_shell():
451
557
 
452
558
  return _shell_process
453
559
 
454
-
455
560
  def _run_shell_command_posix(command, marker=None):
456
561
  """Run a command through the persistent PTY and forward keyboard input.
457
562
 
@@ -469,6 +574,7 @@ def _run_shell_command_posix(command, marker=None):
469
574
  prompt_bytes = _shell_prompt.encode()
470
575
 
471
576
  def wait_for_prompt(forward_keyboard=False):
577
+ global _shell_output_streamed
472
578
  collected = bytearray()
473
579
  streamed_pos = 0
474
580
  stdin_fd = None
@@ -481,7 +587,7 @@ def _run_shell_command_posix(command, marker=None):
481
587
  while True:
482
588
  readers = [master_fd]
483
589
  if stdin_fd is not None:
484
- readers.append(stdin_fd)
590
+ readers.append(stdin_fd) #type:ignore
485
591
  try:
486
592
  ready, _, _ = select.select(readers, [], [])
487
593
  except KeyboardInterrupt:
@@ -562,129 +668,178 @@ def _run_shell_command_posix(command, marker=None):
562
668
  close_shell()
563
669
  return f"Shell error: {exc}"
564
670
 
565
-
566
671
  def _run_shell_command_windows(command):
567
- """Run a command in persistent PowerShell and forward line input interactively."""
568
- global _shell_prompt_ready, _shell_output_streamed
672
+ """Execute one command in the persistent PowerShell request loop."""
673
+ import base64
674
+ import msvcrt
675
+ import uuid
676
+
569
677
  shell = get_shell()
570
678
  output_queue = _shell_output_queue
571
679
  if output_queue is None:
572
680
  return "Shell error: PowerShell output reader was not initialized."
681
+ if shell.stdin is None:
682
+ return "Shell error: PowerShell stdin is unavailable."
573
683
 
574
- import msvcrt
684
+ marker = f"__CHEMQL_DONE_{uuid.uuid4().hex}__"
685
+ encoded_command = base64.b64encode(command.encode("utf-8")).decode("ascii")
686
+ timeout_seconds=120
687
+ def wait_for_marker(timeout_seconds=120):
688
+ """Collect output while streaming prompts so interactive commands remain usable."""
689
+ global _shell_output_streamed
575
690
 
576
- def wait_for_prompt(stream_output):
577
691
  collected = ""
578
692
  streamed_pos = 0
579
- marker = _shell_prompt
580
- marker_len = len(marker)
581
- stdin_is_tty = False
693
+ started = time.monotonic()
582
694
  try:
583
695
  stdin_is_tty = sys.stdin is not None and sys.stdin.isatty()
584
696
  except (AttributeError, OSError, ValueError):
585
- pass
697
+ stdin_is_tty = False
586
698
  pending_input = ""
587
699
 
700
+ def stream_until(end):
701
+ nonlocal streamed_pos
702
+ global _shell_output_streamed
703
+ if not stdin_is_tty or end <= streamed_pos:
704
+ return
705
+ visible = collected[streamed_pos:end]
706
+ if visible:
707
+ try:
708
+ sys.stdout.write(visible)
709
+ sys.stdout.flush()
710
+ _shell_output_streamed = True
711
+ except (OSError, UnicodeError):
712
+ pass
713
+ streamed_pos = end
714
+
588
715
  while True:
589
- # While a shell command is running, pass complete lines typed by the
590
- # user to PowerShell. This lets Read-Host receive real input rather
591
- # than consuming a completion marker injected by ChemQL.
716
+ if time.monotonic() - started > timeout_seconds:
717
+ return (None, collected, True)
718
+
592
719
  if stdin_is_tty:
593
720
  try:
594
- while msvcrt.kbhit(): #type:ignore
595
- key = msvcrt.getwch() #type:ignore
596
- if key in ("\x00", "\xe0"): #type:ignore
597
- # Consume the second half of a Windows special key.
598
- if msvcrt.kbhit(): #type:ignore
599
- msvcrt.getwch() #type:ignore
721
+ while msvcrt.kbhit(): # type: ignore
722
+ key = msvcrt.getwch() # type: ignore
723
+ if key in ("\x00", "\xe0"):
724
+ if msvcrt.kbhit(): # type: ignore
725
+ msvcrt.getwch() # type: ignore
600
726
  elif key in ("\r", "\n"):
601
- shell.stdin.write(pending_input + "\r\n") # type: ignore
727
+ shell.stdin.write(pending_input + "\n") # type: ignore # text mode normalizes newlines on Windows
602
728
  shell.stdin.flush() # type: ignore
729
+ sys.stdout.write("\n")
730
+ sys.stdout.flush()
603
731
  pending_input = ""
604
732
  elif key == "\b":
605
- pending_input = pending_input[:-1]
733
+ if pending_input:
734
+ pending_input = pending_input[:-1]
735
+ sys.stdout.write("\b \b")
736
+ sys.stdout.flush()
606
737
  elif key == "\x03":
607
- # Ctrl-C is sent as a line input; reset is safer than
608
- # leaving ChemQL stuck in a nested interactive prompt.
609
738
  close_shell()
610
- return None
611
- elif key >= " " and key != "\x7f":
739
+ return (None, collected, False)
740
+ elif key >= " " and key != "\x7f": # I have no idea what I did but it workd
612
741
  pending_input += key
742
+ sys.stdout.write(key)
743
+ sys.stdout.flush()
613
744
  except (OSError, ValueError):
614
745
  stdin_is_tty = False
615
746
 
616
747
  try:
617
748
  character = output_queue.get(timeout=0.04)
618
749
  except queue.Empty:
750
+ if shell.poll() is not None:
751
+ return (None, collected, False)
619
752
  continue
620
753
  if character is None:
621
- return None
754
+ return (None, collected, False)
622
755
 
623
756
  collected += character
624
757
  marker_pos = collected.find(marker)
625
758
  if marker_pos >= 0:
759
+ stream_until(marker_pos)
626
760
  visible = collected[:marker_pos]
627
- if stream_output and marker_pos > streamed_pos:
628
- sys.stdout.write(collected[streamed_pos:marker_pos])
629
- sys.stdout.flush()
630
- _shell_output_streamed = True
631
- return visible.replace("\r\n", "\n").rstrip("\r\n ")
632
-
633
- if stream_output:
634
- # Stream immediately, retaining the full output for history and
635
- # only withholding a suffix that could begin the prompt marker.
636
- overlap = 0
637
- max_overlap = min(marker_len - 1, len(collected))
638
- for size in range(max_overlap, 0, -1):
639
- if collected.endswith(marker[:size]):
640
- overlap = size
641
- break
642
- safe_end = len(collected) - overlap
643
- if safe_end > streamed_pos:
644
- sys.stdout.write(collected[streamed_pos:safe_end])
645
- sys.stdout.flush()
646
- _shell_output_streamed = True
647
- streamed_pos = safe_end
761
+ return (
762
+ visible.replace("\r\n", "\n").rstrip("\r\n "),
763
+ collected,
764
+ False,
765
+ )
648
766
 
649
- if not _shell_prompt_ready:
650
- initial = wait_for_prompt(stream_output=False)
651
- if initial is None:
652
- close_shell()
653
- return "Shell session terminated."
654
- _shell_prompt_ready = True
767
+ # Hold back a suffix that could be the beginning of the completion
768
+ # marker, so protocol text is never accidentally printed.
769
+ overlap = min(len(marker) - 1, len(collected))
770
+ while overlap and not collected.endswith(marker[:overlap]):
771
+ overlap -= 1
772
+ stream_until(len(collected) - overlap)
655
773
 
656
774
  try:
657
- shell.stdin.write(command.replace("\n", "\r\n") + "\r\n") # type: ignore
658
- shell.stdin.flush() # type: ignore
659
- result = wait_for_prompt(stream_output=True)
775
+ shell.stdin.write(marker + "|" + encoded_command + "\n") # text mode normalizes newlines on Windows
776
+ shell.stdin.flush()
777
+ result, collected, timed_out = wait_for_marker()
778
+ if timed_out:
779
+ close_shell()
780
+ debug = collected.replace("\r\n", "\n").rstrip()
781
+ detail = f"PowerShell did not return its completion marker within {timeout_seconds} seconds."
782
+ if debug:
783
+ detail += f"\nOutput received before timeout:\n{debug}"
784
+ else:
785
+ detail += "\nNo output was received. The PowerShell request loop may not have started or may not be reading stdin."
786
+ return detail
660
787
  if result is None:
661
788
  close_shell()
662
- return "Shell session terminated."
789
+ return "Shell session terminated before PowerShell returned its completion marker."
663
790
  return result
664
791
  except (BrokenPipeError, OSError, ValueError) as exc:
665
792
  close_shell()
666
793
  return f"Shell error: {exc}"
667
794
 
668
-
669
795
  def run_shell_command(command):
670
796
  global _shell_output_streamed
671
- command = command.replace("\r\n", "\n").replace("\r", "\n").rstrip("\n")
797
+
798
+ command = (
799
+ command.replace("\r\n", "\n")
800
+ .replace("\r", "\n")
801
+ .rstrip("\n")
802
+ )
803
+
672
804
  try:
673
805
  if os.name == "nt":
674
- if command.strip().casefold() in {"powershell", "powershell.exe", "pwsh", "pwsh.exe"}:
675
- return "ChemQL already uses PowerShell on Windows; run the command directly instead of starting a nested shell."
806
+ stripped = command.strip()
807
+
808
+ if stripped.casefold() in {
809
+ "powershell",
810
+ "powershell.exe",
811
+ "pwsh",
812
+ "pwsh.exe",
813
+ }:
814
+ return (
815
+ "ChemQL already uses PowerShell on Windows; "
816
+ "run the command directly instead of starting "
817
+ "a nested shell."
818
+ )
819
+
820
+ # Clear the terminal without sending Clear-Host to PowerShell.
821
+ if stripped.casefold() in {
822
+ "clear",
823
+ "cls",
824
+ "clear-host",
825
+ }:
826
+ os.system("cls")
827
+ return ""
828
+
676
829
  _shell_output_streamed = False
677
830
  return _run_shell_command_windows(command)
831
+
832
+ # POSIX behavior remains unchanged.
678
833
  return _run_shell_command_posix(command)
834
+
679
835
  except KeyboardInterrupt:
680
836
  close_shell()
681
837
  return "Shell command interrupted. Shell session was reset."
682
838
 
683
-
684
839
  def close_shell():
685
840
  """Clean up the persistent shell and its pseudo-terminal on exit."""
686
841
  global _shell_process, _shell_master_fd, _shell_prompt_ready
687
- global _shell_output_queue, _shell_reader_thread
842
+ global _shell_output_queue, _shell_reader_thread, _shell_script_path
688
843
 
689
844
  if _shell_process is not None:
690
845
  if _shell_process.poll() is None:
@@ -709,6 +864,13 @@ def close_shell():
709
864
  _shell_output_queue = None
710
865
  _shell_reader_thread = None
711
866
 
867
+ if _shell_script_path is not None:
868
+ try:
869
+ os.unlink(_shell_script_path)
870
+ except OSError:
871
+ pass
872
+ _shell_script_path = None
873
+
712
874
 
713
875
  atexit.register(close_shell)
714
876
 
@@ -2317,6 +2479,24 @@ def _space_query_operators(text):
2317
2479
  out.append(ch)
2318
2480
  i += 1
2319
2481
  continue
2482
+ # Preserve the compound operator "like!" as a single token.
2483
+ if text[i:i + 5].casefold() == "like!":
2484
+ before_ok = (
2485
+ i == 0 or not (
2486
+ text[i - 1].isalnum() or text[i - 1] == "_"
2487
+ )
2488
+ )
2489
+ end = i + 5
2490
+ after_ok = (
2491
+ end == len(text) or not (
2492
+ text[end].isalnum() or text[end] == "_"
2493
+ )
2494
+ )
2495
+
2496
+ if before_ok and after_ok:
2497
+ out.extend((" ", text[i:end], " "))
2498
+ i = end
2499
+ continue
2320
2500
  if ch in "()":
2321
2501
  out.extend((" ", ch, " "))
2322
2502
  i += 1
@@ -3111,7 +3291,7 @@ def process_lines(lines):
3111
3291
  result = strip_ansi(result) if NO_ANSI else result
3112
3292
  # POSIX PTY commands stream output live. Never print their returned
3113
3293
  # capture a second time; Windows pipe mode still needs normal printing.
3114
- if result and (os.name == "nt" or not _shell_output_streamed):
3294
+ if result and not _shell_output_streamed:
3115
3295
  print(result)
3116
3296
  _record_execution("```\n" + command + "\n```", result, not result.startswith("Shell error:"))
3117
3297
  globals()["_shell_output_streamed"] = False
@@ -3526,10 +3706,13 @@ def execute(arguments, query_text=None, positions=None):
3526
3706
  # Convenience aliases for very common lookups.
3527
3707
  # These are syntactic sugar for normal search queries.
3528
3708
  if arguments[0] == "findel":
3529
- if len(arguments) != 2:
3709
+ if len(arguments) < 2:
3530
3710
  return "Usage: findel <name-or-symbol>"
3531
3711
 
3532
3712
  value = arguments[1]
3713
+
3714
+ return_attributes = None
3715
+
3533
3716
  return execute(
3534
3717
  ["search", "elements", "name", "like", value, "or", "symbol", "like", value]
3535
3718
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chemql-python
3
- Version: 1.2.4
3
+ Version: 1.2.5
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.3 combines database-style
20
+ elements, molecules, and reactions. Version 1.2.4 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.3\
28
- > The examples below describe the ChemQL 1.2.3 source. Dataset
27
+ > **Version covered by this README:** 1.2.4\
28
+ > The examples below describe the ChemQL 1.2.4 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.3 adds three ways to reuse values and logic. These declarations
541
+ ChemQL 1.2.5 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.3
836
+ chemql --upgrade 1.2.4
837
837
  ```
838
838
 
839
839
  `--upgrade` installs the latest published `chemql-python` release; supplying a
File without changes
File without changes