chemql-python 1.5.2__py3-none-any.whl → 1.5.4__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
@@ -64,7 +64,7 @@ __all__ = [
64
64
  "__version__"
65
65
  ]
66
66
 
67
- __version__ = "1.5.2"
67
+ __version__ = "1.5.4"
68
68
 
69
69
  # Persistent preprocessor/function state for the current ChemQL session.
70
70
  DEFINITIONS = {}
@@ -568,16 +568,16 @@ def _help_text(topic=None):
568
568
  def _call_builtin(name, arguments):
569
569
  entry = BUILTIN_FUNCTIONS.get(name)
570
570
  if entry is None:
571
- raise NameError(f"Unknown ChemQL built-in `{name}`")
571
+ raise NameError((f"Unknown ChemQL built-in `{name}`" if NO_ANSI else f"Unknown ChemQL built-in \033[1m{name}\033[22m"))
572
572
  function, minimum, maximum = entry
573
573
  count = len(arguments)
574
574
  if count < minimum:
575
- raise TooFewArguments(f"Too few arguments for `{name}`: expected at least {minimum}, got {count}")
575
+ raise TooFewArguments((f"Too few arguments for `{name}`: expected at least {minimum}, got {count}" if NO_ANSI else f"Too few arguments for \033[1m{name}\033[22m: expected at least {minimum}, got {count}"))
576
576
  if maximum is not None and count > maximum:
577
- raise TooManyArguments(f"Too many arguments for `{name}`: expected at most {maximum}, got {count}")
577
+ raise TooManyArguments((f"Too many arguments for `{name}`: expected at most {maximum}, got {count}" if NO_ANSI else f"Too many arguments for \033[1m{name}\033[22m: expected at most {maximum}, got {count}"))
578
578
  if name == "help":
579
579
  if len(arguments) > 1:
580
- raise TooManyArguments("Too many arguments for `help`: expected at most 1, got " + str(len(arguments)))
580
+ raise TooManyArguments(("Too many arguments for `help`: expected at most 1, got " + str(len(arguments)) if NO_ANSI else "Too many arguments for \033[1mhelp\033[22m: expected at most 1, got " + str(len(arguments))))
581
581
  topic = arguments[0].strip() if arguments else None
582
582
  if topic is not None and len(topic) >= 2 and topic[0] in ("\"", "'") and topic[-1] == topic[0]:
583
583
  try:
@@ -591,18 +591,18 @@ def _call_builtin(name, arguments):
591
591
  except ChemQLBuiltinError:
592
592
  raise
593
593
  except TypeError as exc:
594
- raise InvalidArgumentType(f"Invalid argument type for `{name}`: {exc}") from exc
594
+ raise InvalidArgumentType((f"Invalid argument type for `{name}`: {exc}" if NO_ANSI else f"Invalid argument type for \033[1m{name}\033[22m: {exc}")) from exc
595
595
  except ValueError as exc:
596
- raise InvalidArgumentValue(f"Invalid argument value for `{name}`: {exc}") from exc
596
+ raise InvalidArgumentValue((f"Invalid argument value for `{name}`: {exc}" if NO_ANSI else f"Invalid argument value for \033[1m{name}\033[22m: {exc}")) from exc
597
597
  except IndexError as exc:
598
- raise InvalidArgumentValue(f"Invalid argument value for `{name}`: {exc}") from exc
598
+ raise InvalidArgumentValue((f"Invalid argument value for `{name}`: {exc}" if NO_ANSI else f"Invalid argument value for \033[1m{name}\033[22m: {exc}")) from exc
599
599
 
600
600
 
601
601
  def _call_function(name, arguments):
602
602
  """Call a ChemQL function, including let assignments and block syntax."""
603
603
  global _CURRENT_NAMESPACE
604
604
  if name not in FUNCTIONS:
605
- raise NameError(f"Unknown ChemQL function `{name}`")
605
+ raise NameError((f"Unknown ChemQL function `{name}`" if NO_ANSI else f"Unknown ChemQL function \033[1m{name}\033[22m"))
606
606
 
607
607
  values = [evaluate_python(arg) for arg in arguments]
608
608
  definition = FUNCTIONS[name]
@@ -697,7 +697,7 @@ def _call_function(name, arguments):
697
697
  if risk_depth == 0: break
698
698
  end_risk += 1
699
699
  if end_risk >= len(lines):
700
- raise SyntaxError(f"Missing `endrisk` in function `{name}`")
700
+ raise SyntaxError((f"Missing `endrisk` in function `{name}`" if NO_ANSI else f"Missing \033[1mendrisk\033[22m in function \033[1m{name}\033[22m"))
701
701
  sections = {"risk": [], "fix": [], "cleanup": []}
702
702
  section = "risk"
703
703
  for risk_line in lines[i + 1:end_risk]:
@@ -748,7 +748,7 @@ def _call_function(name, arguments):
748
748
 
749
749
  if i >= len(lines):
750
750
  raise SyntaxError(
751
- f"Missing `]]]` in function `{name}`"
751
+ (f"Missing `]]]` in function `{name}`" if NO_ANSI else f"Missing \033[1m]]]\033[22m in function \033[1m{name}\033[22m")
752
752
  )
753
753
 
754
754
  evaluate_python("\n".join(block))
@@ -846,7 +846,7 @@ def _call_function(name, arguments):
846
846
 
847
847
  else:
848
848
  raise SyntaxError(
849
- f"Unsupported function block `{code}`"
849
+ (f"Unsupported function block `{code}`" if NO_ANSI else f"Unsupported function block \033[1m{code}\033[22m")
850
850
  )
851
851
 
852
852
  i = end + 1
@@ -854,7 +854,7 @@ def _call_function(name, arguments):
854
854
 
855
855
  if line == "[endblock]":
856
856
  raise SyntaxError(
857
- f"Unexpected `[endblock]` in function `{name}`"
857
+ (f"Unexpected `[endblock]` in function `{name}`" if NO_ANSI else f"Unexpected \033[1m[endblock]\033[22m in function \033[1m{name}\033[22m")
858
858
  )
859
859
 
860
860
  # Handle return statements inside the function.
@@ -963,7 +963,7 @@ def _expand_function_calls(text):
963
963
  args = _split_function_arguments(text[opening + 1:end])
964
964
  if resolved_name in FUNCTIONS:
965
965
  if not _namespace_function_is_callable(resolved_name):
966
- raise PermissionError(f"Function `{name}` is private")
966
+ raise PermissionError((f"Function `{name}` is private" if NO_ANSI else f"Function \033[1m{name}\033[22m is private"))
967
967
  value = _call_function(resolved_name, args)
968
968
  elif "::" not in resolved_name and resolved_name in BUILTIN_FUNCTIONS:
969
969
  value = _call_builtin(resolved_name, args)
@@ -978,7 +978,7 @@ def _expand_function_calls(text):
978
978
  changed = True
979
979
  break
980
980
  else:
981
- raise ChemQLError(f"Unknown function `{name}`")
981
+ raise ChemQLError((f"Unknown function `{name}`" if NO_ANSI else f"Unknown function \033[1m{name}\033[22m"))
982
982
  replacement = repr(value) if isinstance(value, str) else ("None" if value is None else str(value))
983
983
  text = text[:i] + replacement + text[end + 1:]
984
984
  changed = True
@@ -2839,7 +2839,7 @@ def _formula_counts(formula):
2839
2839
  char = formula[index]
2840
2840
  if char == ")":
2841
2841
  if not nested:
2842
- raise ValueError(f"Unmatched closing parenthesis in formula `{formula}`")
2842
+ raise ValueError((f"Unmatched closing parenthesis in formula `{formula}`" if NO_ANSI else f"Unmatched closing parenthesis in formula \033[1m{formula}\033[22m"))
2843
2843
  return counts, index
2844
2844
  if char == "(":
2845
2845
  group, index = parse_group(index + 1, True)
@@ -4391,7 +4391,7 @@ def _package_relative_path(package_name):
4391
4391
  """Convert a canonical `a::b` package name to a relative source path."""
4392
4392
  parts = package_name.split("::")
4393
4393
  if not parts or any(not re.fullmatch(r"[A-Za-z_]\w*", part) for part in parts):
4394
- raise ImportError(f"Invalid ChemQL package name: `{package_name}`")
4394
+ raise ImportError((f"Invalid ChemQL package name: `{package_name}`" if NO_ANSI else f"Invalid ChemQL package name: \033[1m{package_name}\033[22m"))
4395
4395
  return Path(*parts[:-1], parts[-1] + ".cql")
4396
4396
 
4397
4397
 
@@ -4410,7 +4410,7 @@ def _find_cql_package_file(package_name):
4410
4410
  def _ensure_package_namespace(package_name, allow_namespace_only=False):
4411
4411
  """Load a canonical ChemQL package and return its namespace object."""
4412
4412
  if not re.fullmatch(r"[A-Za-z_]\w*(?:::[A-Za-z_]\w*)*", package_name):
4413
- raise ImportError(f"Invalid ChemQL package name: `{package_name}`")
4413
+ raise ImportError((f"Invalid ChemQL package name: `{package_name}`" if NO_ANSI else f"Invalid ChemQL package name: \033[1m{package_name}\033[22m"))
4414
4414
  cached = NAMESPACES.get(package_name)
4415
4415
  if cached is not None and package_name in CHEMQL_PACKAGE_SOURCE_LOADED:
4416
4416
  return cached
@@ -4419,8 +4419,11 @@ def _ensure_package_namespace(package_name, allow_namespace_only=False):
4419
4419
  path = _find_cql_package_file(package_name)
4420
4420
  if path is None and not allow_namespace_only:
4421
4421
  raise ImportError(
4422
- f"Cannot find ChemQL package `{package_name}`. Searched the current "
4423
- f"directory and ChemQL's bundled `stdlib` directory for .cql files."
4422
+ (f"Cannot find ChemQL package `{package_name}`. Searched the current "
4423
+ f"directory and ChemQL's bundled `stdlib` directory for .cql files."
4424
+ if NO_ANSI else
4425
+ f"Cannot find ChemQL package \033[1m{package_name}\033[22m. Searched the current "
4426
+ f"directory and ChemQL's bundled \033[1mstdlib\033[22m directory for .cql files.")
4424
4427
  )
4425
4428
  parent = _ensure_package_namespace(parent_name, allow_namespace_only=True) if separator else None
4426
4429
 
@@ -4465,7 +4468,7 @@ def _execute_load_statement(statement):
4465
4468
  )
4466
4469
  if not match:
4467
4470
  if statement.startswith("load"):
4468
- raise SyntaxError("Expected `load package::name` or `load package::name as alias`")
4471
+ raise SyntaxError(("Expected `load package::name` or `load package::name as alias`" if NO_ANSI else "Expected \033[1mload package::name\033[22m or \033[1mload package::name as alias\033[22m"))
4469
4472
  return False
4470
4473
 
4471
4474
  package_name, alias = match.groups()
@@ -4476,24 +4479,34 @@ def _execute_load_statement(statement):
4476
4479
  existing = CHEMQL_PACKAGE_BINDINGS.get(alias)
4477
4480
  if existing is not None and existing != package_name:
4478
4481
  raise ImportError(
4479
- f"Package name `{alias}` is already bound to `{existing}`; "
4480
- f"cannot also bind it to `{package_name}`. Use a different alias, "
4481
- f"for example `load {package_name} as {alias}_pkg`."
4482
+ (f"Package name `{alias}` is already bound to `{existing}`; "
4483
+ f"cannot also bind it to `{package_name}`. Use a different alias, "
4484
+ f"for example `load {package_name} as {alias}_pkg`."
4485
+ if NO_ANSI else
4486
+ f"Package name \033[1m{alias}\033[22m is already bound to \033[1m{existing}\033[22m; "
4487
+ f"cannot also bind it to \033[1m{package_name}\033[22m. Use a different alias, "
4488
+ f"for example \033[1mload {package_name} as {alias}_pkg\033[22m.")
4482
4489
  )
4483
4490
  if alias in CHEMQL_VARIABLES and alias not in CHEMQL_PACKAGE_BINDINGS:
4484
4491
  raise ImportError(
4485
- f"Name `{alias}` is already defined and cannot be used as a package alias. "
4486
- f"Choose another alias."
4492
+ (f"Name `{alias}` is already defined and cannot be used as a package alias. "
4493
+ f"Choose another alias."
4494
+ if NO_ANSI else
4495
+ f"Name \033[1m{alias}\033[22m is already defined and cannot be used as a package alias. "
4496
+ f"Choose another alias.")
4487
4497
  )
4488
4498
  elif "::" not in package_name:
4489
4499
  existing = CHEMQL_PACKAGE_BINDINGS.get(package_name)
4490
4500
  if existing is not None and existing != package_name:
4491
4501
  raise ImportError(
4492
- f"Name `{package_name}` is already bound to `{existing}`. "
4493
- f"Use an explicit alias to load `{package_name}` under another name."
4502
+ (f"Name `{package_name}` is already bound to `{existing}`. "
4503
+ f"Use an explicit alias to load `{package_name}` under another name."
4504
+ if NO_ANSI else
4505
+ f"Name \033[1m{package_name}\033[22m is already bound to \033[1m{existing}\033[22m. "
4506
+ f"Use an explicit alias to load \033[1m{package_name}\033[22m under another name.")
4494
4507
  )
4495
4508
  if package_name in CHEMQL_VARIABLES and package_name not in CHEMQL_PACKAGE_BINDINGS:
4496
- raise ImportError(f"Name `{package_name}` is already defined; cannot bind package to it.")
4509
+ raise ImportError((f"Name `{package_name}` is already defined; cannot bind package to it." if NO_ANSI else f"Name \033[1m{package_name}\033[22m is already defined; cannot bind package to it."))
4497
4510
 
4498
4511
  namespace = _ensure_package_namespace(package_name)
4499
4512
 
@@ -4505,11 +4518,14 @@ def _execute_load_statement(statement):
4505
4518
  root_binding = CHEMQL_PACKAGE_BINDINGS.get(root_name)
4506
4519
  if root_binding is not None and root_binding != root_name:
4507
4520
  raise ImportError(
4508
- f"Canonical namespace `{root_name}` is already bound to `{root_binding}`; "
4509
- f"cannot expose `{package_name}` through its canonical path."
4521
+ (f"Canonical namespace `{root_name}` is already bound to `{root_binding}`; "
4522
+ f"cannot expose `{package_name}` through its canonical path."
4523
+ if NO_ANSI else
4524
+ f"Canonical namespace \033[1m{root_name}\033[22m is already bound to \033[1m{root_binding}\033[22m; "
4525
+ f"cannot expose \033[1m{package_name}\033[22m through its canonical path.")
4510
4526
  )
4511
4527
  if root_name in CHEMQL_VARIABLES and root_name not in CHEMQL_PACKAGE_BINDINGS:
4512
- raise ImportError(f"Name `{root_name}` is already defined; cannot expose `{package_name}`.")
4528
+ raise ImportError((f"Name `{root_name}` is already defined; cannot expose `{package_name}`." if NO_ANSI else f"Name \033[1m{root_name}\033[22m is already defined; cannot expose \033[1m{package_name}\033[22m."))
4513
4529
  CHEMQL_PACKAGE_BINDINGS[root_name] = root_name
4514
4530
  python_globals[root_name] = root_namespace
4515
4531
  CHEMQL_VARIABLES.add(root_name)
@@ -4518,15 +4534,22 @@ def _execute_load_statement(statement):
4518
4534
  existing = CHEMQL_PACKAGE_BINDINGS.get(alias)
4519
4535
  if existing is not None and existing != package_name:
4520
4536
  raise ImportError(
4521
- f"Package name `{alias}` is already bound to `{existing}`; "
4522
- f"cannot also bind it to `{package_name}`. Use a different alias, "
4523
- f"for example `load {package_name} as {alias}_pkg`."
4537
+ (f"Package name `{alias}` is already bound to `{existing}`; "
4538
+ f"cannot also bind it to `{package_name}`. Use a different alias, "
4539
+ f"for example `load {package_name} as {alias}_pkg`."
4540
+ if NO_ANSI else
4541
+ f"Package name \033[1m{alias}\033[22m is already bound to \033[1m{existing}\033[22m; "
4542
+ f"cannot also bind it to \033[1m{package_name}\033[22m. Use a different alias, "
4543
+ f"for example \033[1mload {package_name} as {alias}_pkg\033[22m.")
4524
4544
  )
4525
4545
  # Avoid silently overwriting a non-package ChemQL variable either.
4526
4546
  if alias in CHEMQL_VARIABLES and alias not in CHEMQL_PACKAGE_BINDINGS:
4527
4547
  raise ImportError(
4528
- f"Name `{alias}` is already defined and cannot be used as a package alias. "
4529
- f"Choose another alias."
4548
+ (f"Name `{alias}` is already defined and cannot be used as a package alias. "
4549
+ f"Choose another alias."
4550
+ if NO_ANSI else
4551
+ f"Name \033[1m{alias}\033[22m is already defined and cannot be used as a package alias. "
4552
+ f"Choose another alias.")
4530
4553
  )
4531
4554
  CHEMQL_PACKAGE_BINDINGS[alias] = package_name
4532
4555
  python_globals[alias] = namespace
@@ -4540,11 +4563,14 @@ def _execute_load_statement(statement):
4540
4563
  existing = CHEMQL_PACKAGE_BINDINGS.get(package_name)
4541
4564
  if existing is not None and existing != package_name:
4542
4565
  raise ImportError(
4543
- f"Name `{package_name}` is already bound to `{existing}`. "
4544
- f"Use an explicit alias to load `{package_name}` under another name."
4566
+ (f"Name `{package_name}` is already bound to `{existing}`. "
4567
+ f"Use an explicit alias to load `{package_name}` under another name."
4568
+ if NO_ANSI else
4569
+ f"Name \033[1m{package_name}\033[22m is already bound to \033[1m{existing}\033[22m. "
4570
+ f"Use an explicit alias to load \033[1m{package_name}\033[22m under another name.")
4545
4571
  )
4546
4572
  if package_name in CHEMQL_VARIABLES and package_name not in CHEMQL_PACKAGE_BINDINGS:
4547
- raise ImportError(f"Name `{package_name}` is already defined; cannot bind package to it.")
4573
+ raise ImportError((f"Name `{package_name}` is already defined; cannot bind package to it." if NO_ANSI else f"Name \033[1m{package_name}\033[22m is already defined; cannot bind package to it."))
4548
4574
  CHEMQL_PACKAGE_BINDINGS[package_name] = package_name
4549
4575
  python_globals[package_name] = namespace
4550
4576
  CHEMQL_VARIABLES.add(package_name)
@@ -4602,7 +4628,7 @@ def process_lines(lines):
4602
4628
  break
4603
4629
  end += 1
4604
4630
  if end >= len(lines):
4605
- raise SyntaxError(f"Missing `endnamespace` for namespace `{ns_name}`")
4631
+ raise SyntaxError((f"Missing `endnamespace` for namespace `{ns_name}`" if NO_ANSI else f"Missing \033[1mendnamespace\033[22m for namespace \033[1m{ns_name}\033[22m"))
4606
4632
  parent = _CURRENT_NAMESPACE
4607
4633
  full_name = (parent._chemql_name + "::" + ns_name) if parent else ns_name # type: ignore
4608
4634
  ns = NAMESPACES.get(full_name)
@@ -4623,7 +4649,7 @@ def process_lines(lines):
4623
4649
  i = end + 1
4624
4650
  continue
4625
4651
  if stripped.casefold() == "endnamespace":
4626
- raise SyntaxError("Unexpected `endnamespace`")
4652
+ raise SyntaxError(("Unexpected `endnamespace`" if NO_ANSI else "Unexpected \033[1mendnamespace\033[22m"))
4627
4653
  visibility_match = re.match(r"^(public|private)\s+(.+)$", stripped, re.IGNORECASE)
4628
4654
  if visibility_match:
4629
4655
  visibility, declaration = visibility_match.groups()
@@ -4638,7 +4664,7 @@ def process_lines(lines):
4638
4664
  while block_end < len(lines) and lines[block_end].strip().casefold() != "endfunc":
4639
4665
  block_end += 1
4640
4666
  if block_end >= len(lines):
4641
- raise SyntaxError("Missing `endfunc` after visibility declaration")
4667
+ raise SyntaxError(("Missing `endfunc` after visibility declaration" if NO_ANSI else "Missing \033[1mendfunc\033[22m after visibility declaration"))
4642
4668
  elif re.match(r"^namespace\s+", declaration, re.IGNORECASE):
4643
4669
  depth = 1
4644
4670
  block_end = i + 1
@@ -4652,7 +4678,7 @@ def process_lines(lines):
4652
4678
  break
4653
4679
  block_end += 1
4654
4680
  if block_end >= len(lines):
4655
- raise SyntaxError("Missing `endnamespace` after visibility declaration")
4681
+ raise SyntaxError(("Missing `endnamespace` after visibility declaration" if NO_ANSI else "Missing \033[1mendnamespace\033[22m after visibility declaration"))
4656
4682
  elif re.match(r"^(struct|record|enum)\s+[A-Za-z_]\w*(?:\s+from\s+(?:\[[^\]]+\]|[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*))?\s*$", declaration, re.IGNORECASE):
4657
4683
  kind = re.match(r"^(struct|record|enum)", declaration, re.IGNORECASE).group(1).casefold() # type:ignore
4658
4684
  terminator = {"struct": "endstruct", "record": "endrecord", "enum": "endenum"}[kind]
@@ -4660,7 +4686,7 @@ def process_lines(lines):
4660
4686
  while block_end < len(lines) and lines[block_end].strip().casefold() != terminator:
4661
4687
  block_end += 1
4662
4688
  if block_end >= len(lines):
4663
- raise SyntaxError(f"Missing `{terminator}` after visibility declaration")
4689
+ raise SyntaxError((f"Missing `{terminator}` after visibility declaration" if NO_ANSI else f"Missing \033[1m{terminator}\033[22m after visibility declaration"))
4664
4690
  process_lines([declaration] + lines[i + 1:block_end + 1])
4665
4691
  finally:
4666
4692
  _CURRENT_VISIBILITY = old_visibility
@@ -4674,7 +4700,7 @@ def process_lines(lines):
4674
4700
  while end < len(lines) and lines[end].strip().casefold() != "endenum":
4675
4701
  end += 1
4676
4702
  if end >= len(lines):
4677
- raise SyntaxError(f"Missing `endenum` for enum `{type_name}`")
4703
+ raise SyntaxError((f"Missing `endenum` for enum `{type_name}`" if NO_ANSI else f"Missing \033[1mendenum\033[22m for enum \033[1m{type_name}\033[22m"))
4678
4704
  members = []
4679
4705
  member_names = set()
4680
4706
  for member_line in lines[i + 1:end]:
@@ -4686,12 +4712,12 @@ def process_lines(lines):
4686
4712
  raise SyntaxError(f"Invalid enum member declaration: {member_line}")
4687
4713
  member_name, value_expression = member_match.groups()
4688
4714
  if member_name in member_names:
4689
- raise SyntaxError(f"Duplicate enum member `{member_name}` in {type_name}")
4715
+ raise SyntaxError((f"Duplicate enum member `{member_name}` in {type_name}" if NO_ANSI else f"Duplicate enum member \033[1m{member_name}\033[22m in {type_name}"))
4690
4716
  member_names.add(member_name)
4691
4717
  value = auto() if value_expression is None else evaluate_python(_expand_definitions(value_expression))
4692
4718
  members.append((member_name, value))
4693
4719
  if not members:
4694
- raise SyntaxError(f"Enum `{type_name}` must declare members")
4720
+ raise SyntaxError((f"Enum `{type_name}` must declare members" if NO_ANSI else f"Enum \033[1m{type_name}\033[22m must declare members"))
4695
4721
  generated = Enum(type_name, members, module=__name__)
4696
4722
  ENUMS[type_name] = generated
4697
4723
  python_globals[type_name] = generated
@@ -4725,7 +4751,7 @@ def process_lines(lines):
4725
4751
  if parent not in RECORDS.values():
4726
4752
  parent = RECORDS.get(base_reference)
4727
4753
  if parent is None:
4728
- raise SyntaxError(f"Record `{type_name}` can only inherit from another record")
4754
+ raise SyntaxError((f"Record `{type_name}` can only inherit from another record" if NO_ANSI else f"Record \033[1m{type_name}\033[22m can only inherit from another record"))
4729
4755
  else:
4730
4756
  parent = (
4731
4757
  parent
@@ -4735,9 +4761,9 @@ def process_lines(lines):
4735
4761
  or getattr(builtins, base_reference, None)
4736
4762
  )
4737
4763
  if not isinstance(parent, type):
4738
- raise SyntaxError(f"Unknown base class `{base_reference}` for struct `{type_name}`")
4764
+ raise SyntaxError((f"Unknown base class `{base_reference}` for struct `{type_name}`" if NO_ANSI else f"Unknown base class \033[1m{base_reference}\033[22m for struct \033[1m{type_name}\033[22m"))
4739
4765
  if issubclass(parent, Enum):
4740
- raise SyntaxError(f"Struct `{type_name}` cannot inherit from an enum")
4766
+ raise SyntaxError((f"Struct `{type_name}` cannot inherit from an enum" if NO_ANSI else f"Struct \033[1m{type_name}\033[22m cannot inherit from an enum"))
4741
4767
  if hasattr(parent, "_chemql_fields"):
4742
4768
  base_fields = list(parent._chemql_fields)
4743
4769
  base_required = list(parent._chemql_required)
@@ -4747,7 +4773,7 @@ def process_lines(lines):
4747
4773
  try:
4748
4774
  parameters = list(inspect.signature(parent.__init__).parameters.values())[1:]
4749
4775
  except (TypeError, ValueError) as exc:
4750
- raise SyntaxError(f"Cannot inspect constructor for base class `{base_name}`") from exc
4776
+ raise SyntaxError((f"Cannot inspect constructor for base class `{base_name}`" if NO_ANSI else f"Cannot inspect constructor for base class \033[1m{base_name}\033[22m")) from exc
4751
4777
  for parameter in parameters:
4752
4778
  if parameter.kind not in (
4753
4779
  inspect.Parameter.POSITIONAL_ONLY,
@@ -4774,7 +4800,7 @@ def process_lines(lines):
4774
4800
  required_match = re.fullmatch(r"(?:required\s+([A-Za-z_]\w*)|([A-Za-z_]\w*)!)", field_line, re.IGNORECASE)
4775
4801
  if required_match:
4776
4802
  if kind != "struct":
4777
- raise SyntaxError("`required` fields are supported only in structs")
4803
+ raise SyntaxError(("`required` fields are supported only in structs" if NO_ANSI else "\033[1mrequired\033[22m fields are supported only in structs"))
4778
4804
  field = required_match.group(1) if required_match.group(1) is not None else required_match.group(2)
4779
4805
  default = None
4780
4806
  is_required = True
@@ -4795,7 +4821,7 @@ def process_lines(lines):
4795
4821
  declared_fields = required_fields + optional_fields if kind == "struct" else optional_fields
4796
4822
  duplicate_fields = set(base_fields) & set(declared_fields)
4797
4823
  if duplicate_fields:
4798
- raise SyntaxError(f"Field `{sorted(duplicate_fields)[0]}` is already inherited by {type_name}")
4824
+ raise SyntaxError((f"Field `{sorted(duplicate_fields)[0]}` is already inherited by {type_name}" if NO_ANSI else f"Field \033[1m{sorted(duplicate_fields)[0]}\033[22m is already inherited by {type_name}"))
4799
4825
  fields = base_fields + declared_fields
4800
4826
  if not fields and parent is None:
4801
4827
  raise SyntaxError((f"{kind.title()} `{type_name}` must declare fields" if NO_ANSI else f"{kind.title()} \033[1m{type_name}\033[22m must declare fields"))
@@ -4818,7 +4844,7 @@ def process_lines(lines):
4818
4844
  values = dict(zip(field_names, args))
4819
4845
  overlap = values.keys() & kwargs.keys()
4820
4846
  if overlap:
4821
- raise TypeError(f"Multiple values for field `{next(iter(overlap))}`")
4847
+ raise TypeError((f"Multiple values for field `{next(iter(overlap))}`" if NO_ANSI else f"Multiple values for field \033[1m{next(iter(overlap))}\033[22m"))
4822
4848
  values.update(kwargs)
4823
4849
  extra = values.keys() - set(field_names)
4824
4850
  if extra:
@@ -4918,7 +4944,7 @@ def process_lines(lines):
4918
4944
  values = dict(zip(field_names, args))
4919
4945
  overlap = values.keys() & kwargs.keys()
4920
4946
  if overlap:
4921
- raise TypeError(f"Multiple values for field `{next(iter(overlap))}`")
4947
+ raise TypeError((f"Multiple values for field `{next(iter(overlap))}`" if NO_ANSI else f"Multiple values for field \033[1m{next(iter(overlap))}\033[22m"))
4922
4948
  values.update(kwargs)
4923
4949
  extra = values.keys() - set(field_names)
4924
4950
  if extra:
@@ -5225,7 +5251,7 @@ def process_lines(lines):
5225
5251
  body.append(lines[i])
5226
5252
  i += 1
5227
5253
  if i >= len(lines):
5228
- raise SyntaxError("Missing closing \033[1m```\033[22m for terminal block")
5254
+ raise SyntaxError(("Missing closing ``` for terminal block" if NO_ANSI else "Missing closing \033[1m```\033[22m for terminal block"))
5229
5255
  body = [_expand_definitions(source_line) for source_line in body]
5230
5256
  command = "\n".join(body)
5231
5257
  result = run_shell_command(command)
chemql/codecql.py CHANGED
@@ -4,9 +4,11 @@ import json
4
4
  import argparse
5
5
  import keyword
6
6
  import builtins
7
+ import signal
7
8
  import sys
8
9
  import shutil
9
10
  import subprocess
11
+ import time
10
12
 
11
13
 
12
14
  def _write_system_clipboard(text):
@@ -126,7 +128,8 @@ if os.name == 'nt':
126
128
  0x4B: "arrow_left", 0x4D: "arrow_right",
127
129
  0x47: "arrow_home", 0x4F: "arrow_end",
128
130
  0x53: "delete", 0x73: "ctrl+arrow_left",
129
- 0x74: "ctrl+arrow_right", 0x9B: "alt+arrow_left",
131
+ 0x74: "ctrl+arrow_right", 0x8D: "ctrl+arrow_up",
132
+ 0x91: "ctrl+arrow_down", 0x9B: "alt+arrow_left",
130
133
  0x9D: "alt+arrow_right",
131
134
  }
132
135
 
@@ -137,6 +140,9 @@ if os.name == 'nt':
137
140
  return WINDOWS_KEYS.get(ch2[0], f"arrow_{ch2.hex()}")
138
141
  return ch.decode('utf-8', errors='ignore')
139
142
  else:
143
+ import fcntl
144
+ import pty
145
+ import struct
140
146
  import tty
141
147
  import termios
142
148
  import select
@@ -379,10 +385,12 @@ class Editor:
379
385
  if self.handle_key(key):
380
386
  break
381
387
  finally:
388
+ if os.name != "nt":
389
+ sys.stdout.write("\x1b[?6l\x1b[r")
382
390
  sys.stdout.write("\x1b[?2004l\x1b[?1049l")
383
391
  sys.stdout.flush()
384
392
 
385
- def draw_screen(self):
393
+ def draw_screen(self, terminal_height=0):
386
394
  try:
387
395
  cols, rows = os.get_terminal_size()
388
396
  except OSError:
@@ -390,7 +398,8 @@ class Editor:
390
398
  cols = max(cols, 20)
391
399
  rows = max(rows, 4)
392
400
  gutter_width = len(str(max(len(self.lines), self.cursor_y + 1))) + 2
393
- viewport_height = rows - 2
401
+ terminal_height = min(max(0, terminal_height), rows - 2)
402
+ viewport_height = rows - terminal_height - 2
394
403
  viewport_width = max(1, cols - gutter_width)
395
404
  visual_cursor_x = len(self.lines[self.cursor_y][:self.cursor_x].expandtabs(4))
396
405
  self._keep_cursor_visible(viewport_height, viewport_width, visual_cursor_x)
@@ -423,7 +432,7 @@ class Editor:
423
432
  if self.search_active
424
433
  else self.message or (
425
434
  "Ctrl+S Save Ctrl+Z Undo Ctrl+Y Redo Ctrl+Q Quit "
426
- "Ctrl+F Search Ctrl+Backspace/Delete Word"
435
+ "Ctrl+F Search Ctrl+T Terminal Ctrl+Backspace/Delete Word"
427
436
  )
428
437
  )
429
438
  output.extend((DIM, footer[:cols].ljust(cols), RESET))
@@ -495,6 +504,10 @@ class Editor:
495
504
  def handle_key(self, key):
496
505
  if self.search_active:
497
506
  return self._handle_search_key(key)
507
+ if key in ("\x14", "ctrl+t"):
508
+ self._run_split_terminal()
509
+ self.message = "Terminal closed"
510
+ return False
498
511
  if key in ("\x06", "ctrl+f"):
499
512
  self._open_search()
500
513
  return False
@@ -556,12 +569,22 @@ class Editor:
556
569
  "arrow_up", "arrow_down", "arrow_left", "arrow_right",
557
570
  "arrow_home", "arrow_end",
558
571
  }:
559
- self._move_cursor(base_key, shift=shift, wordwise=wordwise)
572
+ if "ctrl" in modifiers and base_key in {"arrow_up", "arrow_down"}:
573
+ self._scroll_view(-1 if base_key == "arrow_up" else 1)
574
+ else:
575
+ self._move_cursor(base_key, shift=shift, wordwise=wordwise)
560
576
  elif key in ("\x08", "ctrl+backspace"):
561
- self._delete_word("left")
577
+ if os.name == "nt":
578
+ if not self._delete_selection(): # For some reason on windows it is the opposit
579
+ self._backspace() # I have no idea why just stick to it
580
+ else:
581
+ self._delete_word("left")
562
582
  elif key in ("\x7f", "backspace"):
563
- if not self._delete_selection():
564
- self._backspace()
583
+ if os.name == "nt":
584
+ self._delete_word("left") # same here
585
+ else:
586
+ if not self._delete_selection():
587
+ self._backspace()
565
588
  elif key in ("ctrl+delete",):
566
589
  self._delete_word("right")
567
590
  elif key in ("delete", "\x1b[3~"):
@@ -581,6 +604,148 @@ class Editor:
581
604
  self.cursor_x = min(self.cursor_x, len(self.lines[self.cursor_y]))
582
605
  return False
583
606
 
607
+ def _run_split_terminal(self):
608
+ if os.name == "nt":
609
+ subprocess.run(os.environ.get("COMSPEC", "cmd.exe"), check=False)
610
+ return
611
+
612
+ stdin_fd = sys.stdin.fileno()
613
+ stdout_fd = sys.stdout.fileno()
614
+ old_settings = termios.tcgetattr(stdin_fd)
615
+ try:
616
+ cols, rows = os.get_terminal_size(stdin_fd)
617
+ except OSError:
618
+ cols, rows = 80, 24
619
+ rows = max(rows, 4)
620
+ pane_height = min(max(4, rows // 3), rows - 2)
621
+ pane_top = rows - pane_height + 1
622
+ shell_rows = rows - pane_top + 1
623
+ master_fd = None
624
+ child_pid = None
625
+
626
+ try:
627
+ self.message = "Terminal open: Esc or Ctrl+T returns"
628
+ self.draw_screen(terminal_height=pane_height)
629
+ for row in range(pane_top, rows + 1):
630
+ sys.stdout.write(f"\x1b[{row};1H\x1b[2K")
631
+ sys.stdout.flush()
632
+
633
+ child_pid, master_fd = pty.fork()
634
+ if child_pid == 0:
635
+ fcntl.ioctl(
636
+ sys.stdin.fileno(),
637
+ termios.TIOCSWINSZ,
638
+ struct.pack("HHHH", shell_rows, cols, 0, 0),
639
+ )
640
+ shell = os.environ.get("SHELL") or "bash"
641
+ try:
642
+ os.execvp(shell, [shell, "-i"])
643
+ except OSError as error:
644
+ os.write(2, f"Could not start {shell}: {error}\n".encode())
645
+ os._exit(127)
646
+
647
+ fcntl.ioctl(
648
+ master_fd,
649
+ termios.TIOCSWINSZ,
650
+ struct.pack("HHHH", shell_rows, cols, 0, 0),
651
+ )
652
+ tty.setraw(stdin_fd)
653
+ sys.stdout.write(f"\x1b[{pane_top};{rows}r\x1b[?6h\x1b[1;1H")
654
+ sys.stdout.flush()
655
+
656
+ while True:
657
+ try:
658
+ ready, _, _ = select.select([stdin_fd, master_fd], [], [])
659
+ except InterruptedError:
660
+ continue
661
+
662
+ if master_fd in ready:
663
+ try:
664
+ output = os.read(master_fd, 4096)
665
+ except OSError:
666
+ output = b""
667
+ if not output:
668
+ break
669
+ view = memoryview(output)
670
+ while view:
671
+ written = os.write(stdout_fd, view)
672
+ view = view[written:]
673
+
674
+ if stdin_fd in ready:
675
+ user_input = os.read(stdin_fd, 4096)
676
+ if not user_input:
677
+ break
678
+ exit_positions = [
679
+ position for marker in (b"\x14", b"\x11")
680
+ if (position := user_input.find(marker)) >= 0
681
+ ]
682
+ escape_position = user_input.find(b"\x1b")
683
+ if escape_position >= 0 and user_input[escape_position:] == b"\x1b":
684
+ try:
685
+ escaped_input, _, _ = select.select(
686
+ [stdin_fd], [], [], 0.05
687
+ )
688
+ except InterruptedError:
689
+ escaped_input = []
690
+ if escaped_input:
691
+ user_input += os.read(stdin_fd, 4096)
692
+ else:
693
+ exit_positions.append(escape_position)
694
+ if exit_positions:
695
+ exit_position = min(exit_positions)
696
+ if exit_position:
697
+ os.write(master_fd, user_input[:exit_position])
698
+ break
699
+ os.write(master_fd, user_input)
700
+ finally:
701
+ tty.tcsetattr(stdin_fd, termios.TCSADRAIN, old_settings)
702
+ sys.stdout.write(
703
+ "\x1b[?6l\x1b[r\x1b[?25h\x1b[?2004h\x1b[?1l\x1b[H\x1b[2J"
704
+ )
705
+ sys.stdout.flush()
706
+ if master_fd is not None:
707
+ os.close(master_fd)
708
+ if child_pid is not None:
709
+ try:
710
+ waited_pid, _ = os.waitpid(child_pid, os.WNOHANG)
711
+ except ChildProcessError:
712
+ waited_pid = child_pid
713
+ if waited_pid != child_pid:
714
+ try:
715
+ os.kill(child_pid, signal.SIGHUP)
716
+ except ProcessLookupError:
717
+ pass
718
+ deadline = time.monotonic() + 1
719
+ while time.monotonic() < deadline:
720
+ try:
721
+ waited_pid, _ = os.waitpid(child_pid, os.WNOHANG)
722
+ except ChildProcessError:
723
+ waited_pid = child_pid
724
+ if waited_pid == child_pid:
725
+ break
726
+ time.sleep(0.02)
727
+ else:
728
+ try:
729
+ os.kill(child_pid, signal.SIGKILL)
730
+ except ProcessLookupError:
731
+ pass
732
+ try:
733
+ os.waitpid(child_pid, 0)
734
+ except ChildProcessError:
735
+ pass
736
+
737
+ def _scroll_view(self, direction):
738
+ try:
739
+ _, rows = os.get_terminal_size()
740
+ except OSError:
741
+ rows = 24
742
+ viewport_height = max(rows, 4) - 2
743
+ max_scroll = max(0, len(self.lines) - viewport_height)
744
+ new_scroll_y = max(0, min(max_scroll, self.scroll_y + direction))
745
+ scroll_delta = new_scroll_y - self.scroll_y
746
+ self.scroll_y = new_scroll_y
747
+ self.cursor_y = max(0, min(len(self.lines) - 1, self.cursor_y + scroll_delta))
748
+
584
749
  def _open_search(self):
585
750
  self.search_active = True
586
751
  self.search_query = ""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chemql-python
3
- Version: 1.5.2
3
+ Version: 1.5.4
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
@@ -19,7 +19,7 @@ SQL-inspired syntax, inspect elements and molecules, look up reactions,
19
19
  balance equations, and combine chemistry queries with Python and shell
20
20
  commands.
21
21
 
22
- - **Version:** `1.5.1`
22
+ - **Version:** `1.5.4`
23
23
  - **PyPI distribution:** `chemql-python`
24
24
  - **Python import / CLI name:** `chemql`
25
25
  - **Author:** Mohammad Rakibul Islam
@@ -159,6 +159,8 @@ when a supported clipboard command is available), word deletion with
159
159
  undo (`Ctrl+Z`), redo (`Ctrl+Y`), save (`Ctrl+S`), and quit (`Ctrl+Q`).
160
160
  Namespace declarations are highlighted and auto-indented. Its syntax and UI
161
161
  colors are configurable in the packaged `chemql/colors.json` file.
162
+ Use `Ctrl+Up` and `Ctrl+Down` to scroll the viewport while keeping the cursor
163
+ at the same screen position.
162
164
 
163
165
  Press `Ctrl+F` to search; matches highlight as you type. Press Enter or the
164
166
  Down Arrow for the next match, Up Arrow for the previous match, and Escape
@@ -168,6 +170,11 @@ default. Prefix a query with `r:` for a case-sensitive regular expression,
168
170
  literal text, or `li:` for case-insensitive literal text. Add `w` to match
169
171
  whole words, for example `lwi:cat`.
170
172
 
173
+ Press `CTRL+T` to open a terminal inside the code editor to test your code. You can leave the terminal by pressing the Escape key
174
+
175
+ Use `Ctrl+Up` and `Ctrl+Down` to scroll the viewport while keeping the cursor
176
+ at the same screen position.
177
+
171
178
  ## Query language
172
179
 
173
180
  A typical query starts with `search`, a source, optional conditions, and
@@ -1,6 +1,6 @@
1
- chemql/__init__.py,sha256=nVUYYL0b_t2cGlPHM0smqkI9hhfx-7298aEfo1RjTcc,285207
1
+ chemql/__init__.py,sha256=-wKlFf4O5Qf0rjb8zvWVuVkP0BoAsfclxltQYpp3CpA,290449
2
2
  chemql/__main__.py,sha256=5eIIUjMeE90JcHhOamg7KczBDQVHjMHNNCSshBQ6buw,32
3
- chemql/codecql.py,sha256=AiS8wHylu2cmwGi8c5toFi1ELesD0NZ9OhBkGI0ZbjU,35812
3
+ chemql/codecql.py,sha256=4YckjyJXND-NSI0ZEuthCmF5HeGDfww-WK34COP2zX8,42558
4
4
  chemql/colors.json,sha256=zZE69-ILhPGZBpjje44gZCGUb_QycOn0F0IC4ufu8Ac,1848
5
5
  chemql/data/BondEnergies.json,sha256=QZN3iWZrD8125jV41EG1GiRohBtP0CzAP_EDQTfaEKc,8269
6
6
  chemql/data/MoleculeBonds.json,sha256=Iw2shYxJScH_tI80IytBYoqVDSbgT7OSqPJUIgKZme4,84262
@@ -21,9 +21,9 @@ chemql/stdlib/pathlib.cql,sha256=XkgWoPCtxW7tJKJokx52byb2RiWBKnVzG7X5uPfBFNQ,133
21
21
  chemql/stdlib/random.cql,sha256=Rvxbs-XP_gHUh0AmYmYl9WZXpJJJY3JpNNPn2HUobZU,742
22
22
  chemql/stdlib/text.cql,sha256=vBHE9b5SUxTQutaqViMteZeAC580Wg39J1eExkIx4yw,2038
23
23
  chemql/stdlib/math/statistics.cql,sha256=TVfazUbGGabUgAkLsg4LtPD8CaqZaj8mPnHbSuAmi40,1083
24
- chemql_python-1.5.2.dist-info/licenses/LICENSE,sha256=o4ywHC0EwRy2zgIASiRD2sK3Vcsf7LBcTHjDvoJgRE0,1079
25
- chemql_python-1.5.2.dist-info/METADATA,sha256=ZafQeh7_N1VxIDw3RazWzcgEADm-SMyLIslRj45cQok,30619
26
- chemql_python-1.5.2.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
27
- chemql_python-1.5.2.dist-info/entry_points.txt,sha256=UER27uM7rZzTp7oaJgKCY3J-G8i_hmQB4aZPjEFdVW0,69
28
- chemql_python-1.5.2.dist-info/top_level.txt,sha256=9UzHwnk_F6W19Q9nayaFgVyAELQXXwoYjeBgB6MWNr8,7
29
- chemql_python-1.5.2.dist-info/RECORD,,
24
+ chemql_python-1.5.4.dist-info/licenses/LICENSE,sha256=o4ywHC0EwRy2zgIASiRD2sK3Vcsf7LBcTHjDvoJgRE0,1079
25
+ chemql_python-1.5.4.dist-info/METADATA,sha256=P4IO2vBscNhGOUomYqtr2NtR4F6e46VP0XT5f_l6Qh0,30965
26
+ chemql_python-1.5.4.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
27
+ chemql_python-1.5.4.dist-info/entry_points.txt,sha256=UER27uM7rZzTp7oaJgKCY3J-G8i_hmQB4aZPjEFdVW0,69
28
+ chemql_python-1.5.4.dist-info/top_level.txt,sha256=9UzHwnk_F6W19Q9nayaFgVyAELQXXwoYjeBgB6MWNr8,7
29
+ chemql_python-1.5.4.dist-info/RECORD,,