chemql-python 1.4.1__py3-none-any.whl → 1.4.2__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
@@ -61,7 +61,7 @@ __all__ = [
61
61
  "__version__"
62
62
  ]
63
63
 
64
- __version__ = "1.4.1"
64
+ __version__ = "1.4.2"
65
65
 
66
66
  # Persistent preprocessor/function state for the current ChemQL session.
67
67
  DEFINITIONS = {}
@@ -407,6 +407,104 @@ _HELP_TOPICS = {
407
407
  "chemistry": ("Chemistry Functions", ["molar_mass{formula}", "count_atoms{formula, element_symbol}", "empirical_formula{formula}", "element{symbol, field?}"]),
408
408
  }
409
409
 
410
+ _STDLIB_HELP = {
411
+ "math": (
412
+ "Mathematical constants and numeric functions.",
413
+ [
414
+ "PI, E, TAU — Common mathematical constants.",
415
+ "sqrt{number}, cbrt{number}, abs{number}, floor{number}, ceil{number}, factorial{integer}",
416
+ "gcd{integer, ...}, lcm{integer, ...}, hypot{number, ...}, pow{x, y}",
417
+ "sin{radians}, cos{radians}, tan{radians}, asin{x}, acos{x}, atan{x}, atan2{y, x}",
418
+ "log{x}, log10{x}, log2{x}, exp{x}, degrees{radians}, radians{degrees}",
419
+ "isfinite{x}, isinf{x}, isnan{x}, isclose{x, y}",
420
+ "Example: math::sqrt{81} # 9.0",
421
+ ],
422
+ ),
423
+ "math::statistics": (
424
+ "Descriptive statistics for numeric data.",
425
+ [
426
+ "mean{values...}, median{values...}, median_low{values...}, median_high{values...}",
427
+ "mode{values...}, multimode{values...}, quantiles{values...}",
428
+ "variance{values...}, stdev{values...}, pvariance{values...}, pstdev{values...}",
429
+ "harmonic_mean{values...}, geometric_mean{values...}",
430
+ "Example: math::statistics::mean{2, 4, 6} # 4",
431
+ ],
432
+ ),
433
+ "text": (
434
+ "String transformations, searching, splitting, and regular expressions.",
435
+ [
436
+ "upper{text}, lower{text}, title{text}, capitalize{text}, casefold{text}",
437
+ "strip{text, chars?}, lstrip{text}, rstrip{text}, reverse{text}",
438
+ "startswith{text, prefix}, endswith{text, suffix}, replace{text, old, new}",
439
+ "split{text, separator?}, join{separator, values...}, count{text, substring}, find{text, substring}",
440
+ "isalpha{text}, isdigit{text}, isalnum{text}, isspace{text}",
441
+ "regex_search{pattern, text}, regex_findall{pattern, text}, regex_replace{pattern, replacement, text}",
442
+ 'Example: text::upper{"hello"} # HELLO',
443
+ ],
444
+ ),
445
+ "collections": (
446
+ "Collection helpers for sequences and iterables.",
447
+ [
448
+ "unique{values...}, flatten{groups...}, zip{values...}, pairwise{values...}",
449
+ "frequencies{values...}, group_count{values...}, product{numbers...}",
450
+ "sorted{values...}, reversed{values...}",
451
+ 'Example: collections::frequencies{"red", "blue", "red"}',
452
+ ],
453
+ ),
454
+ "datetime": (
455
+ "Date and time values, ISO parsing, timestamps, and date arithmetic.",
456
+ [
457
+ "today{}, now{}, utcnow{}",
458
+ "date{year, month, day}, time{hour, minute?, second?, microsecond?}",
459
+ "timestamp{iso_datetime}, from_timestamp{seconds}, weekday{iso_date}",
460
+ "date_add_days{iso_date, days}, date_difference_days{date1, date2}",
461
+ 'Example: datetime::date_add_days{"2026-10-20", 7}',
462
+ ],
463
+ ),
464
+ "json": (
465
+ "JSON serialization, parsing, and decoded value type inspection.",
466
+ [
467
+ "dumps{value}, dumps_pretty{value}, loads{json_text}, json_type{json_text}",
468
+ 'Example: json::loads{"{\\"formula\\": \\"H2O\\"}"}',
469
+ ],
470
+ ),
471
+ "random": (
472
+ "Pseudo-random values and sequence sampling using Python's random module.",
473
+ [
474
+ "seed{value}, random{}, randint{minimum, maximum}, randrange{stop} or randrange{start, stop, step}",
475
+ "choice{values}, sample{values, count}, shuffle{values}, uniform{minimum, maximum}",
476
+ "Use seed{value} when repeatable pseudo-random output is required.",
477
+ "Example: random::randint{1, 6}",
478
+ ],
479
+ ),
480
+ "decimal": (
481
+ "Base-10 decimal arithmetic, useful when decimal rounding matters.",
482
+ [
483
+ "create{value}, add{x, y}, subtract{x, y}, multiply{x, y}, divide{x, y}",
484
+ "quantize{value, quantum}, to_string{value}",
485
+ 'Pass decimal values as strings for exact input, e.g. decimal::add{"0.1", "0.2"}.',
486
+ ],
487
+ ),
488
+ "fractions": (
489
+ "Rational arithmetic using reduced fractions.",
490
+ [
491
+ "create{value} or create{numerator, denominator}",
492
+ "add{x, y}, subtract{x, y}, multiply{x, y}, divide{x, y}",
493
+ "numerator{value}, denominator{value}, to_float{value}",
494
+ "Example: fractions::create{1, 3}",
495
+ ],
496
+ ),
497
+ "pathlib": (
498
+ "Path inspection and UTF-8 text file operations using pathlib.",
499
+ [
500
+ "name{path}, stem{path}, suffix{path}, parent{path}, join{path, part...}",
501
+ "exists{path}, is_file{path}, is_dir{path}",
502
+ "read_text{path, encoding?}, write_text{path, content, encoding?}",
503
+ "File reads and writes operate with the current process permissions.",
504
+ ],
505
+ ),
506
+ }
507
+
410
508
 
411
509
  def _help_text(topic=None):
412
510
  if topic is None or not str(topic).strip():
@@ -415,16 +513,34 @@ def _help_text(topic=None):
415
513
  for name in names:
416
514
  category = _BUILTIN_HELP[name][0]
417
515
  categories.setdefault(category, []).append(name)
418
- lines = [f"ChemQL {__version__} help", "", "Usage: help{topic} or help topic", "Use help{function_name} for details, or help{syntax}, help{errors}, help{files}, help{chemistry}.", ""]
516
+ lines = [f"ChemQL {__version__} help", "", "Usage: help{topic} or help topic", "Use help{function_name} for details, or help{syntax}, help{errors}, help{files}, help{chemistry}, help{stdlib}.", ""]
419
517
  for category, items in categories.items():
420
518
  lines.append(f"{category}: " + ", ".join(items))
421
519
  lines += ["", "User-defined functions:"]
422
520
  lines += [f" {name}{{...}}" for name in sorted(FUNCTIONS)] or [" (none defined in this session)"]
423
521
  lines += ["", "Packages: load package_name; then use package_name::function{...}."]
522
+ lines += ["Standard library: " + ", ".join(_STDLIB_HELP)]
424
523
  return "\n".join(lines)
425
524
 
426
525
  key = str(topic).strip().strip("`\"'")
427
- key = key.replace("::", "::")
526
+ if key == "stdlib":
527
+ return "\n".join([
528
+ "ChemQL Standard Library",
529
+ "Load a package with `load package_name`, then call `package_name::function{...}`.",
530
+ "Get module details with `help{module}` and function details with `help{module::function}`.",
531
+ "",
532
+ *[f"{name} — {details[0]}" for name, details in _STDLIB_HELP.items()],
533
+ ])
534
+ resolved_key = _resolve_package_qualified_name(key)
535
+ if resolved_key in _STDLIB_HELP:
536
+ description, entries = _STDLIB_HELP[resolved_key]
537
+ return "\n".join([
538
+ f"{resolved_key} — Standard library",
539
+ description,
540
+ f"Load with: load {resolved_key}",
541
+ "Functions and constants:",
542
+ *[f" {entry}" for entry in entries],
543
+ ])
428
544
  if key in _BUILTIN_HELP:
429
545
  category, signature, description, examples = _BUILTIN_HELP[key]
430
546
  return "\n".join([f"{key} — {category}", f"Signature: {signature}", description, "", "Examples:", *[f" {line}" for line in examples]])
@@ -434,12 +550,16 @@ def _help_text(topic=None):
434
550
  if key in FUNCTIONS:
435
551
  return f"User-defined ChemQL function: {key}{{...}}\nDefined in the current session. Use `view`/`dump` where appropriate to inspect program state."
436
552
  if "::" in key:
437
- resolved = _resolve_package_qualified_name(key)
438
- if resolved in FUNCTIONS:
439
- return f"ChemQL package function: {key}{{...}}\nCanonical name: {resolved}"
440
- package = key.rsplit("::", 1)[0]
553
+ package, _, member = resolved_key.rpartition("::")
554
+ if package in _STDLIB_HELP:
555
+ _, entries = _STDLIB_HELP[package]
556
+ match = next((entry for entry in entries if entry.split("{", 1)[0].endswith(member)), None)
557
+ if match:
558
+ return f"{package}::{member} — Standard library\n{match}"
559
+ if resolved_key in FUNCTIONS:
560
+ return f"ChemQL package function: {key}{{...}}\nCanonical name: {resolved_key}"
441
561
  return f"Package/member help is not registered for `{key}`.\nTry `load {package}` and inspect the package namespace, or use `help{{{package}}}` if documented."
442
- return f"No help available for `{key}`. Try `help` to list built-ins, or `help{{syntax}}`, `help{{errors}}`, `help{{files}}`, and `help{{chemistry}}`."
562
+ return f"No help available for `{key}`. Try `help` to list built-ins, `help{{stdlib}}` for standard-library packages, or `help{{syntax}}`, `help{{errors}}`, `help{{files}}`, and `help{{chemistry}}`."
443
563
 
444
564
 
445
565
  def _call_builtin(name, arguments):
@@ -587,7 +707,13 @@ def _call_function(name, arguments):
587
707
  return result
588
708
  python_globals["error"] = None
589
709
  except Exception as exc:
590
- python_globals["error"] = exc.error_type if isinstance(exc, ChemQLSignal) else type(exc).__name__
710
+ python_globals["error"] = (
711
+ exc.error_type
712
+ if isinstance(exc, ChemQLSignal)
713
+ else "ChemQLError"
714
+ if isinstance(exc, ChemQLError)
715
+ else type(exc).__name__
716
+ )
591
717
  if sections["fix"]:
592
718
  result = execute_function_lines(sections["fix"])
593
719
  if result is not _NO_RETURN:
@@ -822,6 +948,9 @@ def _expand_function_calls(text):
822
948
  continue
823
949
  if ch in ("'", '"'):
824
950
  quote = ch; i += 1; continue
951
+ if i and re.match(r"[A-Za-z0-9_.:]", text[i - 1]):
952
+ i += 1
953
+ continue
825
954
  match = re.match(r"([A-Za-z_][A-Za-z0-9_]*(?:::[A-Za-z_][A-Za-z0-9_]*)*(?:\.[A-Za-z_][A-Za-z0-9_]*)?)\{", text[i:])
826
955
  if match:
827
956
  name = match.group(1)
@@ -835,9 +964,18 @@ def _expand_function_calls(text):
835
964
  value = _call_function(resolved_name, args)
836
965
  elif "::" not in resolved_name and resolved_name in BUILTIN_FUNCTIONS:
837
966
  value = _call_builtin(resolved_name, args)
967
+ elif (
968
+ "::" not in resolved_name
969
+ and isinstance(python_globals.get(resolved_name), type)
970
+ ):
971
+ replacement = (
972
+ f"{resolved_name}({text[opening + 1:end]})"
973
+ )
974
+ text = text[:i] + replacement + text[end + 1:]
975
+ changed = True
976
+ break
838
977
  else:
839
- i += 1
840
- continue
978
+ raise ChemQLError(f"Unknown function `{name}`")
841
979
  replacement = repr(value) if isinstance(value, str) else ("None" if value is None else str(value))
842
980
  text = text[:i] + replacement + text[end + 1:]
843
981
  changed = True
@@ -1391,10 +1529,17 @@ class ChemQLError(Exception):
1391
1529
  self.length = length
1392
1530
 
1393
1531
 
1532
+ class _ChemQLUnknownCommand(ChemQLError):
1533
+ pass
1534
+
1535
+
1394
1536
  _CHEMQL_RISK_DEPTH = 0
1395
1537
 
1396
1538
 
1397
1539
  def format_error(message, text=None, position=None, length=1):
1540
+ if _CHEMQL_RISK_DEPTH:
1541
+ raise ChemQLError(message, position, length)
1542
+
1398
1543
  output = ""
1399
1544
  if (text is not None) and (position is not None) and (not NO_ANSI):
1400
1545
  output += f"\033[1;31m{' ' * (position+4)}{'^' * max(1, length)}\n\033[0m"
@@ -4084,14 +4229,20 @@ def process_inline(text):
4084
4229
  return evaluate_python(stripped)
4085
4230
 
4086
4231
  # A standalone user-defined function call returns its value directly.
4087
- function_call = re.fullmatch(r"([A-Za-z_][A-Za-z0-9_]*(?:::[A-Za-z_][A-Za-z0-9_]*)*)\s*\{(.*)\}", stripped, re.DOTALL)
4232
+ function_call = re.match(
4233
+ r"([A-Za-z_][A-Za-z0-9_]*(?:::[A-Za-z_][A-Za-z0-9_]*)*)\s*\{",
4234
+ stripped,
4235
+ )
4088
4236
  if function_call:
4089
- function_name = _resolve_package_qualified_name(function_call.group(1))
4090
- arguments = _split_function_arguments(function_call.group(2))
4091
- if function_name in FUNCTIONS:
4092
- return _call_function(function_name, arguments)
4093
- if "::" not in function_name and function_name in BUILTIN_FUNCTIONS:
4094
- return _call_builtin(function_name, arguments)
4237
+ opening = function_call.end() - 1
4238
+ closing = find_matching(stripped, opening, "{", "}")
4239
+ if not stripped[closing + 1:].strip():
4240
+ function_name = _resolve_package_qualified_name(function_call.group(1))
4241
+ arguments = _split_function_arguments(stripped[opening + 1:closing])
4242
+ if function_name in FUNCTIONS:
4243
+ return _call_function(function_name, arguments)
4244
+ if "::" not in function_name and function_name in BUILTIN_FUNCTIONS:
4245
+ return _call_builtin(function_name, arguments)
4095
4246
 
4096
4247
  # [ ... ] is Python.
4097
4248
  if stripped.startswith("["):
@@ -4134,11 +4285,33 @@ def process_inline(text):
4134
4285
  except (SyntaxError, NameError):
4135
4286
  pass
4136
4287
 
4288
+ # Function calls can compose into ordinary Python expressions, e.g.
4289
+ # `int{1} + int{1}`. Expand them before deciding whether this is a query;
4290
+ # otherwise the query parser sees the resulting `1 + 1` as a command.
4291
+ if re.search(
4292
+ r"(?<![A-Za-z0-9_.:])[A-Za-z_][A-Za-z0-9_]*(?:::[A-Za-z_][A-Za-z0-9_]*)*(?:\.[A-Za-z_][A-Za-z0-9_]*)?\s*\{",
4293
+ stripped,
4294
+ ):
4295
+ expanded = _expand_function_calls(stripped)
4296
+ try:
4297
+ ast.parse(expanded, mode="eval")
4298
+ except SyntaxError:
4299
+ stripped = expanded
4300
+ else:
4301
+ return evaluate_python(expanded)
4302
+
4137
4303
  # Try ChemQL first. If the parser specifically reports an unknown command,
4138
4304
  # treat the input as a possible Python expression. Only Python SyntaxError
4139
4305
  # or NameError falls back to the original ChemQL error; other Python errors
4140
4306
  # (e.g. TypeError) are meaningful and should remain visible.
4141
- result = execute_query_text(stripped)
4307
+ try:
4308
+ result = execute_query_text(stripped)
4309
+ except _ChemQLUnknownCommand as error:
4310
+ try:
4311
+ return evaluate_python(stripped)
4312
+ except (SyntaxError, NameError):
4313
+ raise error
4314
+
4142
4315
  plain_result = strip_ansi(result) if isinstance(result, str) else ""
4143
4316
  # The query engine formats unknown-command failures as text (including a
4144
4317
  # caret line), so detect the diagnostic independently of any leading ANSI
@@ -4504,11 +4677,11 @@ def process_lines(lines):
4504
4677
  field_line = remove_comments(field_line).strip()
4505
4678
  if not field_line:
4506
4679
  continue
4507
- required_match = re.fullmatch(r"required\s+([A-Za-z_]\w*)", field_line, re.IGNORECASE)
4680
+ required_match = re.fullmatch(r"(?:required\s+([A-Za-z_]\w*)|([A-Za-z_]\w*)!)", field_line, re.IGNORECASE)
4508
4681
  if required_match:
4509
4682
  if kind != "struct":
4510
4683
  raise SyntaxError("`required` fields are supported only in structs")
4511
- field = required_match.group(1)
4684
+ field = required_match.group(1) if required_match.group(1) is not None else required_match.group(2)
4512
4685
  default = None
4513
4686
  is_required = True
4514
4687
  else:
@@ -4624,7 +4797,13 @@ def process_lines(lines):
4624
4797
  process_lines(sections["risk"])
4625
4798
  python_globals["error"] = None
4626
4799
  except Exception as exc:
4627
- python_globals["error"] = exc.error_type if isinstance(exc, ChemQLSignal) else type(exc).__name__
4800
+ python_globals["error"] = (
4801
+ exc.error_type
4802
+ if isinstance(exc, ChemQLSignal)
4803
+ else "ChemQLError"
4804
+ if isinstance(exc, ChemQLError)
4805
+ else type(exc).__name__
4806
+ )
4628
4807
  if sections["fix"]:
4629
4808
  process_lines(sections["fix"])
4630
4809
  elif not isinstance(exc, ChemQLSignal):
@@ -4935,13 +5114,7 @@ def process_lines(lines):
4935
5114
  result = _dump_history(command)
4936
5115
  else:
4937
5116
  result = process_inline(command)
4938
- # Query parsing can report an error as formatted text instead
4939
- # of raising. Inside `risk`, promote that result to an exception
4940
- # so `fix` runs and the `error` variable is populated.
4941
- if _CHEMQL_RISK_DEPTH and isinstance(result, str):
4942
- plain_result = strip_ansi(result).lstrip()
4943
- if plain_result.startswith("Error:"):
4944
- raise ChemQLError(plain_result[6:].strip())
5117
+
4945
5118
  rendered = "" if result is None else str(result)
4946
5119
  if result is not None:
4947
5120
  if NO_ANSI:
@@ -5560,14 +5733,20 @@ def execute(arguments, query_text=None, positions=None):
5560
5733
 
5561
5734
  if query_text is not None and positions:
5562
5735
  pos, length = error_position(positions, 0)
5736
+ message = f"Unknown command `{arguments[0]}`"
5737
+ if _CHEMQL_RISK_DEPTH:
5738
+ raise _ChemQLUnknownCommand(message, pos, length)
5563
5739
  return format_error(
5564
- (f"Unknown command `{arguments[0]}`" if NO_ANSI else f"Unknown command \033[1m{arguments[0]}\033[22m"),
5740
+ (message if NO_ANSI else f"Unknown command \033[1m{arguments[0]}\033[22m"),
5565
5741
  query_text,
5566
5742
  pos,
5567
5743
  length
5568
5744
  )
5569
5745
 
5570
- return format_error((f"Unknown command `{arguments[0]}`" if NO_ANSI else f"Unknown command \033[1m{arguments[0]}\033[22m"))
5746
+ message = f"Unknown command `{arguments[0]}`"
5747
+ if _CHEMQL_RISK_DEPTH:
5748
+ raise _ChemQLUnknownCommand(message)
5749
+ return format_error(message if NO_ANSI else f"Unknown command \033[1m{arguments[0]}\033[22m")
5571
5750
 
5572
5751
 
5573
5752
  HELP = (
@@ -5586,6 +5765,9 @@ HELP = (
5586
5765
  " \x1b[92mbalance H2 + O2 -> H2O \x1b[0m \x1b[90m│\x1b[0m Balance a chemical equation\n"
5587
5766
  " \x1b[92mhelp or ? \x1b[0m \x1b[90m│\x1b[0m Display this help message\n"
5588
5767
  " \x1b[92mhelp{read_csv} \x1b[0m \x1b[90m│\x1b[0m Show detailed built-in documentation\n"
5768
+ " \x1b[92mhelp{stdlib} \x1b[0m \x1b[90m│\x1b[0m List standard-library packages\n"
5769
+ " \x1b[92mhelp{math} \x1b[0m \x1b[90m│\x1b[0m Show package documentation\n"
5770
+ " \x1b[92mhelp{math::sqrt} \x1b[0m \x1b[90m│\x1b[0m Show package function help\n"
5589
5771
  " \x1b[92mversion \x1b[0m \x1b[90m│\x1b[0m Display the installed version\n"
5590
5772
  " \x1b[92mexit \x1b[0m \x1b[90m│\x1b[0m Exit the interactive prompt\n"
5591
5773
  "\n"
@@ -5757,7 +5939,7 @@ class ChemqlLexer(Lexer):
5757
5939
  "view", "help", "dump", "version", "return", "union", "intersection",
5758
5940
  "let", "unset", "variables", "macros", "definitions", "arguments",
5759
5941
  "define", "func", "struct", "record", "risk", "fix", "cleanup",
5760
- "signal", "import", "remove", "install",
5942
+ "signal", "import", "remove", "load"
5761
5943
  }
5762
5944
  OPERATORS = {
5763
5945
  "and", "or", "has", "like", "like!", "likep", "sort", "limit",
@@ -5948,7 +6130,7 @@ class ChemqlCompleter(Completer):
5948
6130
  "endfor", "endwhile", "endif", "endrisk", "endstruct", "endrecord", "endfunc", "enddefine",
5949
6131
  )
5950
6132
  QUERY_OPERATORS = ["=", "!=", ">", "<", ">=", "<=", "like", "like!", "likep", "has", "sort", "limit", "count", "asc", "desc", "return", "and", "or", "union", "intersection"]
5951
- CONTROL_COMPLETIONS = ["if ", "for ", "while ", "endfor", "endwhile", "endif", "risk", "fix", "cleanup", "endrisk", "struct ", "endstruct", "record ", "endrecord", "func ", "endfunc", "define ", "enddefine"]
6133
+ CONTROL_COMPLETIONS = ["if ", "for ", "while ", "endfor", "endwhile", "endif", "risk", "fix", "cleanup", "endrisk", "endstruct", "record ", "endrecord", "func ", "endfunc", "define ", "enddefine"]
5952
6134
 
5953
6135
  @staticmethod
5954
6136
  def _names():
@@ -6542,7 +6724,37 @@ def run_interactive():
6542
6724
  break
6543
6725
  if not lines:
6544
6726
  continue
6727
+ # Collect risk/fix/cleanup blocks as complete REPL units.
6728
+ if lines and lines[0].strip().casefold() == "risk":
6729
+ depth = 0
6545
6730
 
6731
+ for existing_line in lines:
6732
+ token = existing_line.strip().casefold()
6733
+ if token == "risk":
6734
+ depth += 1
6735
+ elif token == "endrisk":
6736
+ depth -= 1
6737
+
6738
+ while depth > 0:
6739
+ try:
6740
+ next_line = session.prompt("... ")
6741
+ except (KeyboardInterrupt, EOFError):
6742
+ print("\nIncomplete risk block")
6743
+ lines = []
6744
+ break
6745
+
6746
+ added_lines = next_line.splitlines() or [next_line]
6747
+ lines.extend(added_lines)
6748
+
6749
+ for added_line in added_lines:
6750
+ token = added_line.strip().casefold()
6751
+ if token == "risk":
6752
+ depth += 1
6753
+ elif token == "endrisk":
6754
+ depth -= 1
6755
+
6756
+ if not lines:
6757
+ continue
6546
6758
  # Collect new multiline Python, terminal, and comment blocks in the REPL.
6547
6759
  if lines and lines[0].strip() in {"[[[", "```", "```bash", "```sh", "```cmd", "```terminal"}:
6548
6760
  opener = lines[0].strip()
chemql/stdlib/README.md CHANGED
@@ -11,6 +11,10 @@ directory next to the interpreter module and preserve the folder layout.
11
11
  | `collections` | Uniqueness, flattening, pairing, frequency counts, product |
12
12
  | `datetime` | Dates, timestamps, local/UTC time and date arithmetic |
13
13
  | `json` | JSON encode/decode helpers |
14
+ | `random` | Pseudo-random numbers and sequence sampling |
15
+ | `decimal` | Base-10 decimal arithmetic and quantization |
16
+ | `fractions` | Exact rational numbers and fraction arithmetic |
17
+ | `pathlib` | Path inspection and UTF-8 text file operations |
14
18
 
15
19
  Examples:
16
20
 
@@ -37,4 +41,28 @@ datetime::date_add_days{"2026-10-20", 7}
37
41
 
38
42
  load json
39
43
  json::loads{"{\"formula\": \"H2O\"}"}
44
+
45
+ load random
46
+ random::seed{42}
47
+ random::randint{1, 6}
48
+
49
+ load decimal
50
+ decimal::add{"0.1", "0.2"}
51
+ decimal::quantize{"3.14159", "0.01"}
52
+
53
+ load fractions
54
+ fractions::create{1, 3}
55
+ fractions::add{"1/3", "1/6"}
56
+
57
+ load pathlib
58
+ pathlib::name{"data/results.csv"}
59
+ pathlib::join{"data", "results.csv"}
40
60
  ```
61
+
62
+ Use `help{stdlib}` for the package index, `help{math}` for a module
63
+ overview, and `help{math::sqrt}` for function help. The help pages are
64
+ available whether or not a package has been loaded.
65
+
66
+ `pathlib::read_text` and `pathlib::write_text` access files using the
67
+ current process permissions. `random` uses Python's pseudo-random
68
+ generator; call `random::seed{value}` when repeatable results are needed.
@@ -0,0 +1,23 @@
1
+ # ChemQL standard library: decimal
2
+ [import decimal as _chemql_decimal]
3
+ public func create
4
+ return _chemql_decimal.Decimal(str(arguments[0]))
5
+ endfunc
6
+ public func add
7
+ return _chemql_decimal.Decimal(str(arguments[0])) + _chemql_decimal.Decimal(str(arguments[1]))
8
+ endfunc
9
+ public func subtract
10
+ return _chemql_decimal.Decimal(str(arguments[0])) - _chemql_decimal.Decimal(str(arguments[1]))
11
+ endfunc
12
+ public func multiply
13
+ return _chemql_decimal.Decimal(str(arguments[0])) * _chemql_decimal.Decimal(str(arguments[1]))
14
+ endfunc
15
+ public func divide
16
+ return _chemql_decimal.Decimal(str(arguments[0])) / _chemql_decimal.Decimal(str(arguments[1]))
17
+ endfunc
18
+ public func quantize
19
+ return _chemql_decimal.Decimal(str(arguments[0])).quantize(_chemql_decimal.Decimal(str(arguments[1])))
20
+ endfunc
21
+ public func to_string
22
+ return str(_chemql_decimal.Decimal(str(arguments[0])))
23
+ endfunc
@@ -0,0 +1,29 @@
1
+ # ChemQL standard library: fractions
2
+ [import fractions as _chemql_fractions]
3
+ public func create
4
+ [if len(arguments) > 1]
5
+ return _chemql_fractions.Fraction(arguments[0], arguments[1])
6
+ [endblock]
7
+ return _chemql_fractions.Fraction(str(arguments[0]))
8
+ endfunc
9
+ public func add
10
+ return _chemql_fractions.Fraction(arguments[0]) + _chemql_fractions.Fraction(arguments[1])
11
+ endfunc
12
+ public func subtract
13
+ return _chemql_fractions.Fraction(arguments[0]) - _chemql_fractions.Fraction(arguments[1])
14
+ endfunc
15
+ public func multiply
16
+ return _chemql_fractions.Fraction(arguments[0]) * _chemql_fractions.Fraction(arguments[1])
17
+ endfunc
18
+ public func divide
19
+ return _chemql_fractions.Fraction(arguments[0]) / _chemql_fractions.Fraction(arguments[1])
20
+ endfunc
21
+ public func numerator
22
+ return _chemql_fractions.Fraction(arguments[0]).numerator
23
+ endfunc
24
+ public func denominator
25
+ return _chemql_fractions.Fraction(arguments[0]).denominator
26
+ endfunc
27
+ public func to_float
28
+ return float(_chemql_fractions.Fraction(arguments[0]))
29
+ endfunc
@@ -0,0 +1,38 @@
1
+ # ChemQL standard library: pathlib
2
+ [import pathlib as _chemql_pathlib]
3
+ public func name
4
+ return _chemql_pathlib.Path(str(arguments[0])).name
5
+ endfunc
6
+ public func stem
7
+ return _chemql_pathlib.Path(str(arguments[0])).stem
8
+ endfunc
9
+ public func suffix
10
+ return _chemql_pathlib.Path(str(arguments[0])).suffix
11
+ endfunc
12
+ public func parent
13
+ return str(_chemql_pathlib.Path(str(arguments[0])).parent)
14
+ endfunc
15
+ public func join
16
+ return str(_chemql_pathlib.Path(str(arguments[0])).joinpath(*(str(part) for part in arguments[1:])))
17
+ endfunc
18
+ public func exists
19
+ return _chemql_pathlib.Path(str(arguments[0])).exists()
20
+ endfunc
21
+ public func is_file
22
+ return _chemql_pathlib.Path(str(arguments[0])).is_file()
23
+ endfunc
24
+ public func is_dir
25
+ return _chemql_pathlib.Path(str(arguments[0])).is_dir()
26
+ endfunc
27
+ public func read_text
28
+ [if len(arguments) > 1]
29
+ return _chemql_pathlib.Path(str(arguments[0])).read_text(encoding=str(arguments[1]))
30
+ [endblock]
31
+ return _chemql_pathlib.Path(str(arguments[0])).read_text(encoding="utf-8")
32
+ endfunc
33
+ public func write_text
34
+ [if len(arguments) > 2]
35
+ return _chemql_pathlib.Path(str(arguments[0])).write_text(str(arguments[1]), encoding=str(arguments[2]))
36
+ [endblock]
37
+ return _chemql_pathlib.Path(str(arguments[0])).write_text(str(arguments[1]), encoding="utf-8")
38
+ endfunc
@@ -0,0 +1,29 @@
1
+ # ChemQL standard library: random
2
+ [import random as _chemql_random]
3
+ public func seed
4
+ _chemql_random.seed(arguments[0])
5
+ return None
6
+ endfunc
7
+ public func random
8
+ return _chemql_random.random()
9
+ endfunc
10
+ public func randint
11
+ return _chemql_random.randint(arguments[0], arguments[1])
12
+ endfunc
13
+ public func randrange
14
+ return _chemql_random.randrange(*arguments)
15
+ endfunc
16
+ public func choice
17
+ return _chemql_random.choice(arguments[0])
18
+ endfunc
19
+ public func sample
20
+ return _chemql_random.sample(arguments[0], arguments[1])
21
+ endfunc
22
+ public func shuffle
23
+ values = list(arguments[0])
24
+ _chemql_random.shuffle(values)
25
+ return values
26
+ endfunc
27
+ public func uniform
28
+ return _chemql_random.uniform(arguments[0], arguments[1])
29
+ endfunc
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chemql-python
3
- Version: 1.4.1
3
+ Version: 1.4.2
4
4
  Summary: A chemistry-focused query language and REPL
5
5
  Author-email: Mohammad Rakibul Islam <rakibulofficial1017@gmail.com>
6
6
  Requires-Python: >=3.10
@@ -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.4.0`
22
+ - **Version:** `1.4.1`
23
23
  - **PyPI distribution:** `chemql-python`
24
24
  - **Python import / CLI name:** `chemql`
25
25
  - **Author:** Mohammad Rakibul Islam
@@ -522,49 +522,55 @@ ChemQL provides `risk`, `fix`, `cleanup`, and `signal` for handling errors, resp
522
522
 
523
523
  ### `risk`
524
524
 
525
- Use `risk` to mark a block of code that may encounter an error and needs special handling.
525
+ `risk` executes its body. If a command raises an exception, ChemQL stores
526
+ the error type name in the Python variable `error` and runs the optional
527
+ `fix` body. A returned string that happens to contain error-looking text
528
+ does not trigger `fix`.
526
529
 
527
530
  ```text
528
531
  risk
529
- [result = 10 / 0]
532
+ [result = 10 / 2]
533
+ print(result)
534
+ fix
535
+ print(error)
536
+ cleanup
537
+ print("Always runs")
530
538
  endrisk
531
539
  ```
532
540
 
533
- This illustrates the idea of protecting potentially failing code. The exact behaviour depends on ChemQL's implementation of `risk`.
534
-
535
541
  ### `fix`
536
542
 
537
- Use `fix` to define how ChemQL should respond when an error occurs in a corresponding `risk` block.
543
+ The `fix` section runs only when the `risk` section raises an exception.
544
+ The `error` variable contains the exception type name, such as
545
+ `ChemQLError`, `ValueError`, or the error name carried by `signal`.
538
546
 
539
547
  ```text
540
548
  risk
541
549
  [result = 10 / 0]
542
550
  fix
543
- [print("An error occurred")]
551
+ print("Handled:", error)
544
552
  endrisk
545
553
  ```
546
554
 
547
- This example illustrates the intended relationship between `risk` and `fix`. Check the implementation for the precise syntax and whether execution continues after an error.
548
-
549
555
  ### `cleanup`
550
556
 
551
- Use `cleanup` to organize code that should run as part of a cleanup phase associated with a protected operation.
557
+ The optional `cleanup` section runs whether the `risk` section succeeds
558
+ or fails, after the risk body and any fix body.
552
559
 
553
560
  ```text
554
561
  risk
555
- [print("Performing operation")]
562
+ print("Performing operation")
556
563
  fix
557
- [print("Handling an error")]
564
+ print("Handling an error")
558
565
  cleanup
559
- [print("Cleaning up")]
566
+ print("Cleaning up")
560
567
  endrisk
561
568
  ```
562
569
 
563
- Cleanup logic is useful for keeping resource-management steps separate from the main operation and its error-handling logic.
564
-
565
570
  ### `signal`
566
571
 
567
- Use `signal` to explicitly raise or report an error when a condition cannot be handled normally.
572
+ Use `signal` to raise a named ChemQL error that can be handled by
573
+ `fix`.
568
574
 
569
575
  ```text
570
576
  if temperature < 0
@@ -572,19 +578,18 @@ if temperature < 0
572
578
  endif
573
579
  ```
574
580
 
575
- A signal can make an invalid state or failed operation explicit instead of allowing execution to proceed silently.
576
-
577
581
  ### Summary
578
582
 
579
583
  | Keyword | Purpose |
580
584
  |---|---|
581
- | `risk` | Marks potentially failing code for special handling |
582
- | `fix` | Specifies error-handling behaviour |
583
- | `cleanup` | Organizes cleanup logic |
584
- | `signal` | Raises or reports an error |
585
+ | `risk` | Executes the protected body |
586
+ | `fix` | Runs when the risk body raises an exception |
587
+ | `cleanup` | Runs after the risk and fix bodies |
588
+ | `signal` | Raises a named error |
585
589
  | `endrisk` | Ends the risk handler |
586
590
 
587
- These features help make ChemQL scripts easier to maintain by separating normal execution, error handling, and cleanup responsibilities.
591
+ Without a `fix` section, an ordinary exception propagates. `cleanup`
592
+ still runs during exception propagation.
588
593
 
589
594
  ## Functions, definitions, and macros
590
595
 
@@ -647,7 +652,8 @@ command lists definitions in the current session.
647
652
 
648
653
  ## Structs and records
649
654
 
650
- ChemQL v1.4.0 allows required fields defined by a trailing exclamation mark `!` or the `required` keyword
655
+ ChemQL 1.4.1 supports required fields, marked with a trailing `!` or the
656
+ `required` keyword:
651
657
 
652
658
  ```text
653
659
  struct Fruit
@@ -661,8 +667,8 @@ apple.name
661
667
  apple.price
662
668
  let orange = Fruit{"Orange", 20} // you can use position rather than just keyword arguments
663
669
  let banana = Fruit{"Banana", price = 10} // or you can even mix
664
-
665
670
  ```
671
+
666
672
  ```text
667
673
  struct Vegetable
668
674
  name!
@@ -680,24 +686,24 @@ is fully implemented.
680
686
 
681
687
  ## Namespaces
682
688
 
683
- Namespaces store functions, structs, records, variables, etc.
684
-
685
- To declare functions accessible in the global scope you must use the `public` keyword, everything is private by default, you may still add the `private` keyword for fun
686
- Example
687
- ```text
688
- namespace math
689
- public let PI = 3.14159265358979
690
- public let E = 2.718281828459045
691
- public
692
-
689
+ Namespaces group functions, structs, records, and variables. Members are
690
+ private by default; use `public` to expose a member through the namespace:
693
691
 
692
+ ```chemql
693
+ namespace geometry
694
+ public let PI = 3.141592653589793
695
+ public func circle_area
696
+ return [PI * \1 * \1]
697
+ endfunc
698
+ endnamespace
699
+
700
+ geometry::circle_area{2}
694
701
  ```
695
702
 
696
703
  ## Standard library
697
704
 
698
- ChemQL loads reusable `.cql` packages with `load`. The loader searches the current
699
- directory and then the interpreter's adjacent `stdlib/` directory. Preserve the
700
- folder structure when extracting `chemql_stdlib.zip`.
705
+ ChemQL loads reusable `.cql` packages with `load`. The loader searches the
706
+ current directory first and then ChemQL's bundled `stdlib/` directory.
701
707
 
702
708
  | Package | Purpose |
703
709
  | --- | --- |
@@ -707,6 +713,10 @@ folder structure when extracting `chemql_stdlib.zip`.
707
713
  | `collections` | `unique`, `flatten`, `zip`, `pairwise`, frequency counts, product |
708
714
  | `datetime` | Current date/time, ISO dates, timestamps, weekday and date arithmetic |
709
715
  | `json` | JSON serialization, pretty serialization, parsing and type inspection |
716
+ | `random` | Pseudo-random numbers and sequence sampling |
717
+ | `decimal` | Base-10 decimal arithmetic and quantization |
718
+ | `fractions` | Exact rational numbers and fraction arithmetic |
719
+ | `pathlib` | Path inspection and UTF-8 text file operations |
710
720
 
711
721
  ```chemql
712
722
  load math
@@ -731,9 +741,30 @@ datetime::date_add_days{"2026-10-20", 7}
731
741
 
732
742
  load json
733
743
  json::loads{"{\"formula\": \"H2O\"}"}
744
+
745
+ load random
746
+ random::seed{42}
747
+ random::randint{1, 6}
748
+
749
+ load decimal
750
+ decimal::add{"0.1", "0.2"}
751
+ decimal::quantize{"3.14159", "0.01"}
752
+
753
+ load fractions
754
+ fractions::create{1, 3}
755
+ fractions::add{"1/3", "1/6"}
756
+
757
+ load pathlib
758
+ pathlib::name{"data/results.csv"}
759
+ pathlib::join{"data", "results.csv"}
734
760
  ```
735
761
 
736
762
  These packages use Python's standard library only; they add no third-party dependencies.
763
+ Use `help{stdlib}` for the package index, `help{math}` for a module
764
+ overview, and `help{math::sqrt}` for function help. These help pages are
765
+ available before loading the package. `pathlib::read_text` and
766
+ `pathlib::write_text` access files with the permissions of the ChemQL
767
+ process.
737
768
 
738
769
  ## Built-in help
739
770
 
@@ -747,10 +778,14 @@ help{syntax}
747
778
  help{errors}
748
779
  help{files}
749
780
  help{chemistry}
781
+ help{stdlib}
782
+ help{math}
783
+ help{math::sqrt}
750
784
  ```
751
785
 
752
- Use `help{function_name}` for built-in documentation. The ANSI-colored help screen
753
- also includes a quick reference to the standard-library packages.
786
+ Use `help{function_name}` for built-in documentation and
787
+ `help{module::function}` for standard-library function documentation. The
788
+ help index also lists the available standard-library packages.
754
789
 
755
790
  ## Shell integration and scripting
756
791
 
@@ -1,4 +1,4 @@
1
- chemql/__init__.py,sha256=67HptiyMQ5Kd-Rirj0enV-yFR6ppC6BAp1WFFDFzb5Y,262911
1
+ chemql/__init__.py,sha256=INIV28S5-HJkfc32Fpql9Cbn7tENAuHiPmmj69Zx6V0,272034
2
2
  chemql/__main__.py,sha256=5eIIUjMeE90JcHhOamg7KczBDQVHjMHNNCSshBQ6buw,32
3
3
  chemql/data/BondEnergies.json,sha256=QZN3iWZrD8125jV41EG1GiRohBtP0CzAP_EDQTfaEKc,8269
4
4
  chemql/data/MoleculeBonds.json,sha256=Iw2shYxJScH_tI80IytBYoqVDSbgT7OSqPJUIgKZme4,84262
@@ -8,16 +8,20 @@ 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/stdlib/README.md,sha256=54ECAxQeOcSzUmoGMAPRvjFZL3NIyhrlS5yqHVdbNP4,1111
11
+ chemql/stdlib/README.md,sha256=mv-oXxTcFl1i-PiZehx9wDzY4zFwwg2UxdjPtWv14GI,2045
12
12
  chemql/stdlib/collections.cql,sha256=MEiD1BJ11tW9Y7Fbo58xVn6VW_PmCZOZ8IZAICqPml0,804
13
13
  chemql/stdlib/datetime.cql,sha256=_EOg9M6Gq8E8fCWrNVZZ50v5nMkOIB5_HqcLKo0Dl-Q,1252
14
+ chemql/stdlib/decimal.cql,sha256=m9gJdNe-D4C2qiPGzsazulPU1tc17JRFl5O7QhW5BI4,882
15
+ chemql/stdlib/fractions.cql,sha256=FzonkrbtCNhy2QT8wEVCyEiOh0pkIOGZNYpfHZZFrYs,1039
14
16
  chemql/stdlib/json.cql,sha256=D-l8teKi-9F3Rk3z61WlOB4MWid7BAeR0FEMxDfH2Cs,397
15
17
  chemql/stdlib/math.cql,sha256=2RjuZlkrgIaN74G5OkcNmtrosNI9s-eZANDjuGvPzCA,2036
18
+ chemql/stdlib/pathlib.cql,sha256=XkgWoPCtxW7tJKJokx52byb2RiWBKnVzG7X5uPfBFNQ,1332
19
+ chemql/stdlib/random.cql,sha256=Rvxbs-XP_gHUh0AmYmYl9WZXpJJJY3JpNNPn2HUobZU,742
16
20
  chemql/stdlib/text.cql,sha256=vBHE9b5SUxTQutaqViMteZeAC580Wg39J1eExkIx4yw,2038
17
21
  chemql/stdlib/math/statistics.cql,sha256=TVfazUbGGabUgAkLsg4LtPD8CaqZaj8mPnHbSuAmi40,1083
18
- chemql_python-1.4.1.dist-info/licenses/LICENSE,sha256=o4ywHC0EwRy2zgIASiRD2sK3Vcsf7LBcTHjDvoJgRE0,1079
19
- chemql_python-1.4.1.dist-info/METADATA,sha256=N5p9n3JBf6sBypAVI-3Z2RQ1wGQQRL4nFb0kOgDqzMc,27173
20
- chemql_python-1.4.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
21
- chemql_python-1.4.1.dist-info/entry_points.txt,sha256=j98ObVWR743an524z24AiWeRsppag-py5cc7l0D0HkI,39
22
- chemql_python-1.4.1.dist-info/top_level.txt,sha256=9UzHwnk_F6W19Q9nayaFgVyAELQXXwoYjeBgB6MWNr8,7
23
- chemql_python-1.4.1.dist-info/RECORD,,
22
+ chemql_python-1.4.2.dist-info/licenses/LICENSE,sha256=o4ywHC0EwRy2zgIASiRD2sK3Vcsf7LBcTHjDvoJgRE0,1079
23
+ chemql_python-1.4.2.dist-info/METADATA,sha256=QeC1TFVbuH7-d6PHoSlXPvi52nLyza64rShUECCPjZc,27679
24
+ chemql_python-1.4.2.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
25
+ chemql_python-1.4.2.dist-info/entry_points.txt,sha256=j98ObVWR743an524z24AiWeRsppag-py5cc7l0D0HkI,39
26
+ chemql_python-1.4.2.dist-info/top_level.txt,sha256=9UzHwnk_F6W19Q9nayaFgVyAELQXXwoYjeBgB6MWNr8,7
27
+ chemql_python-1.4.2.dist-info/RECORD,,