chemql-python 1.6.0__tar.gz → 1.6.2__tar.gz

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.
Files changed (41) hide show
  1. {chemql_python-1.6.0 → chemql_python-1.6.2}/PKG-INFO +71 -3
  2. {chemql_python-1.6.0 → chemql_python-1.6.2}/README.md +70 -2
  3. {chemql_python-1.6.0 → chemql_python-1.6.2}/pyproject.toml +1 -1
  4. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql/__init__.py +101 -9
  5. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql/codecql.py +77 -7
  6. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql_python.egg-info/PKG-INFO +71 -3
  7. {chemql_python-1.6.0 → chemql_python-1.6.2}/LICENSE +0 -0
  8. {chemql_python-1.6.0 → chemql_python-1.6.2}/setup.cfg +0 -0
  9. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql/__main__.py +0 -0
  10. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql/build.py +0 -0
  11. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql/colors.json +0 -0
  12. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql/data/BondEnergies.json +0 -0
  13. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql/data/MoleculeBonds.json +0 -0
  14. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql/data/Molecules.json +0 -0
  15. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql/data/PeriodicTableJSON.json +0 -0
  16. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql/data/Reactions.json +0 -0
  17. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql/data/THIRD_PARTY_LICENSES/PeriodicTableJSON.txt +0 -0
  18. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql/data/THIRD_PARTY_LICENSES/RHEA.txt +0 -0
  19. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql/html/periodic_table.html +0 -0
  20. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql/stdlib/README.md +0 -0
  21. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql/stdlib/collections.cql +0 -0
  22. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql/stdlib/datetime.cql +0 -0
  23. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql/stdlib/decimal.cql +0 -0
  24. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql/stdlib/fractions.cql +0 -0
  25. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql/stdlib/json.cql +0 -0
  26. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql/stdlib/math/statistics.cql +0 -0
  27. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql/stdlib/math.cql +0 -0
  28. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql/stdlib/pathlib.cql +0 -0
  29. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql/stdlib/random.cql +0 -0
  30. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql/stdlib/text.cql +0 -0
  31. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql_python.egg-info/SOURCES.txt +0 -0
  32. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql_python.egg-info/dependency_links.txt +0 -0
  33. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql_python.egg-info/entry_points.txt +0 -0
  34. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql_python.egg-info/requires.txt +0 -0
  35. {chemql_python-1.6.0 → chemql_python-1.6.2}/src/chemql_python.egg-info/top_level.txt +0 -0
  36. {chemql_python-1.6.0 → chemql_python-1.6.2}/tests/test_codecql.py +0 -0
  37. {chemql_python-1.6.0 → chemql_python-1.6.2}/tests/test_external_molecule_lookup.py +0 -0
  38. {chemql_python-1.6.0 → chemql_python-1.6.2}/tests/test_public_api.py +0 -0
  39. {chemql_python-1.6.0 → chemql_python-1.6.2}/tests/test_reactions.py +0 -0
  40. {chemql_python-1.6.0 → chemql_python-1.6.2}/tests/test_stdlib.py +0 -0
  41. {chemql_python-1.6.0 → chemql_python-1.6.2}/tests/test_structured_types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chemql-python
3
- Version: 1.6.0
3
+ Version: 1.6.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
@@ -20,7 +20,7 @@ SQL-inspired syntax, inspect elements and molecules, look up reactions,
20
20
  balance equations, and combine chemistry queries with Python and shell
21
21
  commands.
22
22
 
23
- - **Version:** `1.6.0`
23
+ - **Version:** `1.6.2`
24
24
  - **PyPI distribution:** `chemql-python`
25
25
  - **Python import / CLI name:** `chemql`
26
26
  - **Author:** Mohammad Rakibul Islam
@@ -148,10 +148,11 @@ exit
148
148
 
149
149
  ### Terminal editor
150
150
 
151
- Install the package, then open a ChemQL file with the `codecql` command:
151
+ Install the package, then open a ChemQL file with the `codecql` command or `chemql edit`:
152
152
 
153
153
  ``` bash
154
154
  codecql script.cql
155
+ chemql edit script.cql # this works too
155
156
  ```
156
157
 
157
158
  The editor supports syntax highlighting, direct text editing, word movement
@@ -888,6 +889,73 @@ available before loading the package. `pathlib::read_text` and
888
889
  `pathlib::write_text` access files with the permissions of the ChemQL
889
890
  process.
890
891
 
892
+ ## Importing Package Members
893
+
894
+ ChemQL allows you to import individual members from a package while retaining access to the package's other members through its qualified namespace.
895
+
896
+ ### Importing a Single Member
897
+
898
+ Use `load MEMBER from PACKAGE` to import a specific member into the current scope.
899
+
900
+ ```chemql
901
+ load sin from math
902
+
903
+ sin{10}
904
+ ```
905
+
906
+ The imported member can be called directly without specifying its package name.
907
+
908
+ ### Importing with an Alias
909
+
910
+ You can use `as` to assign a different name to an imported member.
911
+
912
+ ```chemql
913
+ load cos from math as c
914
+
915
+ c{10}
916
+ ```
917
+
918
+ This is useful when you want a shorter name or want to avoid naming conflicts.
919
+
920
+ ### Qualified Namespace Access
921
+
922
+ Importing an individual member does not prevent you from accessing other members of its package. ChemQL can initialize the package as needed and resolve qualified function calls using the package namespace.
923
+
924
+ ```chemql
925
+ load sin from math
926
+
927
+ # Call the imported member directly
928
+ sin{10}
929
+
930
+ # Access other package members
931
+ math::cos{10}
932
+ math::sin{30}
933
+ ```
934
+
935
+ You can therefore combine direct member imports with qualified calls in the same program.
936
+
937
+ ### Importing an Entire Package
938
+
939
+ To load a package explicitly, use `load PACKAGE` or assign it an alias with `as`.
940
+
941
+ ```chemql
942
+ load math
943
+
944
+ math::sin{10}
945
+ math::cos{10}
946
+ ```
947
+
948
+ You can also assign a package alias:
949
+
950
+ ```chemql
951
+ load math as MATH
952
+
953
+ MATH::sin{10}
954
+ MATH::cos{10}
955
+ ```
956
+
957
+ **Note:** ChemQL's package initialization and namespace resolution allow qualified package functions to remain accessible even when you initially import only one member. Importing a member is therefore a convenience for direct access, not a restriction on the rest of the package.
958
+
891
959
  ## Built-in help
892
960
 
893
961
  The help system includes a general index and topic/function pages:
@@ -6,7 +6,7 @@ SQL-inspired syntax, inspect elements and molecules, look up reactions,
6
6
  balance equations, and combine chemistry queries with Python and shell
7
7
  commands.
8
8
 
9
- - **Version:** `1.6.0`
9
+ - **Version:** `1.6.2`
10
10
  - **PyPI distribution:** `chemql-python`
11
11
  - **Python import / CLI name:** `chemql`
12
12
  - **Author:** Mohammad Rakibul Islam
@@ -134,10 +134,11 @@ exit
134
134
 
135
135
  ### Terminal editor
136
136
 
137
- Install the package, then open a ChemQL file with the `codecql` command:
137
+ Install the package, then open a ChemQL file with the `codecql` command or `chemql edit`:
138
138
 
139
139
  ``` bash
140
140
  codecql script.cql
141
+ chemql edit script.cql # this works too
141
142
  ```
142
143
 
143
144
  The editor supports syntax highlighting, direct text editing, word movement
@@ -874,6 +875,73 @@ available before loading the package. `pathlib::read_text` and
874
875
  `pathlib::write_text` access files with the permissions of the ChemQL
875
876
  process.
876
877
 
878
+ ## Importing Package Members
879
+
880
+ ChemQL allows you to import individual members from a package while retaining access to the package's other members through its qualified namespace.
881
+
882
+ ### Importing a Single Member
883
+
884
+ Use `load MEMBER from PACKAGE` to import a specific member into the current scope.
885
+
886
+ ```chemql
887
+ load sin from math
888
+
889
+ sin{10}
890
+ ```
891
+
892
+ The imported member can be called directly without specifying its package name.
893
+
894
+ ### Importing with an Alias
895
+
896
+ You can use `as` to assign a different name to an imported member.
897
+
898
+ ```chemql
899
+ load cos from math as c
900
+
901
+ c{10}
902
+ ```
903
+
904
+ This is useful when you want a shorter name or want to avoid naming conflicts.
905
+
906
+ ### Qualified Namespace Access
907
+
908
+ Importing an individual member does not prevent you from accessing other members of its package. ChemQL can initialize the package as needed and resolve qualified function calls using the package namespace.
909
+
910
+ ```chemql
911
+ load sin from math
912
+
913
+ # Call the imported member directly
914
+ sin{10}
915
+
916
+ # Access other package members
917
+ math::cos{10}
918
+ math::sin{30}
919
+ ```
920
+
921
+ You can therefore combine direct member imports with qualified calls in the same program.
922
+
923
+ ### Importing an Entire Package
924
+
925
+ To load a package explicitly, use `load PACKAGE` or assign it an alias with `as`.
926
+
927
+ ```chemql
928
+ load math
929
+
930
+ math::sin{10}
931
+ math::cos{10}
932
+ ```
933
+
934
+ You can also assign a package alias:
935
+
936
+ ```chemql
937
+ load math as MATH
938
+
939
+ MATH::sin{10}
940
+ MATH::cos{10}
941
+ ```
942
+
943
+ **Note:** ChemQL's package initialization and namespace resolution allow qualified package functions to remain accessible even when you initially import only one member. Importing a member is therefore a convenience for direct access, not a restriction on the rest of the package.
944
+
877
945
  ## Built-in help
878
946
 
879
947
  The help system includes a general index and topic/function pages:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "chemql-python"
7
- version = "1.6.0"
7
+ version = "1.6.2"
8
8
  authors = [
9
9
  { name = "Mohammad Rakibul Islam", email = "rakibulofficial1017@gmail.com" }
10
10
  ]
@@ -68,7 +68,7 @@ __all__ = [
68
68
  "__version__"
69
69
  ]
70
70
 
71
- __version__ = "1.6.0"
71
+ __version__ = "1.6.2"
72
72
 
73
73
  # Persistent preprocessor/function state for the current ChemQL session.
74
74
  DEFINITIONS = {}
@@ -1094,6 +1094,24 @@ def _expand_function_calls(text):
1094
1094
  if match:
1095
1095
  name = match.group(1)
1096
1096
  resolved_name = _resolve_package_qualified_name(name)
1097
+ # Enforce explicit package loading BEFORE checking FUNCTIONS.
1098
+ # Otherwise a function registered while internally initializing
1099
+ # a package bypasses the qualified-access restriction below.
1100
+ if "::" in resolved_name:
1101
+ package_name = resolved_name.rsplit("::", 1)[0]
1102
+ root_package = package_name.split("::", 1)[0]
1103
+ explicitly_loaded_packages = globals().get(
1104
+ "CHEMQL_EXPLICITLY_LOADED_PACKAGES", set()
1105
+ )
1106
+ if (
1107
+ package_name not in explicitly_loaded_packages
1108
+ and root_package not in explicitly_loaded_packages
1109
+ ):
1110
+ raise ChemQLError(
1111
+ f"Unknown function `{name}`"
1112
+ if NO_ANSI else
1113
+ f"Unknown function \\033[1m{name}\\033[22m"
1114
+ )
1097
1115
  opening = i + match.end() - 1
1098
1116
  end = find_matching(text, opening, "{", "}")
1099
1117
  args = _split_function_arguments(text[opening + 1:end])
@@ -1103,6 +1121,47 @@ def _expand_function_calls(text):
1103
1121
  value = _call_function(resolved_name, args)
1104
1122
  elif "::" not in resolved_name and resolved_name in BUILTIN_FUNCTIONS:
1105
1123
  value = _call_builtin(resolved_name, args)
1124
+ elif (
1125
+ "::" not in resolved_name
1126
+ and resolved_name in CHEMQL_VARIABLES
1127
+ and callable(python_globals.get(resolved_name))
1128
+ ):
1129
+ # `load member from package` binds the actual public member
1130
+ # in the global scope. Imported function wrappers therefore
1131
+ # need to be callable through the same `name{args}` syntax
1132
+ # as registered ChemQL functions.
1133
+ imported_callable = python_globals[resolved_name]
1134
+ values = [evaluate_python(arg) for arg in args]
1135
+ value = imported_callable(*values)
1136
+ elif "::" in resolved_name:
1137
+ # A qualified call is permitted only after the user has
1138
+ # explicitly loaded its package (possibly under an alias).
1139
+ # `_ensure_package_namespace()` is also used internally by
1140
+ # `load MEMBER from package`; that internal initialization
1141
+ # must not make every member of the package accessible.
1142
+ package_name = resolved_name.rsplit("::", 1)[0]
1143
+ root_package = package_name.split("::", 1)[0]
1144
+ explicitly_loaded_packages = globals().get(
1145
+ "CHEMQL_EXPLICITLY_LOADED_PACKAGES", set()
1146
+ )
1147
+ # Explicit imports are tracked separately from package bindings:
1148
+ # internal package initialization and incidental bindings must not
1149
+ # expose sibling members through qualified calls.
1150
+ explicitly_loaded = (
1151
+ package_name in explicitly_loaded_packages
1152
+ or root_package in explicitly_loaded_packages
1153
+ )
1154
+ if explicitly_loaded:
1155
+ try:
1156
+ _ensure_package_namespace(package_name)
1157
+ except (ImportError, OSError, ValueError):
1158
+ pass
1159
+ if explicitly_loaded and resolved_name in FUNCTIONS:
1160
+ if not _namespace_function_is_callable(resolved_name):
1161
+ raise PermissionError((f"Function `{name}` is private" if NO_ANSI else f"Function \033[1m{name}\033[22m is private"))
1162
+ value = _call_function(resolved_name, args)
1163
+ else:
1164
+ raise ChemQLError((f"Unknown function `{name}`" if NO_ANSI else f"Unknown function \033[1m{name}\033[22m"))
1106
1165
  elif (
1107
1166
  "::" not in resolved_name
1108
1167
  and isinstance(python_globals.get(resolved_name), type)
@@ -4571,6 +4630,9 @@ def _find_unquoted_marker(text, marker, start=0):
4571
4630
  # Canonical ChemQL package names and user-defined aliases are deliberately
4572
4631
  # tracked separately. An alias must never replace a canonical package binding.
4573
4632
  CHEMQL_PACKAGE_BINDINGS = {}
4633
+ # Tracks packages explicitly imported by the user. Internal namespace initialization
4634
+ # for `load MEMBER from package` must not grant qualified-call access.
4635
+ CHEMQL_EXPLICITLY_LOADED_PACKAGES = set()
4574
4636
  CHEMQL_PACKAGE_SOURCE_LOADED = set()
4575
4637
 
4576
4638
 
@@ -4706,22 +4768,42 @@ def _ensure_package_namespace(package_name, allow_namespace_only=False):
4706
4768
  globals()["_CURRENT_VISIBILITY"] = old_visibility
4707
4769
  CHEMQL_PACKAGE_SOURCE_LOADED.add(package_name)
4708
4770
 
4709
- # A namespace-only parent is a canonical container, not an independently
4710
- # loadable package unless it has its own .cql source file.
4711
- if path is not None:
4712
- CHEMQL_PACKAGE_BINDINGS.setdefault(package_name, package_name)
4771
+ # Merely resolving a package namespace must not count as a user-visible
4772
+ # package import. `_execute_load_statement` records bindings explicitly.
4713
4773
  return namespace
4714
4774
 
4715
4775
 
4716
4776
  def _execute_load_statement(statement):
4717
- """Load a ChemQL .cql package, optionally binding an explicit alias."""
4777
+ """Load a package, optionally alias it or import one public member."""
4778
+ from_match = re.fullmatch(
4779
+ r"load\s+([A-Za-z_]\w*)\s+from\s+([A-Za-z_]\w*(?:::[A-Za-z_]\w*)*)(?:\s+as\s+([A-Za-z_]\w*))?",
4780
+ statement,
4781
+ )
4782
+ if from_match:
4783
+ member_name, package_name, alias = from_match.groups()
4784
+ binding_name = alias or member_name
4785
+ namespace = _ensure_package_namespace(package_name)
4786
+ if member_name not in namespace._chemql_public:
4787
+ raise ImportError(f"Package `{package_name}` has no public member `{member_name}`")
4788
+ value = getattr(namespace, member_name)
4789
+ # Bind only the requested member, under either its original name or
4790
+ # the explicit alias. Loading the source is necessary to resolve it,
4791
+ # but does not create a package-name binding in the user's globals.
4792
+ if binding_name in CHEMQL_PACKAGE_BINDINGS:
4793
+ raise ImportError(f"Name `{binding_name}` is already bound as a package name; cannot import `{member_name}` from `{package_name}`.")
4794
+ if binding_name in CHEMQL_VARIABLES:
4795
+ raise ImportError(f"Name `{binding_name}` is already defined; cannot import `{member_name}` from `{package_name}`.")
4796
+ python_globals[binding_name] = value
4797
+ CHEMQL_VARIABLES.add(binding_name)
4798
+ return True
4799
+
4718
4800
  match = re.fullmatch(
4719
4801
  r"load\s+([A-Za-z_]\w*(?:::[A-Za-z_]\w*)*)(?:\s+as\s+([A-Za-z_]\w*))?",
4720
4802
  statement,
4721
4803
  )
4722
4804
  if not match:
4723
4805
  if statement.startswith("load"):
4724
- raise SyntaxError(("Expected `load package::name` or `load package::name as alias`" if NO_ANSI else "Expected \033[1mload package::name\033[22m or \033[1mload package::name as alias\033[22m"))
4806
+ raise SyntaxError(("Expected `load package`, `load package as alias`, or `load MEMBER from package [as alias]`" if NO_ANSI else "Expected \033[1mload package\033[22m, \033[1mload package as alias\033[22m, or \033[1mload MEMBER from package [as alias]\033[22m"))
4725
4807
  return False
4726
4808
 
4727
4809
  package_name, alias = match.groups()
@@ -4827,6 +4909,10 @@ def _execute_load_statement(statement):
4827
4909
  CHEMQL_PACKAGE_BINDINGS[package_name] = package_name
4828
4910
  python_globals[package_name] = namespace
4829
4911
  CHEMQL_VARIABLES.add(package_name)
4912
+
4913
+ # Only `load package [as alias]` reaches this point. Member imports return
4914
+ # earlier, so they never grant qualified access to sibling package members.
4915
+ CHEMQL_EXPLICITLY_LOADED_PACKAGES.add(package_name)
4830
4916
  return True
4831
4917
 
4832
4918
 
@@ -6538,7 +6624,7 @@ class ChemqlLexer(Lexer):
6538
6624
  CONTROL_WORDS = {
6539
6625
  "if", "elif", "else", "for", "while", "break", "continue", "endblock",
6540
6626
  "endfor", "endwhile", "endif", "endrisk", "endstruct", "endrecord", "endenum",
6541
- "endfunc", "enddefine", "func", "define", "struct", "record", "risk",
6627
+ "endfunc", "enddefine", "func", "define", "struct", "record", "risk", "static",
6542
6628
  "fix", "cleanup", "signal", "import", "as", "return", "public", "private",
6543
6629
  "required", "namespace", "endnamespace", "enum", "from"
6544
6630
  }
@@ -7437,6 +7523,12 @@ def _init_project(name=None, directory="."):
7437
7523
 
7438
7524
 
7439
7525
  def main(ANSI: bool = True):
7526
+ if len(sys.argv) >= 2 and sys.argv[1] == "edit":
7527
+ from .codecql import main as codecql_main # type: ignore
7528
+
7529
+ sys.argv = ["codecql"] + sys.argv[2:]
7530
+ codecql_main()
7531
+ return
7440
7532
  parser = argparse.ArgumentParser(
7441
7533
  description=(
7442
7534
  "ChemQL query and chemistry language.\n"
@@ -7586,7 +7678,7 @@ def run_interactive():
7586
7678
  closer = "]]]" if first == "[[[" else "```"
7587
7679
  return not (len(source_lines) > 1 and last == closer)
7588
7680
 
7589
- if re.fullmatch(r"(?:(?:static\s+func|func\s+static|func)\s+)" + _DEFINITION_NAME, first, re.IGNORECASE):
7681
+ if re.fullmatch(r"(?:(?:static\s+func|func\s+static|func)\s+)" + _DEFINITION_NAME + "(?:\\." + _DEFINITION_NAME + ")?" , first, re.IGNORECASE):
7590
7682
  return last.casefold() != "endfunc"
7591
7683
 
7592
7684
  if re.fullmatch(r"define\s+" + _DEFINITION_NAME + r"\s*\{\s*\}", first):
@@ -10,7 +10,7 @@ import shutil
10
10
  import subprocess
11
11
  import time
12
12
 
13
- VERSION = "0.0.8"
13
+ VERSION = "0.0.10"
14
14
 
15
15
 
16
16
  def _write_system_clipboard(text):
@@ -275,7 +275,7 @@ COMMANDS = {
275
275
  CONTROL_KEYWORDS = {
276
276
  "if", "elif", "else", "endif", "for", "while", "in", "endfor",
277
277
  "endwhile", "break", "continue", "endblock", "from", "as", "public",
278
- "private", "required", "struct", "record", "enum", "endstruct",
278
+ "private", "required", "struct", "record", "enum", "endstruct", "static",
279
279
  "endrecord", "endenum", "namespace", "endnamespace",
280
280
  }
281
281
  WORD_OPERATORS = {
@@ -357,7 +357,76 @@ def _token_style(kind, value):
357
357
  return HIGHLIGHT_STYLES[category]
358
358
 
359
359
 
360
- def apply_syntax_highlighting(text):
360
+ MANIFEST_TOKEN_RE = re.compile(
361
+ r"(?P<comment>#.*$)"
362
+ r"|(?P<string>'(?:\\.|[^'\\])*'|\"(?:\\.|[^\"\\])*\")"
363
+ r"|(?P<number>(?<![\w.])[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?(?!\w))"
364
+ r"|(?P<literal>\b(?:true|false|null)\b)"
365
+ r"|(?P<identifier>[A-Za-z_][A-Za-z0-9_-]*)"
366
+ r"|(?P<operator>=)"
367
+ r"|(?P<delimiter>[\[\]{}:,.])",
368
+ re.IGNORECASE,
369
+ )
370
+
371
+ def _apply_manifest_syntax_highlighting(text, filename):
372
+ """Highlight JSON/TOML package manifests with the editor's existing theme."""
373
+ is_json = os.path.basename(filename).casefold() == "cqlpackage.json"
374
+ is_toml = os.path.basename(filename).casefold() == "cqlpackage.toml"
375
+ if not (is_json or is_toml):
376
+ return None
377
+
378
+ # TOML table headers are clearer when highlighted as a unit.
379
+ stripped = text.lstrip()
380
+ if is_toml and stripped.startswith("[") and stripped.endswith("]"):
381
+ leading = text[:len(text) - len(stripped)]
382
+ return leading + HIGHLIGHT_STYLES["type"] + stripped + RESET
383
+
384
+ parts = []
385
+ last_end = 0
386
+ for match in MANIFEST_TOKEN_RE.finditer(text):
387
+ parts.append(text[last_end:match.start()])
388
+ token = match.group()
389
+ kind = match.lastgroup
390
+ tail = text[match.end():]
391
+ style = ""
392
+
393
+ if kind == "comment" and is_toml:
394
+ style = HIGHLIGHT_STYLES["comment"]
395
+ elif kind == "string":
396
+ next_nonspace = tail.lstrip()[:1]
397
+ if (is_json and next_nonspace == ":") or (is_toml and next_nonspace == "="):
398
+ style = HIGHLIGHT_STYLES["property"]
399
+ else:
400
+ style = HIGHLIGHT_STYLES["string"]
401
+ elif kind == "number":
402
+ style = HIGHLIGHT_STYLES["number"]
403
+ elif kind == "literal":
404
+ # JSON literals are lowercase; TOML booleans are lowercase too.
405
+ style = HIGHLIGHT_STYLES["constant"]
406
+ elif kind == "identifier" and is_toml:
407
+ if tail.lstrip().startswith("="):
408
+ style = HIGHLIGHT_STYLES["property"]
409
+ elif token.casefold() in {"true", "false", "inf", "nan"}:
410
+ style = HIGHLIGHT_STYLES["constant"]
411
+ elif kind == "operator":
412
+ style = HIGHLIGHT_STYLES["operator"]
413
+ elif kind == "delimiter" and token.strip():
414
+ style = HIGHLIGHT_STYLES["delimiter"]
415
+
416
+ if style:
417
+ parts.extend((style, token, RESET))
418
+ else:
419
+ parts.append(token)
420
+ last_end = match.end()
421
+ parts.append(text[last_end:])
422
+ return "".join(parts)
423
+
424
+
425
+ def apply_syntax_highlighting(text, filename=None):
426
+ if filename:
427
+ manifest_highlighting = _apply_manifest_syntax_highlighting(text, filename)
428
+ if manifest_highlighting is not None:
429
+ return manifest_highlighting
361
430
  parts = []
362
431
  last_end = 0
363
432
  for match in TOKEN_RE.finditer(text):
@@ -495,7 +564,7 @@ class Editor:
495
564
  current = (match_start, match_end) == self.current_search_match
496
565
  overlays.append((start, end, "\x1b[7m" if current else "\x1b[43m", 1))
497
566
  if not overlays:
498
- return apply_syntax_highlighting(visible_text)
567
+ return apply_syntax_highlighting(visible_text, self.filename)
499
568
 
500
569
  boundaries = {line_start, line_end}
501
570
  for start, end, _, _ in overlays:
@@ -520,7 +589,7 @@ class Editor:
520
589
  style = max(active, key=lambda overlay: overlay[3])[2]
521
590
  parts.extend((style, segment, RESET))
522
591
  else:
523
- parts.append(apply_syntax_highlighting(segment))
592
+ parts.append(apply_syntax_highlighting(segment, self.filename))
524
593
  return "".join(parts)
525
594
 
526
595
  def _keep_cursor_visible(self, height, width, visual_cursor_x):
@@ -628,8 +697,9 @@ class Editor:
628
697
  self._insert_newline()
629
698
  elif key == "\t":
630
699
  self._insert_text("\t")
631
- elif key and key.isprintable() and "+" not in key:
632
- # Symbolic key names (e.g. "alt+delete") are control events, not text.
700
+ elif key and key.isprintable() and (len(key) == 1 or "+" not in key):
701
+ # Single printable characters (including '+') are text; multi-part
702
+ # names such as "alt+delete" are symbolic key events.
633
703
  self._insert_text(key)
634
704
  if key not in ("\x11",):
635
705
  self.quit_requested = False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chemql-python
3
- Version: 1.6.0
3
+ Version: 1.6.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
@@ -20,7 +20,7 @@ SQL-inspired syntax, inspect elements and molecules, look up reactions,
20
20
  balance equations, and combine chemistry queries with Python and shell
21
21
  commands.
22
22
 
23
- - **Version:** `1.6.0`
23
+ - **Version:** `1.6.2`
24
24
  - **PyPI distribution:** `chemql-python`
25
25
  - **Python import / CLI name:** `chemql`
26
26
  - **Author:** Mohammad Rakibul Islam
@@ -148,10 +148,11 @@ exit
148
148
 
149
149
  ### Terminal editor
150
150
 
151
- Install the package, then open a ChemQL file with the `codecql` command:
151
+ Install the package, then open a ChemQL file with the `codecql` command or `chemql edit`:
152
152
 
153
153
  ``` bash
154
154
  codecql script.cql
155
+ chemql edit script.cql # this works too
155
156
  ```
156
157
 
157
158
  The editor supports syntax highlighting, direct text editing, word movement
@@ -888,6 +889,73 @@ available before loading the package. `pathlib::read_text` and
888
889
  `pathlib::write_text` access files with the permissions of the ChemQL
889
890
  process.
890
891
 
892
+ ## Importing Package Members
893
+
894
+ ChemQL allows you to import individual members from a package while retaining access to the package's other members through its qualified namespace.
895
+
896
+ ### Importing a Single Member
897
+
898
+ Use `load MEMBER from PACKAGE` to import a specific member into the current scope.
899
+
900
+ ```chemql
901
+ load sin from math
902
+
903
+ sin{10}
904
+ ```
905
+
906
+ The imported member can be called directly without specifying its package name.
907
+
908
+ ### Importing with an Alias
909
+
910
+ You can use `as` to assign a different name to an imported member.
911
+
912
+ ```chemql
913
+ load cos from math as c
914
+
915
+ c{10}
916
+ ```
917
+
918
+ This is useful when you want a shorter name or want to avoid naming conflicts.
919
+
920
+ ### Qualified Namespace Access
921
+
922
+ Importing an individual member does not prevent you from accessing other members of its package. ChemQL can initialize the package as needed and resolve qualified function calls using the package namespace.
923
+
924
+ ```chemql
925
+ load sin from math
926
+
927
+ # Call the imported member directly
928
+ sin{10}
929
+
930
+ # Access other package members
931
+ math::cos{10}
932
+ math::sin{30}
933
+ ```
934
+
935
+ You can therefore combine direct member imports with qualified calls in the same program.
936
+
937
+ ### Importing an Entire Package
938
+
939
+ To load a package explicitly, use `load PACKAGE` or assign it an alias with `as`.
940
+
941
+ ```chemql
942
+ load math
943
+
944
+ math::sin{10}
945
+ math::cos{10}
946
+ ```
947
+
948
+ You can also assign a package alias:
949
+
950
+ ```chemql
951
+ load math as MATH
952
+
953
+ MATH::sin{10}
954
+ MATH::cos{10}
955
+ ```
956
+
957
+ **Note:** ChemQL's package initialization and namespace resolution allow qualified package functions to remain accessible even when you initially import only one member. Importing a member is therefore a convenience for direct access, not a restriction on the rest of the package.
958
+
891
959
  ## Built-in help
892
960
 
893
961
  The help system includes a general index and topic/function pages:
File without changes
File without changes