chemql-python 1.4.2__py3-none-any.whl → 1.5.0__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,9 +1,12 @@
1
1
  import argparse
2
2
  import ast
3
3
  import atexit
4
+ import builtins
4
5
  import contextlib
6
+ from enum import Enum, auto
5
7
  from fractions import Fraction
6
8
  from functools import reduce
9
+ import inspect
7
10
  import io
8
11
  import importlib
9
12
  import json
@@ -61,7 +64,7 @@ __all__ = [
61
64
  "__version__"
62
65
  ]
63
66
 
64
- __version__ = "1.4.2"
67
+ __version__ = "1.5.0"
65
68
 
66
69
  # Persistent preprocessor/function state for the current ChemQL session.
67
70
  DEFINITIONS = {}
@@ -401,7 +404,7 @@ _BUILTIN_HELP = {
401
404
  }
402
405
 
403
406
  _HELP_TOPICS = {
404
- "syntax": ("ChemQL Syntax", ["Function calls use braces: `name{arg1, arg2}`.", "Quote text containing spaces: `upper{\"hello world\"}`.", "Separate arguments with commas, not spaces.", "Python statements/expressions can be embedded in `[ ... ]`.", "Multiline Python uses `[[[ ... ]]]`; embedded queries use `{{ ... }}`."]),
407
+ "syntax": ("ChemQL Syntax", ["Function calls use braces: `name{arg1, arg2}`.", "Quote text containing spaces: `upper{\"hello world\"}`.", "Separate arguments with commas, not spaces.", "Python statements/expressions can be embedded in `[ ... ]`.", "Multiline Python uses `[[[ ... ]]]`; embedded queries use `{{ ... }}`.", "Enums use `enum Name`, member declarations, and `endenum`; omitted values are generated automatically.", "Structs and records may use `from Base`; structs inherit from structs, records, or Python classes, while records inherit only from records."]),
405
408
  "errors": ("ChemQL Error Signals", ["SyntaxError — the input could not be parsed as valid syntax.", "TooFewArguments — a function received fewer arguments than required.", "TooManyArguments — a function received more arguments than allowed.", "InvalidArgumentType — an argument has an unsupported type.", "InvalidArgumentValue — an argument's value is invalid.", "FileReadError — reading a file failed.", "FileWriteError — writing a file failed.", "NameError — a name or function is unknown.", "PermissionError — a private namespace function was called externally."]),
406
409
  "files": ("File Functions", ["read_text{path, encoding?}", "read_lines{path, encoding?}", "read_csv{path, delimiter?, encoding?, header?}", "read_json{path, encoding?}", "write_text{path, content, encoding?}", "write_csv{path, rows, delimiter?, encoding?}", "write_json{path, value, encoding?, indent?}", "file_exists{path}", "list_files{directory?, pattern?}"]),
407
410
  "chemistry": ("Chemistry Functions", ["molar_mass{formula}", "count_atoms{formula, element_symbol}", "empirical_formula{formula}", "element{symbol, field?}"]),
@@ -3442,6 +3445,7 @@ reaction_state = {
3442
3445
  python_globals = {"__builtins__": __builtins__, "math": math, "error": None}
3443
3446
  STRUCTS = {}
3444
3447
  RECORDS = {}
3448
+ ENUMS = {}
3445
3449
 
3446
3450
  class ChemQLSignal(Exception):
3447
3451
  """A user-raised, recoverable ChemQL error."""
@@ -4649,9 +4653,9 @@ def process_lines(lines):
4649
4653
  block_end += 1
4650
4654
  if block_end >= len(lines):
4651
4655
  raise SyntaxError("Missing `endnamespace` after visibility declaration")
4652
- elif re.match(r"^(struct|record)\s+[A-Za-z_]\w*\s*$", declaration, re.IGNORECASE):
4653
- kind = re.match(r"^(struct|record)", declaration, re.IGNORECASE).group(1).casefold() # type:ignore
4654
- terminator = "endstruct" if kind == "struct" else "endrecord"
4656
+ 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
+ kind = re.match(r"^(struct|record|enum)", declaration, re.IGNORECASE).group(1).casefold() # type:ignore
4658
+ terminator = {"struct": "endstruct", "record": "endrecord", "enum": "endenum"}[kind]
4655
4659
  block_end = i + 1
4656
4660
  while block_end < len(lines) and lines[block_end].strip().casefold() != terminator:
4657
4661
  block_end += 1
@@ -4663,16 +4667,106 @@ def process_lines(lines):
4663
4667
  i = block_end + 1
4664
4668
  continue
4665
4669
 
4666
- type_match = re.fullmatch(r"(struct|record)\s+([A-Za-z_]\w*)", stripped, re.IGNORECASE)
4670
+ enum_match = re.fullmatch(r"enum\s+([A-Za-z_]\w*)", stripped, re.IGNORECASE)
4671
+ if enum_match:
4672
+ type_name = enum_match.group(1)
4673
+ end = i + 1
4674
+ while end < len(lines) and lines[end].strip().casefold() != "endenum":
4675
+ end += 1
4676
+ if end >= len(lines):
4677
+ raise SyntaxError(f"Missing `endenum` for enum `{type_name}`")
4678
+ members = []
4679
+ member_names = set()
4680
+ for member_line in lines[i + 1:end]:
4681
+ member_line = remove_comments(member_line).strip()
4682
+ if not member_line:
4683
+ continue
4684
+ member_match = re.fullmatch(r"([A-Za-z_]\w*)(?:\s*=\s*(.+))?", member_line)
4685
+ if not member_match:
4686
+ raise SyntaxError(f"Invalid enum member declaration: {member_line}")
4687
+ member_name, value_expression = member_match.groups()
4688
+ if member_name in member_names:
4689
+ raise SyntaxError(f"Duplicate enum member `{member_name}` in {type_name}")
4690
+ member_names.add(member_name)
4691
+ value = auto() if value_expression is None else evaluate_python(_expand_definitions(value_expression))
4692
+ members.append((member_name, value))
4693
+ if not members:
4694
+ raise SyntaxError(f"Enum `{type_name}` must declare members")
4695
+ generated = Enum(type_name, members, module=__name__)
4696
+ ENUMS[type_name] = generated
4697
+ python_globals[type_name] = generated
4698
+ i = end + 1
4699
+ continue
4700
+
4701
+ type_match = re.fullmatch(
4702
+ r"(struct|record)\s+([A-Za-z_]\w*)(?:\s+from\s+(\[[^\]]+\]|[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*))?",
4703
+ stripped,
4704
+ re.IGNORECASE,
4705
+ )
4667
4706
  if type_match:
4668
- kind, type_name = type_match.group(1).casefold(), type_match.group(2)
4707
+ kind, type_name, base_name = type_match.group(1).casefold(), type_match.group(2), type_match.group(3)
4669
4708
  terminator = "endstruct" if kind == "struct" else "endrecord"
4670
4709
  end = i + 1
4671
4710
  while end < len(lines) and lines[end].strip().casefold() != terminator:
4672
4711
  end += 1
4673
4712
  if end >= len(lines):
4674
4713
  raise SyntaxError((f"Missing `{terminator}` for {kind} `{type_name}`" if NO_ANSI else f"Missing \033[1m{terminator}\033[22m for {kind} \033[1m{type_name}\033[22m"))
4675
- fields, defaults, required_fields, optional_fields = [], {}, [], []
4714
+ parent = None
4715
+ base_fields, base_required, defaults = [], [], {}
4716
+ base_init_parameters = []
4717
+ tuple_fields = ()
4718
+ if base_name:
4719
+ base_reference = base_name.strip()
4720
+ if base_reference.startswith("[") and base_reference.endswith("]"):
4721
+ parent = evaluate_python(base_reference[1:-1].strip())
4722
+ elif "." in base_reference:
4723
+ parent = evaluate_python(base_reference)
4724
+ if kind == "record":
4725
+ if parent not in RECORDS.values():
4726
+ parent = RECORDS.get(base_reference)
4727
+ if parent is None:
4728
+ raise SyntaxError(f"Record `{type_name}` can only inherit from another record")
4729
+ else:
4730
+ parent = (
4731
+ parent
4732
+ or STRUCTS.get(base_reference)
4733
+ or RECORDS.get(base_reference)
4734
+ or python_globals.get(base_reference)
4735
+ or getattr(builtins, base_reference, None)
4736
+ )
4737
+ if not isinstance(parent, type):
4738
+ raise SyntaxError(f"Unknown base class `{base_reference}` for struct `{type_name}`")
4739
+ if issubclass(parent, Enum):
4740
+ raise SyntaxError(f"Struct `{type_name}` cannot inherit from an enum")
4741
+ if hasattr(parent, "_chemql_fields"):
4742
+ base_fields = list(parent._chemql_fields)
4743
+ base_required = list(parent._chemql_required)
4744
+ defaults.update(parent._chemql_defaults)
4745
+ tuple_fields = tuple(getattr(parent, "_chemql_tuple_fields", ()))
4746
+ elif kind == "struct":
4747
+ try:
4748
+ parameters = list(inspect.signature(parent.__init__).parameters.values())[1:]
4749
+ except (TypeError, ValueError) as exc:
4750
+ raise SyntaxError(f"Cannot inspect constructor for base class `{base_name}`") from exc
4751
+ for parameter in parameters:
4752
+ if parameter.kind not in (
4753
+ inspect.Parameter.POSITIONAL_ONLY,
4754
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
4755
+ inspect.Parameter.KEYWORD_ONLY,
4756
+ ):
4757
+ continue
4758
+ if not parameter.name.isidentifier():
4759
+ continue
4760
+ base_fields.append(parameter.name)
4761
+ if parameter.default is inspect.Parameter.empty:
4762
+ base_required.append(parameter.name)
4763
+ else:
4764
+ defaults[parameter.name] = parameter.default
4765
+ base_init_parameters.append(parameter)
4766
+ if base_fields:
4767
+ tuple_fields = tuple(getattr(parent, "_chemql_tuple_fields", ()))
4768
+
4769
+ required_fields, optional_fields = [], []
4676
4770
  for field_line in lines[i + 1:end]:
4677
4771
  field_line = remove_comments(field_line).strip()
4678
4772
  if not field_line:
@@ -4690,7 +4784,7 @@ def process_lines(lines):
4690
4784
  raise SyntaxError(f"Invalid {kind} field declaration: {field_line}")
4691
4785
  field, default = match.groups()
4692
4786
  is_required = False
4693
- if field in fields or field in required_fields or field in optional_fields:
4787
+ if field in base_fields or field in required_fields or field in optional_fields:
4694
4788
  raise SyntaxError((f"Duplicate field `{field}` in {type_name}" if NO_ANSI else f"Duplicate field \033[1m{field}\033[22m in {type_name}"))
4695
4789
  if is_required:
4696
4790
  required_fields.append(field)
@@ -4698,47 +4792,142 @@ def process_lines(lines):
4698
4792
  optional_fields.append(field)
4699
4793
  if default is not None:
4700
4794
  defaults[field] = evaluate_python(_expand_definitions(default))
4701
- # Required struct fields always come first, preserving order within
4702
- # the required and optional groups. This makes positional calls
4703
- # consistent with the generated constructor signature.
4704
- fields = required_fields + optional_fields if kind == "struct" else optional_fields
4705
- if not fields:
4795
+ declared_fields = required_fields + optional_fields if kind == "struct" else optional_fields
4796
+ duplicate_fields = set(base_fields) & set(declared_fields)
4797
+ if duplicate_fields:
4798
+ raise SyntaxError(f"Field `{sorted(duplicate_fields)[0]}` is already inherited by {type_name}")
4799
+ fields = base_fields + declared_fields
4800
+ if not fields and parent is None:
4706
4801
  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"))
4707
4802
  if kind == "struct":
4708
- def make_struct(class_name, field_names, field_defaults, required_names):
4709
- def __init__(self, *args, **kwargs):
4803
+ all_required = base_required + required_fields
4804
+ own_fields = tuple(declared_fields)
4805
+ native_constructor_base = parent in (
4806
+ int, float, complex, str, bytes, tuple, frozenset,
4807
+ ) if parent is not None else False
4808
+
4809
+ def make_struct(
4810
+ class_name, base_class, field_names, required_names,
4811
+ field_defaults, declared_names, inherited_names,
4812
+ init_parameters, inherited_tuple_fields, base_is_record,
4813
+ native_constructor,
4814
+ ):
4815
+ def bind_fields(*args, **kwargs):
4710
4816
  if len(args) > len(field_names):
4711
4817
  raise TypeError(f"{class_name} expected at most {len(field_names)} positional arguments")
4712
4818
  values = dict(zip(field_names, args))
4713
4819
  overlap = values.keys() & kwargs.keys()
4714
4820
  if overlap:
4715
- 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"))
4821
+ raise TypeError(f"Multiple values for field `{next(iter(overlap))}`")
4716
4822
  values.update(kwargs)
4717
4823
  extra = values.keys() - set(field_names)
4718
4824
  if extra:
4719
4825
  raise TypeError(f"Unexpected field(s): {', '.join(sorted(extra))}")
4720
4826
  missing = [name for name in required_names if name not in values]
4721
4827
  if missing:
4722
- names = ", ".join(missing)
4723
- raise TypeError(f"{class_name} missing required field(s): {names}")
4724
- for field_name in field_names:
4725
- setattr(self, field_name, values[field_name] if field_name in values else field_defaults.get(field_name))
4726
- def __repr__(self):
4727
- return class_name + "{ " + ", ".join(f"{f}={getattr(self, f)!r}" for f in field_names) + " }"
4728
- def __eq__(self, other):
4729
- return type(other) is type(self) and all(getattr(self, f) == getattr(other, f) for f in field_names)
4730
- return type(class_name, (), {"__init__": __init__, "__repr__": __repr__, "__str__": __repr__, "__eq__": __eq__, "__slots__": tuple(field_names)})
4731
- generated = make_struct(type_name, fields, defaults, required_fields)
4828
+ raise TypeError(f"{class_name} missing required field(s): {', '.join(missing)}")
4829
+ return {field: values.get(field, field_defaults.get(field)) for field in field_names}
4830
+
4831
+ def struct_init(self, *args, **kwargs):
4832
+ if native_constructor and not field_names:
4833
+ return
4834
+ values = bind_fields(*args, **kwargs)
4835
+ if base_class is not None:
4836
+ if hasattr(base_class, "_chemql_fields"):
4837
+ if not base_is_record:
4838
+ base_class.__init__(self, **{field: values[field] for field in inherited_names})
4839
+ elif native_constructor:
4840
+ pass
4841
+ elif init_parameters:
4842
+ positional = [
4843
+ values[param.name]
4844
+ for param in init_parameters
4845
+ if param.kind is inspect.Parameter.POSITIONAL_ONLY
4846
+ ]
4847
+ keywords = {
4848
+ param.name: values[param.name]
4849
+ for param in init_parameters
4850
+ if param.kind in (
4851
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
4852
+ inspect.Parameter.KEYWORD_ONLY,
4853
+ )
4854
+ }
4855
+ base_class.__init__(self, *positional, **keywords)
4856
+ else:
4857
+ base_class.__init__(self)
4858
+ for field_name in declared_names:
4859
+ setattr(self, field_name, values[field_name])
4860
+
4861
+ def struct_repr(self):
4862
+ return class_name + "{ " + ", ".join(
4863
+ f"{field}={getattr(self, field)!r}" for field in field_names
4864
+ ) + " }"
4865
+
4866
+ def struct_eq(self, other):
4867
+ return type(other) is type(self) and all(
4868
+ getattr(self, field) == getattr(other, field) for field in field_names
4869
+ )
4870
+
4871
+ attrs = {
4872
+ "__init__": struct_init,
4873
+ "_chemql_fields": tuple(field_names),
4874
+ "_chemql_required": tuple(required_names),
4875
+ "_chemql_defaults": dict(field_defaults),
4876
+ "_chemql_tuple_fields": inherited_tuple_fields,
4877
+ }
4878
+ if not (native_constructor and not field_names):
4879
+ attrs["__repr__"] = struct_repr
4880
+ attrs["__str__"] = struct_repr
4881
+ attrs["__eq__"] = struct_eq
4882
+ if inherited_tuple_fields:
4883
+ def struct_new(cls, *args, **kwargs):
4884
+ values = bind_fields(*args, **kwargs)
4885
+ return tuple.__new__(cls, tuple(values[field] for field in inherited_tuple_fields))
4886
+ attrs["__new__"] = struct_new
4887
+ elif native_constructor and not field_names:
4888
+ def struct_new(cls, *args, **kwargs):
4889
+ return base_class.__new__(cls, *args, **kwargs)
4890
+ attrs["__new__"] = struct_new
4891
+ else:
4892
+ attrs["__slots__"] = tuple(declared_names)
4893
+ return type(class_name, (base_class,) if base_class is not None else (), attrs)
4894
+
4895
+ generated = make_struct(
4896
+ type_name, parent, fields, all_required, defaults, own_fields,
4897
+ base_fields, base_init_parameters, tuple_fields,
4898
+ parent in RECORDS.values() if parent is not None else False,
4899
+ native_constructor_base,
4900
+ )
4732
4901
  STRUCTS[type_name] = generated
4733
4902
  else:
4734
4903
  from collections import namedtuple
4735
4904
  first_default = next((n for n, f in enumerate(fields) if f in defaults), len(fields))
4736
- if any(f not in defaults for f in fields[first_default:]):
4905
+ if parent is None and any(f not in defaults for f in fields[first_default:]):
4737
4906
  raise SyntaxError((f"Record `{type_name}` fields with defaults must be trailing fields" if NO_ANSI else f"Record \033[1m{type_name}\033[22m fields with defaults must be trailing fields"))
4738
- base_record = namedtuple(type_name + "Base", fields, defaults=[defaults[f] for f in fields[first_default:]])
4739
- def make_record(record_name, record_base, field_names):
4907
+ if parent is None:
4908
+ base_record = namedtuple(type_name + "Base", fields, defaults=[defaults[f] for f in fields[first_default:]])
4909
+ else:
4910
+ base_record = parent
4911
+
4912
+ def make_record(record_name, record_base, field_names, field_defaults, derived):
4740
4913
  def _record_repr(self):
4741
4914
  return record_name + " -> " + ", ".join(f"{field}: {getattr(self, field)}" for field in field_names)
4915
+ def _record_new(cls, *args, **kwargs):
4916
+ if len(args) > len(field_names):
4917
+ raise TypeError(f"{record_name} expected at most {len(field_names)} positional arguments")
4918
+ values = dict(zip(field_names, args))
4919
+ overlap = values.keys() & kwargs.keys()
4920
+ if overlap:
4921
+ raise TypeError(f"Multiple values for field `{next(iter(overlap))}`")
4922
+ values.update(kwargs)
4923
+ extra = values.keys() - set(field_names)
4924
+ if extra:
4925
+ raise TypeError(f"Unexpected field(s): {', '.join(sorted(extra))}")
4926
+ missing = [field for field in field_names if field not in values and field not in field_defaults]
4927
+ if missing:
4928
+ raise TypeError(f"{record_name} missing required field(s): {', '.join(missing)}")
4929
+ values = {field: values.get(field, field_defaults.get(field)) for field in field_names}
4930
+ return tuple.__new__(cls, tuple(values[field] for field in field_names))
4742
4931
  def _binary_method(operation, reverse=False):
4743
4932
  def method(self, other):
4744
4933
  if type(other) is type(self):
@@ -4763,9 +4952,22 @@ def process_lines(lines):
4763
4952
  "__pos__": lambda self: type(self)(*(+getattr(self, f) for f in field_names)),
4764
4953
  "__abs__": lambda self: type(self)(*(abs(getattr(self, f)) for f in field_names)),
4765
4954
  }
4955
+ if derived:
4956
+ attrs["__new__"] = _record_new
4957
+ attrs["_fields"] = tuple(field_names)
4958
+ attrs["__match_args__"] = tuple(field_names)
4959
+ attrs["_field_defaults"] = dict(field_defaults)
4960
+ attrs["__slots__"] = ()
4961
+ for index, field in enumerate(field_names):
4962
+ if field not in getattr(record_base, "_fields", ()):
4963
+ attrs[field] = property(lambda self, idx=index: self[idx])
4766
4964
  return type(record_name, (record_base,), attrs)
4767
- generated = make_record(type_name, base_record, fields)
4965
+ generated = make_record(type_name, base_record, fields, defaults, parent is not None)
4768
4966
  RECORDS[type_name] = generated
4967
+ generated._chemql_fields = tuple(fields)
4968
+ generated._chemql_required = tuple(base_required + required_fields) if kind == "struct" else ()
4969
+ generated._chemql_defaults = dict(defaults)
4970
+ generated._chemql_tuple_fields = tuple_fields if kind == "struct" else tuple(fields)
4769
4971
  python_globals[type_name] = generated
4770
4972
  i = end + 1
4771
4973
  continue
@@ -5863,10 +6065,21 @@ HELP = (
5863
6065
  " \x1b[2mThe variable error contains the error type, or None after success.\x1b[0m\n"
5864
6066
  "\n"
5865
6067
  "\x1b[1;93m\x1b[1m◆ STRUCTS & METHODS\x1b[0m\n"
6068
+ " \x1b[95menum Name ... endenum \x1b[0m \x1b[90m│\x1b[0m Define named values; omitted values auto-increment\n"
5866
6069
  " \x1b[95mstruct Name \x1b[0m \x1b[90m│\x1b[0m Define a structured data type\n"
5867
- " \x1b[95mrecord Name \x1b[0m \x1b[90m│\x1b[0m Define or use record data\n"
6070
+ " \x1b[95mstruct Name from Base \x1b[0m \x1b[90m│\x1b[0m Inherit from structs, records, or Python classes\n"
6071
+ " \x1b[95mrecord Name [from Base] \x1b[0m \x1b[90m│\x1b[0m Define a record; records inherit from records\n"
5868
6072
  " \x1b[95mfunc Type.__add__ \x1b[0m \x1b[90m│\x1b[0m Define an operator method\n"
6073
+ " \x1b[2mEnum members use `Name.MEMBER`; explicit values can be mixed with automatic values.\x1b[0m\n"
5869
6074
  " \x1b[2mStruct methods can implement custom behaviour and operator overloads.\x1b[0m\n"
6075
+ " \x1b[95menum Levels\x1b[0m\n"
6076
+ " \x1b[95mLOW = 50\x1b[0m\n"
6077
+ " \x1b[95mMEDIUM\x1b[0m\n"
6078
+ " \x1b[95mHIGH\x1b[0m\n"
6079
+ " \x1b[95mendenum\x1b[0m\n"
6080
+ " \x1b[95mstruct Dog from Animal\x1b[0m\n"
6081
+ " \x1b[95mcolor!\x1b[0m\n"
6082
+ " \x1b[95mendstruct\x1b[0m\n"
5870
6083
  "\n"
5871
6084
  "\x1b[1;93m\x1b[1m◆ PYTHON INTEGRATION\x1b[0m\n"
5872
6085
  " \x1b[1;96m[x = 10] \x1b[0m \x1b[90m│\x1b[0m Execute a Python statement\n"
@@ -5938,7 +6151,7 @@ class ChemqlLexer(Lexer):
5938
6151
  "findre", "set", "add", "remove", "react", "balance", "conditions",
5939
6152
  "view", "help", "dump", "version", "return", "union", "intersection",
5940
6153
  "let", "unset", "variables", "macros", "definitions", "arguments",
5941
- "define", "func", "struct", "record", "risk", "fix", "cleanup",
6154
+ "define", "func", "struct", "record", "enum", "risk", "fix", "cleanup",
5942
6155
  "signal", "import", "remove", "load"
5943
6156
  }
5944
6157
  OPERATORS = {
@@ -5947,10 +6160,10 @@ class ChemqlLexer(Lexer):
5947
6160
  }
5948
6161
  CONTROL_WORDS = {
5949
6162
  "if", "elif", "else", "for", "while", "break", "continue", "endblock",
5950
- "endfor", "endwhile", "endif", "endrisk", "endstruct", "endrecord",
6163
+ "endfor", "endwhile", "endif", "endrisk", "endstruct", "endrecord", "endenum",
5951
6164
  "endfunc", "enddefine", "func", "define", "struct", "record", "risk",
5952
6165
  "fix", "cleanup", "signal", "import", "as", "return", "public", "private",
5953
- "required", "namespace", "endnamespace"
6166
+ "required", "namespace", "endnamespace", "enum", "from"
5954
6167
  }
5955
6168
  CONDITIONS = {
5956
6169
  "temperature", "pressure", "catalyst", "catalysts", "standard", "room",
@@ -5966,6 +6179,7 @@ class ChemqlLexer(Lexer):
5966
6179
  },
5967
6180
  "structs": set(STRUCTS),
5968
6181
  "records": set(RECORDS),
6182
+ "enums": set(ENUMS),
5969
6183
  "functions": set(FUNCTIONS),
5970
6184
  "macros": set(MACROS),
5971
6185
  "definitions": set(DEFINITIONS),
@@ -5992,7 +6206,7 @@ class ChemqlLexer(Lexer):
5992
6206
 
5993
6207
  # Mark declaration names according to the declaration being edited.
5994
6208
  declaration = re.match(
5995
- r"(?:struct|record|func)\s+([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)?)", line[i:], re.I
6209
+ r"(?:struct|record|enum|func)\s+([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)?)", line[i:], re.I
5996
6210
  )
5997
6211
  macro_declaration = re.match(r"define\s+([A-Za-z_]\w*)\s*\{\s*\}", line[i:], re.I)
5998
6212
  definition_declaration = re.match(r"define\s+([A-Za-z_]\w*)\s+", line[i:], re.I)
@@ -6002,7 +6216,7 @@ class ChemqlLexer(Lexer):
6002
6216
  prefix = line[i:]
6003
6217
  keyword = prefix.split(None, 1)[0].casefold()
6004
6218
  declared_name = declaration.group(1)
6005
- declared_style = "struct" if keyword == "struct" else "record" if keyword == "record" else "function"
6219
+ declared_style = "struct" if keyword == "struct" else "record" if keyword == "record" else "enum" if keyword == "enum" else "function"
6006
6220
  elif macro_declaration:
6007
6221
  declared_name, declared_style = macro_declaration.group(1), "macro"
6008
6222
  elif definition_declaration:
@@ -6083,6 +6297,8 @@ class ChemqlLexer(Lexer):
6083
6297
  style = "class:struct"
6084
6298
  elif word in names["records"]:
6085
6299
  style = "class:record"
6300
+ elif word in names["enums"]:
6301
+ style = "class:enum"
6086
6302
  elif word in names["functions"] or word in function_names:
6087
6303
  style = "class:function"
6088
6304
  elif word in names["macros"]:
@@ -6126,11 +6342,11 @@ class ChemqlCompleter(Completer):
6126
6342
  "search", "source", "list", "clear", "return", "exit", "findel", "findmol",
6127
6343
  "findre", "set", "add", "remove", "react", "balance", "conditions", "version",
6128
6344
  "help", "view", "dump", "let", "unset", "variables", "func", "define", "struct",
6129
- "record", "risk", "fix", "cleanup", "signal", "import", "definitions", "macros",
6130
- "endfor", "endwhile", "endif", "endrisk", "endstruct", "endrecord", "endfunc", "enddefine",
6345
+ "record", "enum", "risk", "fix", "cleanup", "signal", "import", "definitions", "macros",
6346
+ "endfor", "endwhile", "endif", "endrisk", "endstruct", "endrecord", "endenum", "endfunc", "enddefine",
6131
6347
  )
6132
6348
  QUERY_OPERATORS = ["=", "!=", ">", "<", ">=", "<=", "like", "like!", "likep", "has", "sort", "limit", "count", "asc", "desc", "return", "and", "or", "union", "intersection"]
6133
- CONTROL_COMPLETIONS = ["if ", "for ", "while ", "endfor", "endwhile", "endif", "risk", "fix", "cleanup", "endrisk", "endstruct", "record ", "endrecord", "func ", "endfunc", "define ", "enddefine"]
6349
+ CONTROL_COMPLETIONS = ["if ", "for ", "while ", "endfor", "endwhile", "endif", "risk", "fix", "cleanup", "endrisk", "struct ", "endstruct", "record ", "endrecord", "enum ", "endenum", "func ", "endfunc", "define ", "enddefine"]
6134
6350
 
6135
6351
  @staticmethod
6136
6352
  def _names():
@@ -6142,6 +6358,11 @@ class ChemqlCompleter(Completer):
6142
6358
  "variables": variables,
6143
6359
  "structs": set(STRUCTS),
6144
6360
  "records": set(RECORDS),
6361
+ "enums": set(ENUMS),
6362
+ "python_types": {
6363
+ name for name, value in python_globals.items()
6364
+ if isinstance(name, str) and isinstance(value, type) and not issubclass(value, Enum)
6365
+ },
6145
6366
  "functions": set(FUNCTIONS),
6146
6367
  "macros": set(MACROS),
6147
6368
  "definitions": set(DEFINITIONS),
@@ -6210,12 +6431,17 @@ class ChemqlCompleter(Completer):
6210
6431
  elif command == "func":
6211
6432
  suggestions = sorted(names["structs"])
6212
6433
  elif command == "struct":
6213
- suggestions = sorted(names["structs"])
6434
+ if len(command_words) >= 3 and command_words[2].casefold() == "from":
6435
+ suggestions = sorted(names["structs"] | names["records"] | names["python_types"])
6436
+ else:
6437
+ suggestions = sorted(names["structs"])
6214
6438
  elif command == "record":
6215
6439
  suggestions = sorted(names["records"])
6440
+ elif command == "enum":
6441
+ suggestions = sorted(names["enums"])
6216
6442
  elif command in {"print", "return", "unset"}:
6217
6443
  suggestions = sorted(names["variables"])
6218
- elif command in {"risk", "fix", "cleanup", "endrisk", "endstruct", "endrecord", "endfunc", "enddefine", "endfor", "endwhile", "endif"}:
6444
+ elif command in {"risk", "fix", "cleanup", "endrisk", "endstruct", "endrecord", "endenum", "endfunc", "enddefine", "endfor", "endwhile", "endif"}:
6219
6445
  suggestions = self.CONTROL_COMPLETIONS
6220
6446
 
6221
6447
  # In expressions, Python blocks, attribute access, and function calls,
@@ -6240,6 +6466,7 @@ chemql_style = Style.from_dict({
6240
6466
  "variable": "ansibrightgreen",
6241
6467
  "struct": "ansibrightblue bold",
6242
6468
  "record": "ansicyan bold",
6469
+ "enum": "ansimagenta bold",
6243
6470
  "function": "ansiyellow bold",
6244
6471
  "macro": "ansimagenta bold",
6245
6472
  "definition": "ansibrightmagenta",
@@ -6468,7 +6695,10 @@ def _unclosed_expression_delimiters(lines):
6468
6695
 
6469
6696
  def main(ANSI: bool = True):
6470
6697
  parser = argparse.ArgumentParser(
6471
- description="Chemql query and chemistry language"
6698
+ description=(
6699
+ "ChemQL query and chemistry language.\n"
6700
+ "Version 1.5.0 adds enums and inheritance for structs and records."
6701
+ )
6472
6702
  )
6473
6703
 
6474
6704
  parser.add_argument(
@@ -6594,7 +6824,7 @@ def run_interactive():
6594
6824
  # such as `struct Fruit` and `record Point` are headers too.
6595
6825
  stack = []
6596
6826
  closers = {
6597
- "endrisk": "risk", "endstruct": "struct", "endrecord": "record",
6827
+ "endrisk": "risk", "endstruct": "struct", "endrecord": "record", "endenum": "enum",
6598
6828
  "endfunc": "func", "enddefine": "macro", "endfor": "for",
6599
6829
  "endwhile": "while", "endif": "if", "[endblock]": "bracket", "endnamespace": "namespace"
6600
6830
  }
@@ -6606,10 +6836,13 @@ def run_interactive():
6606
6836
 
6607
6837
  if re.fullmatch(r"namespace\s+[A-Za-z_]\w*", stripped, re.IGNORECASE):
6608
6838
  stack.append("namespace")
6609
- if re.fullmatch(r"struct\s+[A-Za-z_]\w*", stripped, re.IGNORECASE):
6839
+ declaration_line = re.sub(r"^(?:public|private)\s+", "", stripped, flags=re.IGNORECASE)
6840
+ if re.fullmatch(r"struct\s+[A-Za-z_]\w*(?:\s+from\s+(?:\[[^\]]+\]|[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*))?", declaration_line, re.IGNORECASE):
6610
6841
  stack.append("struct")
6611
- elif re.fullmatch(r"record\s+[A-Za-z_]\w*", stripped, re.IGNORECASE):
6842
+ elif re.fullmatch(r"record\s+[A-Za-z_]\w*(?:\s+from\s+(?:\[[^\]]+\]|[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*))?", declaration_line, re.IGNORECASE):
6612
6843
  stack.append("record")
6844
+ elif re.fullmatch(r"enum\s+[A-Za-z_]\w*", declaration_line, re.IGNORECASE):
6845
+ stack.append("enum")
6613
6846
  elif re.fullmatch(r"func\s+[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)?", stripped, re.IGNORECASE):
6614
6847
  stack.append("func")
6615
6848
  elif re.fullmatch(r"define\s+[A-Za-z_]\w*\s*\{\s*\}", stripped, re.IGNORECASE):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chemql-python
3
- Version: 1.4.2
3
+ Version: 1.5.0
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.1`
22
+ - **Version:** `1.5.0`
23
23
  - **PyPI distribution:** `chemql-python`
24
24
  - **Python import / CLI name:** `chemql`
25
25
  - **Author:** Mohammad Rakibul Islam
@@ -67,6 +67,9 @@ commands.
67
67
  matching.
68
68
  - Use Python statements, expressions, variables, control flow,
69
69
  functions, macros, and structured values.
70
+ - Define enums with automatic or explicit values, and inherit structs
71
+ from structs, records, or Python classes; records can inherit from
72
+ other records.
70
73
  - Execute shell commands and multiline shell blocks from the REPL.
71
74
  - Export recorded commands and results.
72
75
  - Use the command-line interface or import ChemQL from Python.
@@ -652,7 +655,8 @@ command lists definitions in the current session.
652
655
 
653
656
  ## Structs and records
654
657
 
655
- ChemQL 1.4.1 supports required fields, marked with a trailing `!` or the
658
+ ChemQL 1.5.0 supports enums and inheritance in addition to required fields,
659
+ marked with a trailing `!` or the
656
660
  `required` keyword:
657
661
 
658
662
  ```text
@@ -684,6 +688,87 @@ Special methods such as `__add__` are intended for custom behaviour, but
684
688
  do not assume every Python data-model feature or operator dispatch path
685
689
  is fully implemented.
686
690
 
691
+ ### Enums
692
+
693
+ An enum member without an explicit value receives an automatically generated
694
+ integer value. Automatic values continue from the preceding numeric value:
695
+
696
+ ```text
697
+ enum Levels
698
+ LOW = 50
699
+ MEDIUM
700
+ HIGH
701
+ endenum
702
+
703
+ let selected = Levels.MEDIUM
704
+ [selected.value] // 51
705
+ ```
706
+
707
+ ### Inheritance
708
+
709
+ Structs can inherit fields and behavior from another struct, a record, or a
710
+ Python class available in ChemQL's Python globals. Records can inherit only
711
+ from other records:
712
+
713
+ ```text
714
+ struct Animal
715
+ name!
716
+ endstruct
717
+
718
+ struct Dog from Animal
719
+ color!
720
+ isSmall = False
721
+ endstruct
722
+
723
+ let pet = Dog{"Rex", "brown"}
724
+ pet.name
725
+ pet.color
726
+
727
+ record Identity
728
+ id
729
+ endrecord
730
+
731
+ record Product from Identity
732
+ name
733
+ endrecord
734
+
735
+ let item = Product{7, "Copper"}
736
+ item.id
737
+ item.name
738
+ ```
739
+
740
+ Structs may also extend a Python class defined or imported into the active
741
+ Python namespace. Built-in classes such as `int` can be named directly; a
742
+ bracketed Python expression such as `[int]` is also accepted. Initializer
743
+ parameters from ordinary Python classes become inherited constructor fields,
744
+ and the Python initializer is called when the ChemQL struct is constructed.
745
+ Native value types such as `int` keep their native constructor and value
746
+ behavior:
747
+
748
+ ```text
749
+ [[[
750
+ class PythonAnimal:
751
+ def __init__(self, name):
752
+ self.name = name
753
+ ]]]
754
+
755
+ struct Pet from PythonAnimal
756
+ species = "unknown"
757
+ endstruct
758
+
759
+ let pet = Pet{"Milo", "cat"}
760
+ pet.name
761
+ pet.species
762
+ ```
763
+
764
+ ```text
765
+ struct Integer from int
766
+ endstruct
767
+
768
+ let answer = Integer{42}
769
+ [answer + 1] // 43
770
+ ```
771
+
687
772
  ## Namespaces
688
773
 
689
774
  Namespaces group functions, structs, records, and variables. Members are
@@ -1,4 +1,4 @@
1
- chemql/__init__.py,sha256=INIV28S5-HJkfc32Fpql9Cbn7tENAuHiPmmj69Zx6V0,272034
1
+ chemql/__init__.py,sha256=Wz89yVuvYim_6yunWTW0TfDudEDBkmWSef7756hmjEs,285207
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
@@ -19,9 +19,9 @@ chemql/stdlib/pathlib.cql,sha256=XkgWoPCtxW7tJKJokx52byb2RiWBKnVzG7X5uPfBFNQ,133
19
19
  chemql/stdlib/random.cql,sha256=Rvxbs-XP_gHUh0AmYmYl9WZXpJJJY3JpNNPn2HUobZU,742
20
20
  chemql/stdlib/text.cql,sha256=vBHE9b5SUxTQutaqViMteZeAC580Wg39J1eExkIx4yw,2038
21
21
  chemql/stdlib/math/statistics.cql,sha256=TVfazUbGGabUgAkLsg4LtPD8CaqZaj8mPnHbSuAmi40,1083
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,,
22
+ chemql_python-1.5.0.dist-info/licenses/LICENSE,sha256=o4ywHC0EwRy2zgIASiRD2sK3Vcsf7LBcTHjDvoJgRE0,1079
23
+ chemql_python-1.5.0.dist-info/METADATA,sha256=rsXs8GF7CfeUSe_usDVQ_rq8BlLjLf-OTJOMrc0o9f4,29414
24
+ chemql_python-1.5.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
25
+ chemql_python-1.5.0.dist-info/entry_points.txt,sha256=j98ObVWR743an524z24AiWeRsppag-py5cc7l0D0HkI,39
26
+ chemql_python-1.5.0.dist-info/top_level.txt,sha256=9UzHwnk_F6W19Q9nayaFgVyAELQXXwoYjeBgB6MWNr8,7
27
+ chemql_python-1.5.0.dist-info/RECORD,,