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 +279 -46
- {chemql_python-1.4.2.dist-info → chemql_python-1.5.0.dist-info}/METADATA +88 -3
- {chemql_python-1.4.2.dist-info → chemql_python-1.5.0.dist-info}/RECORD +7 -7
- {chemql_python-1.4.2.dist-info → chemql_python-1.5.0.dist-info}/WHEEL +0 -0
- {chemql_python-1.4.2.dist-info → chemql_python-1.5.0.dist-info}/entry_points.txt +0 -0
- {chemql_python-1.4.2.dist-info → chemql_python-1.5.0.dist-info}/licenses/LICENSE +0 -0
- {chemql_python-1.4.2.dist-info → chemql_python-1.5.0.dist-info}/top_level.txt +0 -0
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.
|
|
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
|
|
4653
|
-
kind = re.match(r"^(struct|record)", declaration, re.IGNORECASE).group(1).casefold() # type:ignore
|
|
4654
|
-
terminator = "endstruct"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
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
|
-
|
|
4709
|
-
|
|
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(
|
|
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
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
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
|
-
|
|
4739
|
-
|
|
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[
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
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*",
|
|
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.
|
|
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.
|
|
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.
|
|
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=
|
|
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.
|
|
23
|
-
chemql_python-1.
|
|
24
|
-
chemql_python-1.
|
|
25
|
-
chemql_python-1.
|
|
26
|
-
chemql_python-1.
|
|
27
|
-
chemql_python-1.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|