chemql-python 1.2.3__py3-none-any.whl → 1.2.5__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
@@ -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.3"
50
+ __version__ = "1.2.5"
51
51
 
52
52
  # Persistent preprocessor/function state for the current ChemQL session.
53
53
  DEFINITIONS = {}
@@ -108,6 +108,18 @@ def _split_function_arguments(source):
108
108
  if tail or source.strip():
109
109
  args.append(tail)
110
110
  return args
111
+ def is_block_header(line):
112
+ """Return True only for actual ChemQL block headers."""
113
+ if not (line.startswith("[") and line.endswith("]")):
114
+ return False
115
+
116
+ code = line[1:-1].strip()
117
+
118
+ return (
119
+ code.startswith("if ")
120
+ or code.startswith("for ")
121
+ or code.startswith("while ")
122
+ )
111
123
 
112
124
  def _call_function(name, arguments):
113
125
  """Call a ChemQL function, including let assignments and block syntax."""
@@ -194,13 +206,13 @@ def _call_function(name, arguments):
194
206
 
195
207
  # Recognize block headers generically, while excluding
196
208
  # 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
- )
209
+ # generic_header = re.match(
210
+ # r"^\[\s*(?!endblock\b|else\b|elif\b)"
211
+ # r"[A-Za-z_]\w*\b.*\]\s*$",
212
+ # line,
213
+ # )
202
214
 
203
- if is_python_block_header(line) or generic_header:
215
+ if is_block_header(line):
204
216
  end = find_endblock(lines, i + 1)
205
217
 
206
218
  block_body = [
@@ -355,13 +367,13 @@ _shell_prompt_ready = False
355
367
  _shell_output_streamed = False
356
368
  _shell_output_queue = None
357
369
  _shell_reader_thread = None
370
+ _shell_script_path = None
358
371
  _shell_prompt = f"__CHEMQL_PROMPT_{uuid.uuid4().hex}__"
359
372
 
360
-
361
373
  def get_shell():
362
374
  """Return the persistent OS shell process for the current platform."""
363
375
  global _shell_process, _shell_master_fd, _shell_prompt_ready
364
- global _shell_output_queue, _shell_reader_thread
376
+ global _shell_output_queue, _shell_reader_thread, _shell_script_path
365
377
 
366
378
  if _shell_process is not None and _shell_process.poll() is None:
367
379
  return _shell_process
@@ -370,20 +382,89 @@ def get_shell():
370
382
  close_shell()
371
383
 
372
384
  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 '' }}"
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)
378
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
+
379
456
  _shell_process = subprocess.Popen(
380
- ["powershell.exe", "-NoLogo", "-NoProfile", "-NoExit", "-Command", setup],
457
+ ["powershell.exe", "-NoLogo", "-NoProfile",
458
+ "-ExecutionPolicy", "Bypass", "-File", _shell_script_path],
381
459
  stdin=subprocess.PIPE,
382
460
  stdout=subprocess.PIPE,
383
461
  stderr=subprocess.STDOUT,
384
462
  text=True,
463
+ encoding="utf-8",
464
+ errors="replace",
385
465
  bufsize=1,
386
466
  )
467
+
387
468
  output_queue = queue.Queue()
388
469
  _shell_output_queue = output_queue
389
470
  process = _shell_process
@@ -402,30 +483,67 @@ def get_shell():
402
483
 
403
484
  _shell_reader_thread = threading.Thread(target=read_output, daemon=True)
404
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
+
405
528
  else:
529
+ # Leave the existing POSIX implementation unchanged.
406
530
  import fcntl
407
531
  import pty
408
532
  import termios
409
533
 
410
534
  master_fd, slave_fd = pty.openpty()
411
- # Don't echo bytes sent by ChemQL or keyboard input twice. Keep ISIG
412
- # enabled so forwarding Ctrl-C to the PTY interrupts the foreground job.
413
535
  attrs = termios.tcgetattr(slave_fd)
414
536
  attrs[3] &= ~termios.ECHO
415
537
  termios.tcsetattr(slave_fd, termios.TCSANOW, attrs)
416
538
 
417
539
  def attach_controlling_terminal():
418
- # Popen(start_new_session=True) has already called setsid().
419
540
  fcntl.ioctl(slave_fd, termios.TIOCSCTTY, 0)
420
541
 
421
542
  try:
422
543
  _shell_process = subprocess.Popen(
423
544
  ["bash", "--noprofile", "--norc"],
424
- stdin=slave_fd,
425
- stdout=slave_fd,
426
- stderr=slave_fd,
427
- close_fds=True,
428
- start_new_session=True,
545
+ stdin=slave_fd, stdout=slave_fd, stderr=slave_fd,
546
+ close_fds=True, start_new_session=True,
429
547
  preexec_fn=attach_controlling_terminal,
430
548
  env={**os.environ, "PS1": _shell_prompt, "PS2": ""},
431
549
  )
@@ -439,7 +557,6 @@ def get_shell():
439
557
 
440
558
  return _shell_process
441
559
 
442
-
443
560
  def _run_shell_command_posix(command, marker=None):
444
561
  """Run a command through the persistent PTY and forward keyboard input.
445
562
 
@@ -457,6 +574,7 @@ def _run_shell_command_posix(command, marker=None):
457
574
  prompt_bytes = _shell_prompt.encode()
458
575
 
459
576
  def wait_for_prompt(forward_keyboard=False):
577
+ global _shell_output_streamed
460
578
  collected = bytearray()
461
579
  streamed_pos = 0
462
580
  stdin_fd = None
@@ -469,7 +587,7 @@ def _run_shell_command_posix(command, marker=None):
469
587
  while True:
470
588
  readers = [master_fd]
471
589
  if stdin_fd is not None:
472
- readers.append(stdin_fd)
590
+ readers.append(stdin_fd) #type:ignore
473
591
  try:
474
592
  ready, _, _ = select.select(readers, [], [])
475
593
  except KeyboardInterrupt:
@@ -550,129 +668,178 @@ def _run_shell_command_posix(command, marker=None):
550
668
  close_shell()
551
669
  return f"Shell error: {exc}"
552
670
 
553
-
554
671
  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
672
+ """Execute one command in the persistent PowerShell request loop."""
673
+ import base64
674
+ import msvcrt
675
+ import uuid
676
+
557
677
  shell = get_shell()
558
678
  output_queue = _shell_output_queue
559
679
  if output_queue is None:
560
680
  return "Shell error: PowerShell output reader was not initialized."
681
+ if shell.stdin is None:
682
+ return "Shell error: PowerShell stdin is unavailable."
561
683
 
562
- 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
563
690
 
564
- def wait_for_prompt(stream_output):
565
691
  collected = ""
566
692
  streamed_pos = 0
567
- marker = _shell_prompt
568
- marker_len = len(marker)
569
- stdin_is_tty = False
693
+ started = time.monotonic()
570
694
  try:
571
695
  stdin_is_tty = sys.stdin is not None and sys.stdin.isatty()
572
696
  except (AttributeError, OSError, ValueError):
573
- pass
697
+ stdin_is_tty = False
574
698
  pending_input = ""
575
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
+
576
715
  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.
716
+ if time.monotonic() - started > timeout_seconds:
717
+ return (None, collected, True)
718
+
580
719
  if stdin_is_tty:
581
720
  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
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
588
726
  elif key in ("\r", "\n"):
589
- shell.stdin.write(pending_input + "\r\n") # type: ignore
727
+ shell.stdin.write(pending_input + "\n") # type: ignore # text mode normalizes newlines on Windows
590
728
  shell.stdin.flush() # type: ignore
729
+ sys.stdout.write("\n")
730
+ sys.stdout.flush()
591
731
  pending_input = ""
592
732
  elif key == "\b":
593
- 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()
594
737
  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
738
  close_shell()
598
- return None
599
- 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
600
741
  pending_input += key
742
+ sys.stdout.write(key)
743
+ sys.stdout.flush()
601
744
  except (OSError, ValueError):
602
745
  stdin_is_tty = False
603
746
 
604
747
  try:
605
748
  character = output_queue.get(timeout=0.04)
606
749
  except queue.Empty:
750
+ if shell.poll() is not None:
751
+ return (None, collected, False)
607
752
  continue
608
753
  if character is None:
609
- return None
754
+ return (None, collected, False)
610
755
 
611
756
  collected += character
612
757
  marker_pos = collected.find(marker)
613
758
  if marker_pos >= 0:
759
+ stream_until(marker_pos)
614
760
  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
761
+ return (
762
+ visible.replace("\r\n", "\n").rstrip("\r\n "),
763
+ collected,
764
+ False,
765
+ )
636
766
 
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
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)
643
773
 
644
774
  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)
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
648
787
  if result is None:
649
788
  close_shell()
650
- return "Shell session terminated."
789
+ return "Shell session terminated before PowerShell returned its completion marker."
651
790
  return result
652
791
  except (BrokenPipeError, OSError, ValueError) as exc:
653
792
  close_shell()
654
793
  return f"Shell error: {exc}"
655
794
 
656
-
657
795
  def run_shell_command(command):
658
796
  global _shell_output_streamed
659
- 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
+
660
804
  try:
661
805
  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."
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
+
664
829
  _shell_output_streamed = False
665
830
  return _run_shell_command_windows(command)
831
+
832
+ # POSIX behavior remains unchanged.
666
833
  return _run_shell_command_posix(command)
834
+
667
835
  except KeyboardInterrupt:
668
836
  close_shell()
669
837
  return "Shell command interrupted. Shell session was reset."
670
838
 
671
-
672
839
  def close_shell():
673
840
  """Clean up the persistent shell and its pseudo-terminal on exit."""
674
841
  global _shell_process, _shell_master_fd, _shell_prompt_ready
675
- global _shell_output_queue, _shell_reader_thread
842
+ global _shell_output_queue, _shell_reader_thread, _shell_script_path
676
843
 
677
844
  if _shell_process is not None:
678
845
  if _shell_process.poll() is None:
@@ -697,6 +864,13 @@ def close_shell():
697
864
  _shell_output_queue = None
698
865
  _shell_reader_thread = None
699
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
+
700
874
 
701
875
  atexit.register(close_shell)
702
876
 
@@ -2305,6 +2479,24 @@ def _space_query_operators(text):
2305
2479
  out.append(ch)
2306
2480
  i += 1
2307
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
2308
2500
  if ch in "()":
2309
2501
  out.extend((" ", ch, " "))
2310
2502
  i += 1
@@ -2509,15 +2701,30 @@ def find_endblock(lines, start):
2509
2701
  for i in range(start, len(lines)):
2510
2702
  stripped = lines[i].strip()
2511
2703
 
2512
- if is_python_block_header(stripped):
2513
- depth += 1
2514
-
2515
- elif stripped == "[endblock]":
2704
+ # Closing block.
2705
+ if stripped == "[endblock]":
2516
2706
  depth -= 1
2517
2707
 
2518
2708
  if depth == 0:
2519
2709
  return i
2520
2710
 
2711
+ continue
2712
+
2713
+ # Branch markers do not start new blocks.
2714
+ if stripped in ("[else]", "[endif]", "[endfor]", "[endwhile]"):
2715
+ continue
2716
+
2717
+ # Recognize both validated Python headers and generic
2718
+ # ChemQL block headers, consistently with _call_function().
2719
+ generic_header = re.match(
2720
+ r"^\[\s*(?!endblock\b|else\b|elif\b)"
2721
+ r"[A-Za-z_]\w*\b.*\]\s*$",
2722
+ stripped,
2723
+ )
2724
+
2725
+ if is_block_header(stripped):
2726
+ depth += 1
2727
+
2521
2728
  raise SyntaxError("Missing `[endblock]`")
2522
2729
 
2523
2730
  def remove_comments(line):
@@ -3084,7 +3291,7 @@ def process_lines(lines):
3084
3291
  result = strip_ansi(result) if NO_ANSI else result
3085
3292
  # POSIX PTY commands stream output live. Never print their returned
3086
3293
  # capture a second time; Windows pipe mode still needs normal printing.
3087
- if result and (os.name == "nt" or not _shell_output_streamed):
3294
+ if result and not _shell_output_streamed:
3088
3295
  print(result)
3089
3296
  _record_execution("```\n" + command + "\n```", result, not result.startswith("Shell error:"))
3090
3297
  globals()["_shell_output_streamed"] = False
@@ -3499,10 +3706,13 @@ def execute(arguments, query_text=None, positions=None):
3499
3706
  # Convenience aliases for very common lookups.
3500
3707
  # These are syntactic sugar for normal search queries.
3501
3708
  if arguments[0] == "findel":
3502
- if len(arguments) != 2:
3709
+ if len(arguments) < 2:
3503
3710
  return "Usage: findel <name-or-symbol>"
3504
3711
 
3505
3712
  value = arguments[1]
3713
+
3714
+ return_attributes = None
3715
+
3506
3716
  return execute(
3507
3717
  ["search", "elements", "name", "like", value, "or", "symbol", "like", value]
3508
3718
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chemql-python
3
- Version: 1.2.3
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
@@ -1,4 +1,4 @@
1
- chemql/__init__.py,sha256=buPnjHRRx32NB8I6hXUgKoKLzy3R_FkdNYqm1mmptLA,159524
1
+ chemql/__init__.py,sha256=WmZWHZgR-ZnVebISvUxSNyfjDVXbo8v8LeMDYwYZlFk,167015
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.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,,
11
+ chemql_python-1.2.5.dist-info/licenses/LICENSE,sha256=o4ywHC0EwRy2zgIASiRD2sK3Vcsf7LBcTHjDvoJgRE0,1079
12
+ chemql_python-1.2.5.dist-info/METADATA,sha256=mYrZXpJiCjQDgFiBY2Ili4aCwlK-E-h0jBZFRjxbXoM,30529
13
+ chemql_python-1.2.5.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
14
+ chemql_python-1.2.5.dist-info/entry_points.txt,sha256=j98ObVWR743an524z24AiWeRsppag-py5cc7l0D0HkI,39
15
+ chemql_python-1.2.5.dist-info/top_level.txt,sha256=9UzHwnk_F6W19Q9nayaFgVyAELQXXwoYjeBgB6MWNr8,7
16
+ chemql_python-1.2.5.dist-info/RECORD,,