chemql-python 1.5.3__tar.gz → 1.5.4__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.
- {chemql_python-1.5.3 → chemql_python-1.5.4}/PKG-INFO +4 -2
- {chemql_python-1.5.3 → chemql_python-1.5.4}/README.md +3 -1
- {chemql_python-1.5.3 → chemql_python-1.5.4}/pyproject.toml +1 -1
- {chemql_python-1.5.3 → chemql_python-1.5.4}/src/chemql/__init__.py +83 -57
- {chemql_python-1.5.3 → chemql_python-1.5.4}/src/chemql/codecql.py +145 -3
- {chemql_python-1.5.3 → chemql_python-1.5.4}/src/chemql_python.egg-info/PKG-INFO +4 -2
- {chemql_python-1.5.3 → chemql_python-1.5.4}/tests/test_codecql.py +53 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/LICENSE +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/setup.cfg +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/src/chemql/__main__.py +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/src/chemql/colors.json +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/src/chemql/data/BondEnergies.json +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/src/chemql/data/MoleculeBonds.json +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/src/chemql/data/Molecules.json +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/src/chemql/data/PeriodicTableJSON.json +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/src/chemql/data/Reactions.json +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/src/chemql/data/THIRD_PARTY_LICENSES/PeriodicTableJSON.txt +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/src/chemql/data/THIRD_PARTY_LICENSES/RHEA.txt +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/src/chemql/html/periodic_table.html +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/src/chemql/stdlib/README.md +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/src/chemql/stdlib/collections.cql +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/src/chemql/stdlib/datetime.cql +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/src/chemql/stdlib/decimal.cql +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/src/chemql/stdlib/fractions.cql +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/src/chemql/stdlib/json.cql +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/src/chemql/stdlib/math/statistics.cql +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/src/chemql/stdlib/math.cql +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/src/chemql/stdlib/pathlib.cql +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/src/chemql/stdlib/random.cql +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/src/chemql/stdlib/text.cql +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/src/chemql_python.egg-info/SOURCES.txt +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/src/chemql_python.egg-info/dependency_links.txt +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/src/chemql_python.egg-info/entry_points.txt +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/src/chemql_python.egg-info/requires.txt +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/src/chemql_python.egg-info/top_level.txt +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/tests/test_external_molecule_lookup.py +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/tests/test_public_api.py +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/tests/test_reactions.py +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/tests/test_stdlib.py +0 -0
- {chemql_python-1.5.3 → chemql_python-1.5.4}/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.5.
|
|
3
|
+
Version: 1.5.4
|
|
4
4
|
Summary: A chemistry-focused query language and REPL
|
|
5
5
|
Author-email: Mohammad Rakibul Islam <rakibulofficial1017@gmail.com>
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -19,7 +19,7 @@ SQL-inspired syntax, inspect elements and molecules, look up reactions,
|
|
|
19
19
|
balance equations, and combine chemistry queries with Python and shell
|
|
20
20
|
commands.
|
|
21
21
|
|
|
22
|
-
- **Version:** `1.5.
|
|
22
|
+
- **Version:** `1.5.4`
|
|
23
23
|
- **PyPI distribution:** `chemql-python`
|
|
24
24
|
- **Python import / CLI name:** `chemql`
|
|
25
25
|
- **Author:** Mohammad Rakibul Islam
|
|
@@ -170,6 +170,8 @@ default. Prefix a query with `r:` for a case-sensitive regular expression,
|
|
|
170
170
|
literal text, or `li:` for case-insensitive literal text. Add `w` to match
|
|
171
171
|
whole words, for example `lwi:cat`.
|
|
172
172
|
|
|
173
|
+
Press `CTRL+T` to open a terminal inside the code editor to test your code. You can leave the terminal by pressing the Escape key
|
|
174
|
+
|
|
173
175
|
Use `Ctrl+Up` and `Ctrl+Down` to scroll the viewport while keeping the cursor
|
|
174
176
|
at the same screen position.
|
|
175
177
|
|
|
@@ -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.5.
|
|
9
|
+
- **Version:** `1.5.4`
|
|
10
10
|
- **PyPI distribution:** `chemql-python`
|
|
11
11
|
- **Python import / CLI name:** `chemql`
|
|
12
12
|
- **Author:** Mohammad Rakibul Islam
|
|
@@ -157,6 +157,8 @@ default. Prefix a query with `r:` for a case-sensitive regular expression,
|
|
|
157
157
|
literal text, or `li:` for case-insensitive literal text. Add `w` to match
|
|
158
158
|
whole words, for example `lwi:cat`.
|
|
159
159
|
|
|
160
|
+
Press `CTRL+T` to open a terminal inside the code editor to test your code. You can leave the terminal by pressing the Escape key
|
|
161
|
+
|
|
160
162
|
Use `Ctrl+Up` and `Ctrl+Down` to scroll the viewport while keeping the cursor
|
|
161
163
|
at the same screen position.
|
|
162
164
|
|
|
@@ -64,7 +64,7 @@ __all__ = [
|
|
|
64
64
|
"__version__"
|
|
65
65
|
]
|
|
66
66
|
|
|
67
|
-
__version__ = "1.5.
|
|
67
|
+
__version__ = "1.5.4"
|
|
68
68
|
|
|
69
69
|
# Persistent preprocessor/function state for the current ChemQL session.
|
|
70
70
|
DEFINITIONS = {}
|
|
@@ -568,16 +568,16 @@ def _help_text(topic=None):
|
|
|
568
568
|
def _call_builtin(name, arguments):
|
|
569
569
|
entry = BUILTIN_FUNCTIONS.get(name)
|
|
570
570
|
if entry is None:
|
|
571
|
-
raise NameError(f"Unknown ChemQL built-in `{name}`")
|
|
571
|
+
raise NameError((f"Unknown ChemQL built-in `{name}`" if NO_ANSI else f"Unknown ChemQL built-in \033[1m{name}\033[22m"))
|
|
572
572
|
function, minimum, maximum = entry
|
|
573
573
|
count = len(arguments)
|
|
574
574
|
if count < minimum:
|
|
575
|
-
raise TooFewArguments(f"Too few arguments for `{name}`: expected at least {minimum}, got {count}")
|
|
575
|
+
raise TooFewArguments((f"Too few arguments for `{name}`: expected at least {minimum}, got {count}" if NO_ANSI else f"Too few arguments for \033[1m{name}\033[22m: expected at least {minimum}, got {count}"))
|
|
576
576
|
if maximum is not None and count > maximum:
|
|
577
|
-
raise TooManyArguments(f"Too many arguments for `{name}`: expected at most {maximum}, got {count}")
|
|
577
|
+
raise TooManyArguments((f"Too many arguments for `{name}`: expected at most {maximum}, got {count}" if NO_ANSI else f"Too many arguments for \033[1m{name}\033[22m: expected at most {maximum}, got {count}"))
|
|
578
578
|
if name == "help":
|
|
579
579
|
if len(arguments) > 1:
|
|
580
|
-
raise TooManyArguments("Too many arguments for `help`: expected at most 1, got " + str(len(arguments)))
|
|
580
|
+
raise TooManyArguments(("Too many arguments for `help`: expected at most 1, got " + str(len(arguments)) if NO_ANSI else "Too many arguments for \033[1mhelp\033[22m: expected at most 1, got " + str(len(arguments))))
|
|
581
581
|
topic = arguments[0].strip() if arguments else None
|
|
582
582
|
if topic is not None and len(topic) >= 2 and topic[0] in ("\"", "'") and topic[-1] == topic[0]:
|
|
583
583
|
try:
|
|
@@ -591,18 +591,18 @@ def _call_builtin(name, arguments):
|
|
|
591
591
|
except ChemQLBuiltinError:
|
|
592
592
|
raise
|
|
593
593
|
except TypeError as exc:
|
|
594
|
-
raise InvalidArgumentType(f"Invalid argument type for `{name}`: {exc}") from exc
|
|
594
|
+
raise InvalidArgumentType((f"Invalid argument type for `{name}`: {exc}" if NO_ANSI else f"Invalid argument type for \033[1m{name}\033[22m: {exc}")) from exc
|
|
595
595
|
except ValueError as exc:
|
|
596
|
-
raise InvalidArgumentValue(f"Invalid argument value for `{name}`: {exc}") from exc
|
|
596
|
+
raise InvalidArgumentValue((f"Invalid argument value for `{name}`: {exc}" if NO_ANSI else f"Invalid argument value for \033[1m{name}\033[22m: {exc}")) from exc
|
|
597
597
|
except IndexError as exc:
|
|
598
|
-
raise InvalidArgumentValue(f"Invalid argument value for `{name}`: {exc}") from exc
|
|
598
|
+
raise InvalidArgumentValue((f"Invalid argument value for `{name}`: {exc}" if NO_ANSI else f"Invalid argument value for \033[1m{name}\033[22m: {exc}")) from exc
|
|
599
599
|
|
|
600
600
|
|
|
601
601
|
def _call_function(name, arguments):
|
|
602
602
|
"""Call a ChemQL function, including let assignments and block syntax."""
|
|
603
603
|
global _CURRENT_NAMESPACE
|
|
604
604
|
if name not in FUNCTIONS:
|
|
605
|
-
raise NameError(f"Unknown ChemQL function `{name}`")
|
|
605
|
+
raise NameError((f"Unknown ChemQL function `{name}`" if NO_ANSI else f"Unknown ChemQL function \033[1m{name}\033[22m"))
|
|
606
606
|
|
|
607
607
|
values = [evaluate_python(arg) for arg in arguments]
|
|
608
608
|
definition = FUNCTIONS[name]
|
|
@@ -697,7 +697,7 @@ def _call_function(name, arguments):
|
|
|
697
697
|
if risk_depth == 0: break
|
|
698
698
|
end_risk += 1
|
|
699
699
|
if end_risk >= len(lines):
|
|
700
|
-
raise SyntaxError(f"Missing `endrisk` in function `{name}`")
|
|
700
|
+
raise SyntaxError((f"Missing `endrisk` in function `{name}`" if NO_ANSI else f"Missing \033[1mendrisk\033[22m in function \033[1m{name}\033[22m"))
|
|
701
701
|
sections = {"risk": [], "fix": [], "cleanup": []}
|
|
702
702
|
section = "risk"
|
|
703
703
|
for risk_line in lines[i + 1:end_risk]:
|
|
@@ -748,7 +748,7 @@ def _call_function(name, arguments):
|
|
|
748
748
|
|
|
749
749
|
if i >= len(lines):
|
|
750
750
|
raise SyntaxError(
|
|
751
|
-
f"Missing `]]]` in function `{name}`"
|
|
751
|
+
(f"Missing `]]]` in function `{name}`" if NO_ANSI else f"Missing \033[1m]]]\033[22m in function \033[1m{name}\033[22m")
|
|
752
752
|
)
|
|
753
753
|
|
|
754
754
|
evaluate_python("\n".join(block))
|
|
@@ -846,7 +846,7 @@ def _call_function(name, arguments):
|
|
|
846
846
|
|
|
847
847
|
else:
|
|
848
848
|
raise SyntaxError(
|
|
849
|
-
f"Unsupported function block `{code}`"
|
|
849
|
+
(f"Unsupported function block `{code}`" if NO_ANSI else f"Unsupported function block \033[1m{code}\033[22m")
|
|
850
850
|
)
|
|
851
851
|
|
|
852
852
|
i = end + 1
|
|
@@ -854,7 +854,7 @@ def _call_function(name, arguments):
|
|
|
854
854
|
|
|
855
855
|
if line == "[endblock]":
|
|
856
856
|
raise SyntaxError(
|
|
857
|
-
f"Unexpected `[endblock]` in function `{name}`"
|
|
857
|
+
(f"Unexpected `[endblock]` in function `{name}`" if NO_ANSI else f"Unexpected \033[1m[endblock]\033[22m in function \033[1m{name}\033[22m")
|
|
858
858
|
)
|
|
859
859
|
|
|
860
860
|
# Handle return statements inside the function.
|
|
@@ -963,7 +963,7 @@ def _expand_function_calls(text):
|
|
|
963
963
|
args = _split_function_arguments(text[opening + 1:end])
|
|
964
964
|
if resolved_name in FUNCTIONS:
|
|
965
965
|
if not _namespace_function_is_callable(resolved_name):
|
|
966
|
-
raise PermissionError(f"Function `{name}` is private")
|
|
966
|
+
raise PermissionError((f"Function `{name}` is private" if NO_ANSI else f"Function \033[1m{name}\033[22m is private"))
|
|
967
967
|
value = _call_function(resolved_name, args)
|
|
968
968
|
elif "::" not in resolved_name and resolved_name in BUILTIN_FUNCTIONS:
|
|
969
969
|
value = _call_builtin(resolved_name, args)
|
|
@@ -978,7 +978,7 @@ def _expand_function_calls(text):
|
|
|
978
978
|
changed = True
|
|
979
979
|
break
|
|
980
980
|
else:
|
|
981
|
-
raise ChemQLError(f"Unknown function `{name}`")
|
|
981
|
+
raise ChemQLError((f"Unknown function `{name}`" if NO_ANSI else f"Unknown function \033[1m{name}\033[22m"))
|
|
982
982
|
replacement = repr(value) if isinstance(value, str) else ("None" if value is None else str(value))
|
|
983
983
|
text = text[:i] + replacement + text[end + 1:]
|
|
984
984
|
changed = True
|
|
@@ -2839,7 +2839,7 @@ def _formula_counts(formula):
|
|
|
2839
2839
|
char = formula[index]
|
|
2840
2840
|
if char == ")":
|
|
2841
2841
|
if not nested:
|
|
2842
|
-
raise ValueError(f"Unmatched closing parenthesis in formula `{formula}`")
|
|
2842
|
+
raise ValueError((f"Unmatched closing parenthesis in formula `{formula}`" if NO_ANSI else f"Unmatched closing parenthesis in formula \033[1m{formula}\033[22m"))
|
|
2843
2843
|
return counts, index
|
|
2844
2844
|
if char == "(":
|
|
2845
2845
|
group, index = parse_group(index + 1, True)
|
|
@@ -4391,7 +4391,7 @@ def _package_relative_path(package_name):
|
|
|
4391
4391
|
"""Convert a canonical `a::b` package name to a relative source path."""
|
|
4392
4392
|
parts = package_name.split("::")
|
|
4393
4393
|
if not parts or any(not re.fullmatch(r"[A-Za-z_]\w*", part) for part in parts):
|
|
4394
|
-
raise ImportError(f"Invalid ChemQL package name: `{package_name}`")
|
|
4394
|
+
raise ImportError((f"Invalid ChemQL package name: `{package_name}`" if NO_ANSI else f"Invalid ChemQL package name: \033[1m{package_name}\033[22m"))
|
|
4395
4395
|
return Path(*parts[:-1], parts[-1] + ".cql")
|
|
4396
4396
|
|
|
4397
4397
|
|
|
@@ -4410,7 +4410,7 @@ def _find_cql_package_file(package_name):
|
|
|
4410
4410
|
def _ensure_package_namespace(package_name, allow_namespace_only=False):
|
|
4411
4411
|
"""Load a canonical ChemQL package and return its namespace object."""
|
|
4412
4412
|
if not re.fullmatch(r"[A-Za-z_]\w*(?:::[A-Za-z_]\w*)*", package_name):
|
|
4413
|
-
raise ImportError(f"Invalid ChemQL package name: `{package_name}`")
|
|
4413
|
+
raise ImportError((f"Invalid ChemQL package name: `{package_name}`" if NO_ANSI else f"Invalid ChemQL package name: \033[1m{package_name}\033[22m"))
|
|
4414
4414
|
cached = NAMESPACES.get(package_name)
|
|
4415
4415
|
if cached is not None and package_name in CHEMQL_PACKAGE_SOURCE_LOADED:
|
|
4416
4416
|
return cached
|
|
@@ -4419,8 +4419,11 @@ def _ensure_package_namespace(package_name, allow_namespace_only=False):
|
|
|
4419
4419
|
path = _find_cql_package_file(package_name)
|
|
4420
4420
|
if path is None and not allow_namespace_only:
|
|
4421
4421
|
raise ImportError(
|
|
4422
|
-
f"Cannot find ChemQL package `{package_name}`. Searched the current "
|
|
4423
|
-
|
|
4422
|
+
(f"Cannot find ChemQL package `{package_name}`. Searched the current "
|
|
4423
|
+
f"directory and ChemQL's bundled `stdlib` directory for .cql files."
|
|
4424
|
+
if NO_ANSI else
|
|
4425
|
+
f"Cannot find ChemQL package \033[1m{package_name}\033[22m. Searched the current "
|
|
4426
|
+
f"directory and ChemQL's bundled \033[1mstdlib\033[22m directory for .cql files.")
|
|
4424
4427
|
)
|
|
4425
4428
|
parent = _ensure_package_namespace(parent_name, allow_namespace_only=True) if separator else None
|
|
4426
4429
|
|
|
@@ -4465,7 +4468,7 @@ def _execute_load_statement(statement):
|
|
|
4465
4468
|
)
|
|
4466
4469
|
if not match:
|
|
4467
4470
|
if statement.startswith("load"):
|
|
4468
|
-
raise SyntaxError("Expected `load package::name` or `load package::name as alias`")
|
|
4471
|
+
raise SyntaxError(("Expected `load package::name` or `load package::name as alias`" if NO_ANSI else "Expected \033[1mload package::name\033[22m or \033[1mload package::name as alias\033[22m"))
|
|
4469
4472
|
return False
|
|
4470
4473
|
|
|
4471
4474
|
package_name, alias = match.groups()
|
|
@@ -4476,24 +4479,34 @@ def _execute_load_statement(statement):
|
|
|
4476
4479
|
existing = CHEMQL_PACKAGE_BINDINGS.get(alias)
|
|
4477
4480
|
if existing is not None and existing != package_name:
|
|
4478
4481
|
raise ImportError(
|
|
4479
|
-
f"Package name `{alias}` is already bound to `{existing}`; "
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
+
(f"Package name `{alias}` is already bound to `{existing}`; "
|
|
4483
|
+
f"cannot also bind it to `{package_name}`. Use a different alias, "
|
|
4484
|
+
f"for example `load {package_name} as {alias}_pkg`."
|
|
4485
|
+
if NO_ANSI else
|
|
4486
|
+
f"Package name \033[1m{alias}\033[22m is already bound to \033[1m{existing}\033[22m; "
|
|
4487
|
+
f"cannot also bind it to \033[1m{package_name}\033[22m. Use a different alias, "
|
|
4488
|
+
f"for example \033[1mload {package_name} as {alias}_pkg\033[22m.")
|
|
4482
4489
|
)
|
|
4483
4490
|
if alias in CHEMQL_VARIABLES and alias not in CHEMQL_PACKAGE_BINDINGS:
|
|
4484
4491
|
raise ImportError(
|
|
4485
|
-
f"Name `{alias}` is already defined and cannot be used as a package alias. "
|
|
4486
|
-
|
|
4492
|
+
(f"Name `{alias}` is already defined and cannot be used as a package alias. "
|
|
4493
|
+
f"Choose another alias."
|
|
4494
|
+
if NO_ANSI else
|
|
4495
|
+
f"Name \033[1m{alias}\033[22m is already defined and cannot be used as a package alias. "
|
|
4496
|
+
f"Choose another alias.")
|
|
4487
4497
|
)
|
|
4488
4498
|
elif "::" not in package_name:
|
|
4489
4499
|
existing = CHEMQL_PACKAGE_BINDINGS.get(package_name)
|
|
4490
4500
|
if existing is not None and existing != package_name:
|
|
4491
4501
|
raise ImportError(
|
|
4492
|
-
f"Name `{package_name}` is already bound to `{existing}`. "
|
|
4493
|
-
|
|
4502
|
+
(f"Name `{package_name}` is already bound to `{existing}`. "
|
|
4503
|
+
f"Use an explicit alias to load `{package_name}` under another name."
|
|
4504
|
+
if NO_ANSI else
|
|
4505
|
+
f"Name \033[1m{package_name}\033[22m is already bound to \033[1m{existing}\033[22m. "
|
|
4506
|
+
f"Use an explicit alias to load \033[1m{package_name}\033[22m under another name.")
|
|
4494
4507
|
)
|
|
4495
4508
|
if package_name in CHEMQL_VARIABLES and package_name not in CHEMQL_PACKAGE_BINDINGS:
|
|
4496
|
-
raise ImportError(f"Name `{package_name}` is already defined; cannot bind package to it.")
|
|
4509
|
+
raise ImportError((f"Name `{package_name}` is already defined; cannot bind package to it." if NO_ANSI else f"Name \033[1m{package_name}\033[22m is already defined; cannot bind package to it."))
|
|
4497
4510
|
|
|
4498
4511
|
namespace = _ensure_package_namespace(package_name)
|
|
4499
4512
|
|
|
@@ -4505,11 +4518,14 @@ def _execute_load_statement(statement):
|
|
|
4505
4518
|
root_binding = CHEMQL_PACKAGE_BINDINGS.get(root_name)
|
|
4506
4519
|
if root_binding is not None and root_binding != root_name:
|
|
4507
4520
|
raise ImportError(
|
|
4508
|
-
f"Canonical namespace `{root_name}` is already bound to `{root_binding}`; "
|
|
4509
|
-
|
|
4521
|
+
(f"Canonical namespace `{root_name}` is already bound to `{root_binding}`; "
|
|
4522
|
+
f"cannot expose `{package_name}` through its canonical path."
|
|
4523
|
+
if NO_ANSI else
|
|
4524
|
+
f"Canonical namespace \033[1m{root_name}\033[22m is already bound to \033[1m{root_binding}\033[22m; "
|
|
4525
|
+
f"cannot expose \033[1m{package_name}\033[22m through its canonical path.")
|
|
4510
4526
|
)
|
|
4511
4527
|
if root_name in CHEMQL_VARIABLES and root_name not in CHEMQL_PACKAGE_BINDINGS:
|
|
4512
|
-
raise ImportError(f"Name `{root_name}` is already defined; cannot expose `{package_name}`.")
|
|
4528
|
+
raise ImportError((f"Name `{root_name}` is already defined; cannot expose `{package_name}`." if NO_ANSI else f"Name \033[1m{root_name}\033[22m is already defined; cannot expose \033[1m{package_name}\033[22m."))
|
|
4513
4529
|
CHEMQL_PACKAGE_BINDINGS[root_name] = root_name
|
|
4514
4530
|
python_globals[root_name] = root_namespace
|
|
4515
4531
|
CHEMQL_VARIABLES.add(root_name)
|
|
@@ -4518,15 +4534,22 @@ def _execute_load_statement(statement):
|
|
|
4518
4534
|
existing = CHEMQL_PACKAGE_BINDINGS.get(alias)
|
|
4519
4535
|
if existing is not None and existing != package_name:
|
|
4520
4536
|
raise ImportError(
|
|
4521
|
-
f"Package name `{alias}` is already bound to `{existing}`; "
|
|
4522
|
-
|
|
4523
|
-
|
|
4537
|
+
(f"Package name `{alias}` is already bound to `{existing}`; "
|
|
4538
|
+
f"cannot also bind it to `{package_name}`. Use a different alias, "
|
|
4539
|
+
f"for example `load {package_name} as {alias}_pkg`."
|
|
4540
|
+
if NO_ANSI else
|
|
4541
|
+
f"Package name \033[1m{alias}\033[22m is already bound to \033[1m{existing}\033[22m; "
|
|
4542
|
+
f"cannot also bind it to \033[1m{package_name}\033[22m. Use a different alias, "
|
|
4543
|
+
f"for example \033[1mload {package_name} as {alias}_pkg\033[22m.")
|
|
4524
4544
|
)
|
|
4525
4545
|
# Avoid silently overwriting a non-package ChemQL variable either.
|
|
4526
4546
|
if alias in CHEMQL_VARIABLES and alias not in CHEMQL_PACKAGE_BINDINGS:
|
|
4527
4547
|
raise ImportError(
|
|
4528
|
-
f"Name `{alias}` is already defined and cannot be used as a package alias. "
|
|
4529
|
-
|
|
4548
|
+
(f"Name `{alias}` is already defined and cannot be used as a package alias. "
|
|
4549
|
+
f"Choose another alias."
|
|
4550
|
+
if NO_ANSI else
|
|
4551
|
+
f"Name \033[1m{alias}\033[22m is already defined and cannot be used as a package alias. "
|
|
4552
|
+
f"Choose another alias.")
|
|
4530
4553
|
)
|
|
4531
4554
|
CHEMQL_PACKAGE_BINDINGS[alias] = package_name
|
|
4532
4555
|
python_globals[alias] = namespace
|
|
@@ -4540,11 +4563,14 @@ def _execute_load_statement(statement):
|
|
|
4540
4563
|
existing = CHEMQL_PACKAGE_BINDINGS.get(package_name)
|
|
4541
4564
|
if existing is not None and existing != package_name:
|
|
4542
4565
|
raise ImportError(
|
|
4543
|
-
f"Name `{package_name}` is already bound to `{existing}`. "
|
|
4544
|
-
|
|
4566
|
+
(f"Name `{package_name}` is already bound to `{existing}`. "
|
|
4567
|
+
f"Use an explicit alias to load `{package_name}` under another name."
|
|
4568
|
+
if NO_ANSI else
|
|
4569
|
+
f"Name \033[1m{package_name}\033[22m is already bound to \033[1m{existing}\033[22m. "
|
|
4570
|
+
f"Use an explicit alias to load \033[1m{package_name}\033[22m under another name.")
|
|
4545
4571
|
)
|
|
4546
4572
|
if package_name in CHEMQL_VARIABLES and package_name not in CHEMQL_PACKAGE_BINDINGS:
|
|
4547
|
-
raise ImportError(f"Name `{package_name}` is already defined; cannot bind package to it.")
|
|
4573
|
+
raise ImportError((f"Name `{package_name}` is already defined; cannot bind package to it." if NO_ANSI else f"Name \033[1m{package_name}\033[22m is already defined; cannot bind package to it."))
|
|
4548
4574
|
CHEMQL_PACKAGE_BINDINGS[package_name] = package_name
|
|
4549
4575
|
python_globals[package_name] = namespace
|
|
4550
4576
|
CHEMQL_VARIABLES.add(package_name)
|
|
@@ -4602,7 +4628,7 @@ def process_lines(lines):
|
|
|
4602
4628
|
break
|
|
4603
4629
|
end += 1
|
|
4604
4630
|
if end >= len(lines):
|
|
4605
|
-
raise SyntaxError(f"Missing `endnamespace` for namespace `{ns_name}`")
|
|
4631
|
+
raise SyntaxError((f"Missing `endnamespace` for namespace `{ns_name}`" if NO_ANSI else f"Missing \033[1mendnamespace\033[22m for namespace \033[1m{ns_name}\033[22m"))
|
|
4606
4632
|
parent = _CURRENT_NAMESPACE
|
|
4607
4633
|
full_name = (parent._chemql_name + "::" + ns_name) if parent else ns_name # type: ignore
|
|
4608
4634
|
ns = NAMESPACES.get(full_name)
|
|
@@ -4623,7 +4649,7 @@ def process_lines(lines):
|
|
|
4623
4649
|
i = end + 1
|
|
4624
4650
|
continue
|
|
4625
4651
|
if stripped.casefold() == "endnamespace":
|
|
4626
|
-
raise SyntaxError("Unexpected `endnamespace`")
|
|
4652
|
+
raise SyntaxError(("Unexpected `endnamespace`" if NO_ANSI else "Unexpected \033[1mendnamespace\033[22m"))
|
|
4627
4653
|
visibility_match = re.match(r"^(public|private)\s+(.+)$", stripped, re.IGNORECASE)
|
|
4628
4654
|
if visibility_match:
|
|
4629
4655
|
visibility, declaration = visibility_match.groups()
|
|
@@ -4638,7 +4664,7 @@ def process_lines(lines):
|
|
|
4638
4664
|
while block_end < len(lines) and lines[block_end].strip().casefold() != "endfunc":
|
|
4639
4665
|
block_end += 1
|
|
4640
4666
|
if block_end >= len(lines):
|
|
4641
|
-
raise SyntaxError("Missing `endfunc` after visibility declaration")
|
|
4667
|
+
raise SyntaxError(("Missing `endfunc` after visibility declaration" if NO_ANSI else "Missing \033[1mendfunc\033[22m after visibility declaration"))
|
|
4642
4668
|
elif re.match(r"^namespace\s+", declaration, re.IGNORECASE):
|
|
4643
4669
|
depth = 1
|
|
4644
4670
|
block_end = i + 1
|
|
@@ -4652,7 +4678,7 @@ def process_lines(lines):
|
|
|
4652
4678
|
break
|
|
4653
4679
|
block_end += 1
|
|
4654
4680
|
if block_end >= len(lines):
|
|
4655
|
-
raise SyntaxError("Missing `endnamespace` after visibility declaration")
|
|
4681
|
+
raise SyntaxError(("Missing `endnamespace` after visibility declaration" if NO_ANSI else "Missing \033[1mendnamespace\033[22m after visibility declaration"))
|
|
4656
4682
|
elif re.match(r"^(struct|record|enum)\s+[A-Za-z_]\w*(?:\s+from\s+(?:\[[^\]]+\]|[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*))?\s*$", declaration, re.IGNORECASE):
|
|
4657
4683
|
kind = re.match(r"^(struct|record|enum)", declaration, re.IGNORECASE).group(1).casefold() # type:ignore
|
|
4658
4684
|
terminator = {"struct": "endstruct", "record": "endrecord", "enum": "endenum"}[kind]
|
|
@@ -4660,7 +4686,7 @@ def process_lines(lines):
|
|
|
4660
4686
|
while block_end < len(lines) and lines[block_end].strip().casefold() != terminator:
|
|
4661
4687
|
block_end += 1
|
|
4662
4688
|
if block_end >= len(lines):
|
|
4663
|
-
raise SyntaxError(f"Missing `{terminator}` after visibility declaration")
|
|
4689
|
+
raise SyntaxError((f"Missing `{terminator}` after visibility declaration" if NO_ANSI else f"Missing \033[1m{terminator}\033[22m after visibility declaration"))
|
|
4664
4690
|
process_lines([declaration] + lines[i + 1:block_end + 1])
|
|
4665
4691
|
finally:
|
|
4666
4692
|
_CURRENT_VISIBILITY = old_visibility
|
|
@@ -4674,7 +4700,7 @@ def process_lines(lines):
|
|
|
4674
4700
|
while end < len(lines) and lines[end].strip().casefold() != "endenum":
|
|
4675
4701
|
end += 1
|
|
4676
4702
|
if end >= len(lines):
|
|
4677
|
-
raise SyntaxError(f"Missing `endenum` for enum `{type_name}`")
|
|
4703
|
+
raise SyntaxError((f"Missing `endenum` for enum `{type_name}`" if NO_ANSI else f"Missing \033[1mendenum\033[22m for enum \033[1m{type_name}\033[22m"))
|
|
4678
4704
|
members = []
|
|
4679
4705
|
member_names = set()
|
|
4680
4706
|
for member_line in lines[i + 1:end]:
|
|
@@ -4686,12 +4712,12 @@ def process_lines(lines):
|
|
|
4686
4712
|
raise SyntaxError(f"Invalid enum member declaration: {member_line}")
|
|
4687
4713
|
member_name, value_expression = member_match.groups()
|
|
4688
4714
|
if member_name in member_names:
|
|
4689
|
-
raise SyntaxError(f"Duplicate enum member `{member_name}` in {type_name}")
|
|
4715
|
+
raise SyntaxError((f"Duplicate enum member `{member_name}` in {type_name}" if NO_ANSI else f"Duplicate enum member \033[1m{member_name}\033[22m in {type_name}"))
|
|
4690
4716
|
member_names.add(member_name)
|
|
4691
4717
|
value = auto() if value_expression is None else evaluate_python(_expand_definitions(value_expression))
|
|
4692
4718
|
members.append((member_name, value))
|
|
4693
4719
|
if not members:
|
|
4694
|
-
raise SyntaxError(f"Enum `{type_name}` must declare members")
|
|
4720
|
+
raise SyntaxError((f"Enum `{type_name}` must declare members" if NO_ANSI else f"Enum \033[1m{type_name}\033[22m must declare members"))
|
|
4695
4721
|
generated = Enum(type_name, members, module=__name__)
|
|
4696
4722
|
ENUMS[type_name] = generated
|
|
4697
4723
|
python_globals[type_name] = generated
|
|
@@ -4725,7 +4751,7 @@ def process_lines(lines):
|
|
|
4725
4751
|
if parent not in RECORDS.values():
|
|
4726
4752
|
parent = RECORDS.get(base_reference)
|
|
4727
4753
|
if parent is None:
|
|
4728
|
-
raise SyntaxError(f"Record `{type_name}` can only inherit from another record")
|
|
4754
|
+
raise SyntaxError((f"Record `{type_name}` can only inherit from another record" if NO_ANSI else f"Record \033[1m{type_name}\033[22m can only inherit from another record"))
|
|
4729
4755
|
else:
|
|
4730
4756
|
parent = (
|
|
4731
4757
|
parent
|
|
@@ -4735,9 +4761,9 @@ def process_lines(lines):
|
|
|
4735
4761
|
or getattr(builtins, base_reference, None)
|
|
4736
4762
|
)
|
|
4737
4763
|
if not isinstance(parent, type):
|
|
4738
|
-
raise SyntaxError(f"Unknown base class `{base_reference}` for struct `{type_name}`")
|
|
4764
|
+
raise SyntaxError((f"Unknown base class `{base_reference}` for struct `{type_name}`" if NO_ANSI else f"Unknown base class \033[1m{base_reference}\033[22m for struct \033[1m{type_name}\033[22m"))
|
|
4739
4765
|
if issubclass(parent, Enum):
|
|
4740
|
-
raise SyntaxError(f"Struct `{type_name}` cannot inherit from an enum")
|
|
4766
|
+
raise SyntaxError((f"Struct `{type_name}` cannot inherit from an enum" if NO_ANSI else f"Struct \033[1m{type_name}\033[22m cannot inherit from an enum"))
|
|
4741
4767
|
if hasattr(parent, "_chemql_fields"):
|
|
4742
4768
|
base_fields = list(parent._chemql_fields)
|
|
4743
4769
|
base_required = list(parent._chemql_required)
|
|
@@ -4747,7 +4773,7 @@ def process_lines(lines):
|
|
|
4747
4773
|
try:
|
|
4748
4774
|
parameters = list(inspect.signature(parent.__init__).parameters.values())[1:]
|
|
4749
4775
|
except (TypeError, ValueError) as exc:
|
|
4750
|
-
raise SyntaxError(f"Cannot inspect constructor for base class `{base_name}`") from exc
|
|
4776
|
+
raise SyntaxError((f"Cannot inspect constructor for base class `{base_name}`" if NO_ANSI else f"Cannot inspect constructor for base class \033[1m{base_name}\033[22m")) from exc
|
|
4751
4777
|
for parameter in parameters:
|
|
4752
4778
|
if parameter.kind not in (
|
|
4753
4779
|
inspect.Parameter.POSITIONAL_ONLY,
|
|
@@ -4774,7 +4800,7 @@ def process_lines(lines):
|
|
|
4774
4800
|
required_match = re.fullmatch(r"(?:required\s+([A-Za-z_]\w*)|([A-Za-z_]\w*)!)", field_line, re.IGNORECASE)
|
|
4775
4801
|
if required_match:
|
|
4776
4802
|
if kind != "struct":
|
|
4777
|
-
raise SyntaxError("`required` fields are supported only in structs")
|
|
4803
|
+
raise SyntaxError(("`required` fields are supported only in structs" if NO_ANSI else "\033[1mrequired\033[22m fields are supported only in structs"))
|
|
4778
4804
|
field = required_match.group(1) if required_match.group(1) is not None else required_match.group(2)
|
|
4779
4805
|
default = None
|
|
4780
4806
|
is_required = True
|
|
@@ -4795,7 +4821,7 @@ def process_lines(lines):
|
|
|
4795
4821
|
declared_fields = required_fields + optional_fields if kind == "struct" else optional_fields
|
|
4796
4822
|
duplicate_fields = set(base_fields) & set(declared_fields)
|
|
4797
4823
|
if duplicate_fields:
|
|
4798
|
-
raise SyntaxError(f"Field `{sorted(duplicate_fields)[0]}` is already inherited by {type_name}")
|
|
4824
|
+
raise SyntaxError((f"Field `{sorted(duplicate_fields)[0]}` is already inherited by {type_name}" if NO_ANSI else f"Field \033[1m{sorted(duplicate_fields)[0]}\033[22m is already inherited by {type_name}"))
|
|
4799
4825
|
fields = base_fields + declared_fields
|
|
4800
4826
|
if not fields and parent is None:
|
|
4801
4827
|
raise SyntaxError((f"{kind.title()} `{type_name}` must declare fields" if NO_ANSI else f"{kind.title()} \033[1m{type_name}\033[22m must declare fields"))
|
|
@@ -4818,7 +4844,7 @@ def process_lines(lines):
|
|
|
4818
4844
|
values = dict(zip(field_names, args))
|
|
4819
4845
|
overlap = values.keys() & kwargs.keys()
|
|
4820
4846
|
if overlap:
|
|
4821
|
-
raise TypeError(f"Multiple values for field `{next(iter(overlap))}`")
|
|
4847
|
+
raise TypeError((f"Multiple values for field `{next(iter(overlap))}`" if NO_ANSI else f"Multiple values for field \033[1m{next(iter(overlap))}\033[22m"))
|
|
4822
4848
|
values.update(kwargs)
|
|
4823
4849
|
extra = values.keys() - set(field_names)
|
|
4824
4850
|
if extra:
|
|
@@ -4918,7 +4944,7 @@ def process_lines(lines):
|
|
|
4918
4944
|
values = dict(zip(field_names, args))
|
|
4919
4945
|
overlap = values.keys() & kwargs.keys()
|
|
4920
4946
|
if overlap:
|
|
4921
|
-
raise TypeError(f"Multiple values for field `{next(iter(overlap))}`")
|
|
4947
|
+
raise TypeError((f"Multiple values for field `{next(iter(overlap))}`" if NO_ANSI else f"Multiple values for field \033[1m{next(iter(overlap))}\033[22m"))
|
|
4922
4948
|
values.update(kwargs)
|
|
4923
4949
|
extra = values.keys() - set(field_names)
|
|
4924
4950
|
if extra:
|
|
@@ -5225,7 +5251,7 @@ def process_lines(lines):
|
|
|
5225
5251
|
body.append(lines[i])
|
|
5226
5252
|
i += 1
|
|
5227
5253
|
if i >= len(lines):
|
|
5228
|
-
raise SyntaxError("Missing closing \033[1m```\033[22m for terminal block")
|
|
5254
|
+
raise SyntaxError(("Missing closing ``` for terminal block" if NO_ANSI else "Missing closing \033[1m```\033[22m for terminal block"))
|
|
5229
5255
|
body = [_expand_definitions(source_line) for source_line in body]
|
|
5230
5256
|
command = "\n".join(body)
|
|
5231
5257
|
result = run_shell_command(command)
|
|
@@ -4,9 +4,11 @@ import json
|
|
|
4
4
|
import argparse
|
|
5
5
|
import keyword
|
|
6
6
|
import builtins
|
|
7
|
+
import signal
|
|
7
8
|
import sys
|
|
8
9
|
import shutil
|
|
9
10
|
import subprocess
|
|
11
|
+
import time
|
|
10
12
|
|
|
11
13
|
|
|
12
14
|
def _write_system_clipboard(text):
|
|
@@ -138,6 +140,9 @@ if os.name == 'nt':
|
|
|
138
140
|
return WINDOWS_KEYS.get(ch2[0], f"arrow_{ch2.hex()}")
|
|
139
141
|
return ch.decode('utf-8', errors='ignore')
|
|
140
142
|
else:
|
|
143
|
+
import fcntl
|
|
144
|
+
import pty
|
|
145
|
+
import struct
|
|
141
146
|
import tty
|
|
142
147
|
import termios
|
|
143
148
|
import select
|
|
@@ -380,10 +385,12 @@ class Editor:
|
|
|
380
385
|
if self.handle_key(key):
|
|
381
386
|
break
|
|
382
387
|
finally:
|
|
388
|
+
if os.name != "nt":
|
|
389
|
+
sys.stdout.write("\x1b[?6l\x1b[r")
|
|
383
390
|
sys.stdout.write("\x1b[?2004l\x1b[?1049l")
|
|
384
391
|
sys.stdout.flush()
|
|
385
392
|
|
|
386
|
-
def draw_screen(self):
|
|
393
|
+
def draw_screen(self, terminal_height=0):
|
|
387
394
|
try:
|
|
388
395
|
cols, rows = os.get_terminal_size()
|
|
389
396
|
except OSError:
|
|
@@ -391,7 +398,8 @@ class Editor:
|
|
|
391
398
|
cols = max(cols, 20)
|
|
392
399
|
rows = max(rows, 4)
|
|
393
400
|
gutter_width = len(str(max(len(self.lines), self.cursor_y + 1))) + 2
|
|
394
|
-
|
|
401
|
+
terminal_height = min(max(0, terminal_height), rows - 2)
|
|
402
|
+
viewport_height = rows - terminal_height - 2
|
|
395
403
|
viewport_width = max(1, cols - gutter_width)
|
|
396
404
|
visual_cursor_x = len(self.lines[self.cursor_y][:self.cursor_x].expandtabs(4))
|
|
397
405
|
self._keep_cursor_visible(viewport_height, viewport_width, visual_cursor_x)
|
|
@@ -424,7 +432,7 @@ class Editor:
|
|
|
424
432
|
if self.search_active
|
|
425
433
|
else self.message or (
|
|
426
434
|
"Ctrl+S Save Ctrl+Z Undo Ctrl+Y Redo Ctrl+Q Quit "
|
|
427
|
-
"Ctrl+F Search Ctrl+Backspace/Delete Word"
|
|
435
|
+
"Ctrl+F Search Ctrl+T Terminal Ctrl+Backspace/Delete Word"
|
|
428
436
|
)
|
|
429
437
|
)
|
|
430
438
|
output.extend((DIM, footer[:cols].ljust(cols), RESET))
|
|
@@ -496,6 +504,10 @@ class Editor:
|
|
|
496
504
|
def handle_key(self, key):
|
|
497
505
|
if self.search_active:
|
|
498
506
|
return self._handle_search_key(key)
|
|
507
|
+
if key in ("\x14", "ctrl+t"):
|
|
508
|
+
self._run_split_terminal()
|
|
509
|
+
self.message = "Terminal closed"
|
|
510
|
+
return False
|
|
499
511
|
if key in ("\x06", "ctrl+f"):
|
|
500
512
|
self._open_search()
|
|
501
513
|
return False
|
|
@@ -592,6 +604,136 @@ class Editor:
|
|
|
592
604
|
self.cursor_x = min(self.cursor_x, len(self.lines[self.cursor_y]))
|
|
593
605
|
return False
|
|
594
606
|
|
|
607
|
+
def _run_split_terminal(self):
|
|
608
|
+
if os.name == "nt":
|
|
609
|
+
subprocess.run(os.environ.get("COMSPEC", "cmd.exe"), check=False)
|
|
610
|
+
return
|
|
611
|
+
|
|
612
|
+
stdin_fd = sys.stdin.fileno()
|
|
613
|
+
stdout_fd = sys.stdout.fileno()
|
|
614
|
+
old_settings = termios.tcgetattr(stdin_fd)
|
|
615
|
+
try:
|
|
616
|
+
cols, rows = os.get_terminal_size(stdin_fd)
|
|
617
|
+
except OSError:
|
|
618
|
+
cols, rows = 80, 24
|
|
619
|
+
rows = max(rows, 4)
|
|
620
|
+
pane_height = min(max(4, rows // 3), rows - 2)
|
|
621
|
+
pane_top = rows - pane_height + 1
|
|
622
|
+
shell_rows = rows - pane_top + 1
|
|
623
|
+
master_fd = None
|
|
624
|
+
child_pid = None
|
|
625
|
+
|
|
626
|
+
try:
|
|
627
|
+
self.message = "Terminal open: Esc or Ctrl+T returns"
|
|
628
|
+
self.draw_screen(terminal_height=pane_height)
|
|
629
|
+
for row in range(pane_top, rows + 1):
|
|
630
|
+
sys.stdout.write(f"\x1b[{row};1H\x1b[2K")
|
|
631
|
+
sys.stdout.flush()
|
|
632
|
+
|
|
633
|
+
child_pid, master_fd = pty.fork()
|
|
634
|
+
if child_pid == 0:
|
|
635
|
+
fcntl.ioctl(
|
|
636
|
+
sys.stdin.fileno(),
|
|
637
|
+
termios.TIOCSWINSZ,
|
|
638
|
+
struct.pack("HHHH", shell_rows, cols, 0, 0),
|
|
639
|
+
)
|
|
640
|
+
shell = os.environ.get("SHELL") or "bash"
|
|
641
|
+
try:
|
|
642
|
+
os.execvp(shell, [shell, "-i"])
|
|
643
|
+
except OSError as error:
|
|
644
|
+
os.write(2, f"Could not start {shell}: {error}\n".encode())
|
|
645
|
+
os._exit(127)
|
|
646
|
+
|
|
647
|
+
fcntl.ioctl(
|
|
648
|
+
master_fd,
|
|
649
|
+
termios.TIOCSWINSZ,
|
|
650
|
+
struct.pack("HHHH", shell_rows, cols, 0, 0),
|
|
651
|
+
)
|
|
652
|
+
tty.setraw(stdin_fd)
|
|
653
|
+
sys.stdout.write(f"\x1b[{pane_top};{rows}r\x1b[?6h\x1b[1;1H")
|
|
654
|
+
sys.stdout.flush()
|
|
655
|
+
|
|
656
|
+
while True:
|
|
657
|
+
try:
|
|
658
|
+
ready, _, _ = select.select([stdin_fd, master_fd], [], [])
|
|
659
|
+
except InterruptedError:
|
|
660
|
+
continue
|
|
661
|
+
|
|
662
|
+
if master_fd in ready:
|
|
663
|
+
try:
|
|
664
|
+
output = os.read(master_fd, 4096)
|
|
665
|
+
except OSError:
|
|
666
|
+
output = b""
|
|
667
|
+
if not output:
|
|
668
|
+
break
|
|
669
|
+
view = memoryview(output)
|
|
670
|
+
while view:
|
|
671
|
+
written = os.write(stdout_fd, view)
|
|
672
|
+
view = view[written:]
|
|
673
|
+
|
|
674
|
+
if stdin_fd in ready:
|
|
675
|
+
user_input = os.read(stdin_fd, 4096)
|
|
676
|
+
if not user_input:
|
|
677
|
+
break
|
|
678
|
+
exit_positions = [
|
|
679
|
+
position for marker in (b"\x14", b"\x11")
|
|
680
|
+
if (position := user_input.find(marker)) >= 0
|
|
681
|
+
]
|
|
682
|
+
escape_position = user_input.find(b"\x1b")
|
|
683
|
+
if escape_position >= 0 and user_input[escape_position:] == b"\x1b":
|
|
684
|
+
try:
|
|
685
|
+
escaped_input, _, _ = select.select(
|
|
686
|
+
[stdin_fd], [], [], 0.05
|
|
687
|
+
)
|
|
688
|
+
except InterruptedError:
|
|
689
|
+
escaped_input = []
|
|
690
|
+
if escaped_input:
|
|
691
|
+
user_input += os.read(stdin_fd, 4096)
|
|
692
|
+
else:
|
|
693
|
+
exit_positions.append(escape_position)
|
|
694
|
+
if exit_positions:
|
|
695
|
+
exit_position = min(exit_positions)
|
|
696
|
+
if exit_position:
|
|
697
|
+
os.write(master_fd, user_input[:exit_position])
|
|
698
|
+
break
|
|
699
|
+
os.write(master_fd, user_input)
|
|
700
|
+
finally:
|
|
701
|
+
tty.tcsetattr(stdin_fd, termios.TCSADRAIN, old_settings)
|
|
702
|
+
sys.stdout.write(
|
|
703
|
+
"\x1b[?6l\x1b[r\x1b[?25h\x1b[?2004h\x1b[?1l\x1b[H\x1b[2J"
|
|
704
|
+
)
|
|
705
|
+
sys.stdout.flush()
|
|
706
|
+
if master_fd is not None:
|
|
707
|
+
os.close(master_fd)
|
|
708
|
+
if child_pid is not None:
|
|
709
|
+
try:
|
|
710
|
+
waited_pid, _ = os.waitpid(child_pid, os.WNOHANG)
|
|
711
|
+
except ChildProcessError:
|
|
712
|
+
waited_pid = child_pid
|
|
713
|
+
if waited_pid != child_pid:
|
|
714
|
+
try:
|
|
715
|
+
os.kill(child_pid, signal.SIGHUP)
|
|
716
|
+
except ProcessLookupError:
|
|
717
|
+
pass
|
|
718
|
+
deadline = time.monotonic() + 1
|
|
719
|
+
while time.monotonic() < deadline:
|
|
720
|
+
try:
|
|
721
|
+
waited_pid, _ = os.waitpid(child_pid, os.WNOHANG)
|
|
722
|
+
except ChildProcessError:
|
|
723
|
+
waited_pid = child_pid
|
|
724
|
+
if waited_pid == child_pid:
|
|
725
|
+
break
|
|
726
|
+
time.sleep(0.02)
|
|
727
|
+
else:
|
|
728
|
+
try:
|
|
729
|
+
os.kill(child_pid, signal.SIGKILL)
|
|
730
|
+
except ProcessLookupError:
|
|
731
|
+
pass
|
|
732
|
+
try:
|
|
733
|
+
os.waitpid(child_pid, 0)
|
|
734
|
+
except ChildProcessError:
|
|
735
|
+
pass
|
|
736
|
+
|
|
595
737
|
def _scroll_view(self, direction):
|
|
596
738
|
try:
|
|
597
739
|
_, rows = os.get_terminal_size()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: chemql-python
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.4
|
|
4
4
|
Summary: A chemistry-focused query language and REPL
|
|
5
5
|
Author-email: Mohammad Rakibul Islam <rakibulofficial1017@gmail.com>
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -19,7 +19,7 @@ SQL-inspired syntax, inspect elements and molecules, look up reactions,
|
|
|
19
19
|
balance equations, and combine chemistry queries with Python and shell
|
|
20
20
|
commands.
|
|
21
21
|
|
|
22
|
-
- **Version:** `1.5.
|
|
22
|
+
- **Version:** `1.5.4`
|
|
23
23
|
- **PyPI distribution:** `chemql-python`
|
|
24
24
|
- **Python import / CLI name:** `chemql`
|
|
25
25
|
- **Author:** Mohammad Rakibul Islam
|
|
@@ -170,6 +170,8 @@ default. Prefix a query with `r:` for a case-sensitive regular expression,
|
|
|
170
170
|
literal text, or `li:` for case-insensitive literal text. Add `w` to match
|
|
171
171
|
whole words, for example `lwi:cat`.
|
|
172
172
|
|
|
173
|
+
Press `CTRL+T` to open a terminal inside the code editor to test your code. You can leave the terminal by pressing the Escape key
|
|
174
|
+
|
|
173
175
|
Use `Ctrl+Up` and `Ctrl+Down` to scroll the viewport while keeping the cursor
|
|
174
176
|
at the same screen position.
|
|
175
177
|
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import re
|
|
3
|
+
|
|
1
4
|
from chemql import codecql
|
|
2
5
|
|
|
3
6
|
|
|
@@ -190,3 +193,53 @@ def test_copy_paste_and_bracketed_multiline_paste(monkeypatch):
|
|
|
190
193
|
|
|
191
194
|
editor.handle_key("paste:first\r\nsecond")
|
|
192
195
|
assert editor.lines == ["one twoonfirst", "second"]
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_ctrl_t_opens_split_terminal_without_changing_editor_buffer(monkeypatch):
|
|
199
|
+
editor = codecql.Editor(None)
|
|
200
|
+
editor.lines = ["unsaved editor text"]
|
|
201
|
+
editor.cursor_x = 8
|
|
202
|
+
launched = []
|
|
203
|
+
monkeypatch.setattr(editor, "_run_split_terminal", lambda: launched.append(True))
|
|
204
|
+
|
|
205
|
+
editor.handle_key("\x14")
|
|
206
|
+
|
|
207
|
+
assert launched == [True]
|
|
208
|
+
assert editor.lines == ["unsaved editor text"]
|
|
209
|
+
assert editor.cursor_x == 8
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def test_editor_draw_does_not_scroll_or_duplicate_footer(monkeypatch):
|
|
213
|
+
editor = codecql.Editor(None)
|
|
214
|
+
editor.lines = ["first line", "second line"]
|
|
215
|
+
editor.message = ""
|
|
216
|
+
output = io.StringIO()
|
|
217
|
+
monkeypatch.setattr(codecql.os, "get_terminal_size", lambda: (120, 12))
|
|
218
|
+
monkeypatch.setattr(codecql.sys, "stdout", output)
|
|
219
|
+
|
|
220
|
+
editor.draw_screen()
|
|
221
|
+
|
|
222
|
+
rendered = output.getvalue()
|
|
223
|
+
screen = re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", rendered)
|
|
224
|
+
assert screen.startswith("1 first line")
|
|
225
|
+
assert screen.count("Ctrl+S Save") == 1
|
|
226
|
+
assert screen.count("\r\n") == 11
|
|
227
|
+
assert rendered.endswith("\x1b[1;3H")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_editor_draw_reserves_rows_above_terminal_pane(monkeypatch):
|
|
231
|
+
editor = codecql.Editor(None)
|
|
232
|
+
editor.lines = ["first line", "second line"]
|
|
233
|
+
editor.message = ""
|
|
234
|
+
output = io.StringIO()
|
|
235
|
+
monkeypatch.setattr(codecql.os, "get_terminal_size", lambda: (120, 12))
|
|
236
|
+
monkeypatch.setattr(codecql.sys, "stdout", output)
|
|
237
|
+
|
|
238
|
+
editor.draw_screen(terminal_height=4)
|
|
239
|
+
|
|
240
|
+
rendered = output.getvalue()
|
|
241
|
+
screen = re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]", "", rendered)
|
|
242
|
+
assert screen.startswith("1 first line")
|
|
243
|
+
assert "Ctrl+T Terminal" in screen
|
|
244
|
+
assert screen.count("\r\n") == 7
|
|
245
|
+
assert rendered.endswith("\x1b[1;3H")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|