chemql-python 1.6.2__py3-none-any.whl → 1.6.4__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 +192 -27
- chemql/codecql.py +140 -8
- chemql/highlighting.py +255 -0
- chemql_python-1.6.4.dist-info/METADATA +1341 -0
- {chemql_python-1.6.2.dist-info → chemql_python-1.6.4.dist-info}/RECORD +9 -8
- chemql_python-1.6.2.dist-info/METADATA +0 -1307
- {chemql_python-1.6.2.dist-info → chemql_python-1.6.4.dist-info}/WHEEL +0 -0
- {chemql_python-1.6.2.dist-info → chemql_python-1.6.4.dist-info}/entry_points.txt +0 -0
- {chemql_python-1.6.2.dist-info → chemql_python-1.6.4.dist-info}/licenses/LICENSE +0 -0
- {chemql_python-1.6.2.dist-info → chemql_python-1.6.4.dist-info}/top_level.txt +0 -0
chemql/__init__.py
CHANGED
|
@@ -68,7 +68,7 @@ __all__ = [
|
|
|
68
68
|
"__version__"
|
|
69
69
|
]
|
|
70
70
|
|
|
71
|
-
__version__ = "1.6.
|
|
71
|
+
__version__ = "1.6.4"
|
|
72
72
|
|
|
73
73
|
# Persistent preprocessor/function state for the current ChemQL session.
|
|
74
74
|
DEFINITIONS = {}
|
|
@@ -78,6 +78,7 @@ EXECUTION_HISTORY = []
|
|
|
78
78
|
_DEFINITION_NAME = r"[A-Za-z_][A-Za-z0-9_]*"
|
|
79
79
|
_NO_RETURN = object()
|
|
80
80
|
NO_ANSI = True
|
|
81
|
+
SUPPRESS_WARNINGS = False
|
|
81
82
|
|
|
82
83
|
def _expand_definitions(line):
|
|
83
84
|
"""Replace @NAME literally, including inside strings, using current definitions."""
|
|
@@ -870,7 +871,17 @@ def _call_function(name, arguments):
|
|
|
870
871
|
signal_match = re.fullmatch(r"signal\s+(.+)", line, re.IGNORECASE)
|
|
871
872
|
if signal_match:
|
|
872
873
|
raise ChemQLSignal(evaluate_python(signal_match.group(1)))
|
|
874
|
+
warn_match = re.fullmatch(r"warn\s+(.+)", line, re.IGNORECASE)
|
|
875
|
+
if warn_match:
|
|
876
|
+
warning = evaluate_python(warn_match.group(1))
|
|
877
|
+
python_globals["warnings"] = warning
|
|
873
878
|
|
|
879
|
+
if not SUPPRESS_WARNINGS:
|
|
880
|
+
message = f"Warning: {warning}"
|
|
881
|
+
print(message if NO_ANSI else f"\033[33m{message}\033[0m")
|
|
882
|
+
|
|
883
|
+
i += 1
|
|
884
|
+
continue
|
|
874
885
|
# Multiline Python block.
|
|
875
886
|
if line == "[[[":
|
|
876
887
|
block = []
|
|
@@ -1094,24 +1105,51 @@ def _expand_function_calls(text):
|
|
|
1094
1105
|
if match:
|
|
1095
1106
|
name = match.group(1)
|
|
1096
1107
|
resolved_name = _resolve_package_qualified_name(name)
|
|
1108
|
+
|
|
1109
|
+
# Resolve unqualified function calls against the active namespace.
|
|
1110
|
+
# This allows namespace functions to call themselves and other
|
|
1111
|
+
# functions in the same namespace without repeating the prefix.
|
|
1112
|
+
if "::" not in resolved_name and _CURRENT_NAMESPACE is not None:
|
|
1113
|
+
local_name = f"{_CURRENT_NAMESPACE._chemql_name}::{resolved_name}"
|
|
1114
|
+
|
|
1115
|
+
if local_name in FUNCTIONS:
|
|
1116
|
+
resolved_name = local_name
|
|
1097
1117
|
# Enforce explicit package loading BEFORE checking FUNCTIONS.
|
|
1098
1118
|
# Otherwise a function registered while internally initializing
|
|
1099
1119
|
# a package bypasses the qualified-access restriction below.
|
|
1100
1120
|
if "::" in resolved_name:
|
|
1101
1121
|
package_name = resolved_name.rsplit("::", 1)[0]
|
|
1102
1122
|
root_package = package_name.split("::", 1)[0]
|
|
1123
|
+
|
|
1103
1124
|
explicitly_loaded_packages = globals().get(
|
|
1104
1125
|
"CHEMQL_EXPLICITLY_LOADED_PACKAGES", set()
|
|
1105
1126
|
)
|
|
1127
|
+
loaded_package_sources = globals().get(
|
|
1128
|
+
"CHEMQL_PACKAGE_SOURCE_LOADED", set()
|
|
1129
|
+
)
|
|
1130
|
+
|
|
1131
|
+
is_package_namespace = (
|
|
1132
|
+
package_name in loaded_package_sources
|
|
1133
|
+
or root_package in loaded_package_sources
|
|
1134
|
+
)
|
|
1135
|
+
|
|
1136
|
+
inside_own_namespace = (
|
|
1137
|
+
_CURRENT_NAMESPACE is not None
|
|
1138
|
+
and _CURRENT_NAMESPACE._chemql_name == package_name
|
|
1139
|
+
)
|
|
1140
|
+
|
|
1106
1141
|
if (
|
|
1107
|
-
|
|
1142
|
+
is_package_namespace
|
|
1143
|
+
and not inside_own_namespace
|
|
1144
|
+
and package_name not in explicitly_loaded_packages
|
|
1108
1145
|
and root_package not in explicitly_loaded_packages
|
|
1109
1146
|
):
|
|
1110
1147
|
raise ChemQLError(
|
|
1111
1148
|
f"Unknown function `{name}`"
|
|
1112
1149
|
if NO_ANSI else
|
|
1113
|
-
f"Unknown function
|
|
1150
|
+
f"Unknown function \033[1m{name}\033[22m"
|
|
1114
1151
|
)
|
|
1152
|
+
|
|
1115
1153
|
opening = i + match.end() - 1
|
|
1116
1154
|
end = find_matching(text, opening, "{", "}")
|
|
1117
1155
|
args = _split_function_arguments(text[opening + 1:end])
|
|
@@ -1235,6 +1273,7 @@ while ($true) {
|
|
|
1235
1273
|
$commandText = [System.Text.Encoding]::UTF8.GetString(
|
|
1236
1274
|
[System.Convert]::FromBase64String($encodedCommand)
|
|
1237
1275
|
)
|
|
1276
|
+
[Console]::Out.Flush()
|
|
1238
1277
|
if ([string]::IsNullOrWhiteSpace($commandText)) {
|
|
1239
1278
|
[Console]::Out.WriteLine('ChemQL received an empty PowerShell command.')
|
|
1240
1279
|
}
|
|
@@ -1244,8 +1283,10 @@ while ($true) {
|
|
|
1244
1283
|
# output explicitly for each request; otherwise PowerShell's default
|
|
1245
1284
|
# formatting subsystem can defer table output until the next request,
|
|
1246
1285
|
# making commands such as Get-Location appear blank or duplicated.
|
|
1247
|
-
$
|
|
1248
|
-
|
|
1286
|
+
$result = Invoke-Expression -Command $commandText
|
|
1287
|
+
|
|
1288
|
+
if ($null -ne $result) {
|
|
1289
|
+
$formattedOutput = $result | Out-String -Width 120
|
|
1249
1290
|
[Console]::Out.Write($formattedOutput)
|
|
1250
1291
|
}
|
|
1251
1292
|
}
|
|
@@ -1295,10 +1336,10 @@ while ($true) {
|
|
|
1295
1336
|
def read_output():
|
|
1296
1337
|
try:
|
|
1297
1338
|
while True:
|
|
1298
|
-
|
|
1299
|
-
if not
|
|
1339
|
+
line = process.stdout.readline() # type: ignore
|
|
1340
|
+
if not line:
|
|
1300
1341
|
break
|
|
1301
|
-
output_queue.put(
|
|
1342
|
+
output_queue.put(line)
|
|
1302
1343
|
except (OSError, ValueError):
|
|
1303
1344
|
pass
|
|
1304
1345
|
finally:
|
|
@@ -1576,15 +1617,16 @@ def _run_shell_command_windows(command):
|
|
|
1576
1617
|
stdin_is_tty = False
|
|
1577
1618
|
|
|
1578
1619
|
try:
|
|
1579
|
-
|
|
1620
|
+
chunk = output_queue.get(timeout=0.04)
|
|
1580
1621
|
except queue.Empty:
|
|
1581
1622
|
if shell.poll() is not None:
|
|
1582
1623
|
return (None, collected, False)
|
|
1583
1624
|
continue
|
|
1584
|
-
|
|
1625
|
+
|
|
1626
|
+
if chunk is None:
|
|
1585
1627
|
return (None, collected, False)
|
|
1586
1628
|
|
|
1587
|
-
collected +=
|
|
1629
|
+
collected += chunk
|
|
1588
1630
|
marker_pos = collected.find(marker)
|
|
1589
1631
|
if marker_pos >= 0:
|
|
1590
1632
|
stream_until(marker_pos)
|
|
@@ -1622,7 +1664,7 @@ def _run_shell_command_windows(command):
|
|
|
1622
1664
|
except (BrokenPipeError, OSError, ValueError) as exc:
|
|
1623
1665
|
close_shell()
|
|
1624
1666
|
return f"Shell error: {exc}"
|
|
1625
|
-
|
|
1667
|
+
|
|
1626
1668
|
def run_shell_command(command):
|
|
1627
1669
|
global _shell_output_streamed
|
|
1628
1670
|
|
|
@@ -2733,7 +2775,7 @@ class Reaction:
|
|
|
2733
2775
|
for i in self.conditions:
|
|
2734
2776
|
self.equation += f"\t{i}: {self.conditions[i]}\n"
|
|
2735
2777
|
def __str__(self):
|
|
2736
|
-
return self.equation
|
|
2778
|
+
return self.equation.replace("1 ", "")
|
|
2737
2779
|
def __repr__(self):
|
|
2738
2780
|
return self.name
|
|
2739
2781
|
|
|
@@ -3176,7 +3218,7 @@ def balance_stoichiometry(reactants, products, reversible=None):
|
|
|
3176
3218
|
def _execute_balance_command(text):
|
|
3177
3219
|
text = text.split("//", 1)[0].rstrip()
|
|
3178
3220
|
match = re.fullmatch(
|
|
3179
|
-
r"\s*balance\s+(
|
|
3221
|
+
r"\s*balance\s+((?:\S+?\s*)(?:\+\s*\S+?)*)\s*(<->|->)\s*(\S+)\s*",
|
|
3180
3222
|
text,
|
|
3181
3223
|
re.IGNORECASE,
|
|
3182
3224
|
)
|
|
@@ -3637,7 +3679,13 @@ reaction_state = {
|
|
|
3637
3679
|
}
|
|
3638
3680
|
|
|
3639
3681
|
# Python variables persist for the whole session.
|
|
3640
|
-
python_globals = {
|
|
3682
|
+
python_globals = {
|
|
3683
|
+
"__builtins__": __builtins__,
|
|
3684
|
+
"math": math,
|
|
3685
|
+
"error": None,
|
|
3686
|
+
"warnings": None,
|
|
3687
|
+
"_": None,
|
|
3688
|
+
}
|
|
3641
3689
|
STRUCTS = {}
|
|
3642
3690
|
RECORDS = {}
|
|
3643
3691
|
ENUMS = {}
|
|
@@ -4014,18 +4062,23 @@ def evaluate_python(code):
|
|
|
4014
4062
|
tree = ast.parse(rewritten, mode="eval")
|
|
4015
4063
|
except SyntaxError:
|
|
4016
4064
|
module = ast.parse(rewritten, mode="exec")
|
|
4065
|
+
|
|
4017
4066
|
for node in ast.walk(module):
|
|
4018
4067
|
if isinstance(node, ast.Name) and isinstance(node.ctx, ast.Store):
|
|
4019
|
-
if
|
|
4068
|
+
if node.id.isidentifier() and not node.id.startswith("__chemql_"):
|
|
4020
4069
|
CHEMQL_VARIABLES.add(node.id)
|
|
4070
|
+
|
|
4021
4071
|
return _run_python_in_active_namespace(
|
|
4022
4072
|
compile(module, "<embedded-python>", "exec"), "exec"
|
|
4023
4073
|
)
|
|
4024
4074
|
|
|
4025
|
-
|
|
4075
|
+
result = _run_python_in_active_namespace(
|
|
4026
4076
|
compile(tree, "<embedded-python>", "eval"), "eval"
|
|
4027
4077
|
)
|
|
4028
4078
|
|
|
4079
|
+
python_globals["_"] = result
|
|
4080
|
+
return result
|
|
4081
|
+
|
|
4029
4082
|
|
|
4030
4083
|
def is_python_block_header(text):
|
|
4031
4084
|
"""Recognize [for ...], [if ...], etc. without requiring a colon."""
|
|
@@ -4423,7 +4476,7 @@ def process_inline(text):
|
|
|
4423
4476
|
return literal_value
|
|
4424
4477
|
|
|
4425
4478
|
# Native dynamically typed variables: `let name = expression`.
|
|
4426
|
-
let_match = re.fullmatch(r"let\s+([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)\s*=\s*(.+)", stripped, re.DOTALL)
|
|
4479
|
+
let_match = re.fullmatch(r"let\s+((?:[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)(?:::[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)?)\s*=\s*(.+)", stripped, re.DOTALL)
|
|
4427
4480
|
if let_match:
|
|
4428
4481
|
name, expression = let_match.groups()
|
|
4429
4482
|
value = evaluate_python(expression)
|
|
@@ -4457,7 +4510,7 @@ def process_inline(text):
|
|
|
4457
4510
|
|
|
4458
4511
|
# A bare native variable is an expression that prints its value in the REPL.
|
|
4459
4512
|
if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", stripped) and stripped in CHEMQL_VARIABLES:
|
|
4460
|
-
return
|
|
4513
|
+
return evaluate_python(stripped)
|
|
4461
4514
|
|
|
4462
4515
|
# Attribute reads on ChemQL variables (e.g. `apple.price`).
|
|
4463
4516
|
if re.fullmatch(r"[A-Za-z_]\w*(?:\.[A-Za-z_]\w*)+", stripped):
|
|
@@ -4516,10 +4569,10 @@ def process_inline(text):
|
|
|
4516
4569
|
# square brackets (for example `q+w`). Try Python expression parsing before
|
|
4517
4570
|
# the query parser whenever the line references a known ChemQL variable.
|
|
4518
4571
|
# This handles compact operators as well as spaced expressions.
|
|
4519
|
-
known_expression_names =
|
|
4572
|
+
known_expression_names = CHEMQL_VARIABLES | {"_"} | {
|
|
4520
4573
|
name for name in python_globals
|
|
4521
4574
|
if isinstance(name, str) and name.isidentifier() and not name.startswith("__")
|
|
4522
|
-
}
|
|
4575
|
+
}
|
|
4523
4576
|
references_variable = any(
|
|
4524
4577
|
re.search(r"\b" + re.escape(name) + r"\b", stripped)
|
|
4525
4578
|
for name in known_expression_names
|
|
@@ -5393,6 +5446,17 @@ def process_lines(lines):
|
|
|
5393
5446
|
signal_match = re.fullmatch(r"signal\s+(.+)", stripped, re.IGNORECASE)
|
|
5394
5447
|
if signal_match:
|
|
5395
5448
|
raise ChemQLSignal(evaluate_python(signal_match.group(1)))
|
|
5449
|
+
warn_match = re.fullmatch(r"warn\s+(.+)", stripped, re.IGNORECASE)
|
|
5450
|
+
if warn_match:
|
|
5451
|
+
warning = evaluate_python(warn_match.group(1))
|
|
5452
|
+
python_globals["warnings"] = warning
|
|
5453
|
+
|
|
5454
|
+
if not SUPPRESS_WARNINGS:
|
|
5455
|
+
message = f"Warning: {warning}"
|
|
5456
|
+
print(message if NO_ANSI else f"\033[33m{message}\033[0m")
|
|
5457
|
+
|
|
5458
|
+
i += 1
|
|
5459
|
+
continue
|
|
5396
5460
|
|
|
5397
5461
|
# Definitions are processed in source order and affect only later lines.
|
|
5398
5462
|
define_match = re.fullmatch(r"define\s+(" + _DEFINITION_NAME + r")\s+(.+)", stripped)
|
|
@@ -5697,9 +5761,9 @@ def process_lines(lines):
|
|
|
5697
5761
|
i = end + 1
|
|
5698
5762
|
continue
|
|
5699
5763
|
|
|
5700
|
-
if line == "[break]":
|
|
5764
|
+
if line == "[break]" or line == "break":
|
|
5701
5765
|
raise ChemQLBreak()
|
|
5702
|
-
if line == "[continue]":
|
|
5766
|
+
if line == "[continue]" or line == "continue" or line == "skip":
|
|
5703
5767
|
raise ChemQLContinue()
|
|
5704
5768
|
if line == "view":
|
|
5705
5769
|
view_periodic_table()
|
|
@@ -6014,11 +6078,10 @@ def execute(arguments, query_text=None, positions=None):
|
|
|
6014
6078
|
if status != 0 and not message:
|
|
6015
6079
|
message = f"Installation of {package_name} failed."
|
|
6016
6080
|
return message or f"Installed {package_name}."
|
|
6017
|
-
|
|
6018
6081
|
if arguments[0].lower() == "balance":
|
|
6019
6082
|
try:
|
|
6020
6083
|
return _execute_balance_command(
|
|
6021
|
-
query_text if query_text is not None else shlex.join(arguments)
|
|
6084
|
+
query_text.replace("< - >", "<->").replace("- >", "->").replace("<= >", "<->").replace("= >", "->") if query_text is not None else shlex.join(arguments).replace("'<' - '>'", "<->").replace("- '>'", "->").replace("'<=' '>'", "<->").replace("= '>'", "->")
|
|
6022
6085
|
)
|
|
6023
6086
|
except ValueError as exc:
|
|
6024
6087
|
return format_error(str(exc), query_text)
|
|
@@ -6516,7 +6579,8 @@ HELP = (
|
|
|
6516
6579
|
" \x1b[95mrisk ... endrisk \x1b[0m \x1b[90m│\x1b[0m Run a protected block\n"
|
|
6517
6580
|
" \x1b[95mfix \x1b[0m \x1b[90m│\x1b[0m Handle an error from the risk block\n"
|
|
6518
6581
|
" \x1b[95mcleanup \x1b[0m \x1b[90m│\x1b[0m Run cleanup whether successful or not\n"
|
|
6519
|
-
" \x1b[95msignal \"message\" \x1b[0m \x1b[90m│\x1b[0m Raise a ChemQL signal\n"
|
|
6582
|
+
" \x1b[95msignal \"message\" \x1b[0m \x1b[90m│\x1b[0m Raise a ChemQL signal, latest signal is stored in error\n"
|
|
6583
|
+
" \x1b[95mwarn \"message\" \x1b[0m \x1b[90m│\x1b[0m Raise a warning, latest warning is stored in warnings\n"
|
|
6520
6584
|
" \x1b[95merror \x1b[0m \x1b[90m│\x1b[0m Inspect the error type in a fix block\n"
|
|
6521
6585
|
" \x1b[95mrisk\x1b[0m\n"
|
|
6522
6586
|
" \x1b[95msignal \"Something went wrong\"\x1b[0m\n"
|
|
@@ -7269,6 +7333,8 @@ def _download_package(name, version=None, force_replace=False):
|
|
|
7269
7333
|
release_info = _api_object(_http_json(f"{endpoint}/latest"))
|
|
7270
7334
|
else:
|
|
7271
7335
|
release_info = _api_object(_http_json(f"{endpoint}/{urllib.parse.quote(version, safe='')}"))
|
|
7336
|
+
if release_info.get("safe") != 1:
|
|
7337
|
+
raise RuntimeError("Package was too unsafe to download")
|
|
7272
7338
|
actual_version = str(release_info.get("version") or release_info.get("version_number") or version or "")
|
|
7273
7339
|
if not actual_version or not re.fullmatch(r"[A-Za-z0-9.+_-]+", actual_version):
|
|
7274
7340
|
raise RuntimeError("Registry response did not include a valid package version.")
|
|
@@ -7357,6 +7423,8 @@ def _package_command(args):
|
|
|
7357
7423
|
return _download_package(args.name, args.version)
|
|
7358
7424
|
if args.subcommand == "upgrade":
|
|
7359
7425
|
return _download_package(args.name, args.version, force_replace=True)
|
|
7426
|
+
if args.subcommand == "upload":
|
|
7427
|
+
return _upload_project()
|
|
7360
7428
|
if args.subcommand == "search":
|
|
7361
7429
|
data = _http_json(f"{_registry_url()}/api/v1/search?q={urllib.parse.quote(args.query)}")
|
|
7362
7430
|
results = data.get("results", data.get("packages", [])) if isinstance(data, dict) else data
|
|
@@ -7403,6 +7471,95 @@ def _package_command(args):
|
|
|
7403
7471
|
return 1
|
|
7404
7472
|
return 2
|
|
7405
7473
|
|
|
7474
|
+
def _upload_project(directory="."):
|
|
7475
|
+
root = Path(directory).expanduser().resolve()
|
|
7476
|
+
manifest_path = root / "cqlpackage.json"
|
|
7477
|
+
|
|
7478
|
+
if not manifest_path.is_file():
|
|
7479
|
+
raise RuntimeError(
|
|
7480
|
+
f"No cqlpackage.json found in {root}"
|
|
7481
|
+
)
|
|
7482
|
+
|
|
7483
|
+
with manifest_path.open("r", encoding="utf-8") as stream:
|
|
7484
|
+
package = json.load(stream)
|
|
7485
|
+
|
|
7486
|
+
if not isinstance(package, dict):
|
|
7487
|
+
raise RuntimeError("Invalid package manifest.")
|
|
7488
|
+
|
|
7489
|
+
name = package.get("name")
|
|
7490
|
+
version = str(package.get("version", "")).strip()
|
|
7491
|
+
|
|
7492
|
+
if not isinstance(name, str) or not _safe_package_name(name):
|
|
7493
|
+
raise RuntimeError("Invalid package name.")
|
|
7494
|
+
|
|
7495
|
+
if not version or not re.fullmatch(r"[A-Za-z0-9.+_-]+", version):
|
|
7496
|
+
raise RuntimeError("Invalid package version.")
|
|
7497
|
+
|
|
7498
|
+
archive_path = root / "dist" / f"{name}-{version}.tar.gz"
|
|
7499
|
+
|
|
7500
|
+
if not archive_path.is_file():
|
|
7501
|
+
raise RuntimeError(
|
|
7502
|
+
f"Package archive not found: {archive_path}\n"
|
|
7503
|
+
"Run `chemql build` first."
|
|
7504
|
+
)
|
|
7505
|
+
|
|
7506
|
+
payload = archive_path.read_bytes()
|
|
7507
|
+
|
|
7508
|
+
if len(payload) > 10 * 1024 * 1024:
|
|
7509
|
+
raise RuntimeError(
|
|
7510
|
+
"Package archive exceeds the registry's 10 MB upload limit."
|
|
7511
|
+
)
|
|
7512
|
+
|
|
7513
|
+
boundary = "----ChemQL" + os.urandom(16).hex()
|
|
7514
|
+
|
|
7515
|
+
body = (
|
|
7516
|
+
f"--{boundary}\r\n"
|
|
7517
|
+
f'Content-Disposition: form-data; name="file"; '
|
|
7518
|
+
f'filename="{archive_path.name}"\r\n'
|
|
7519
|
+
f"Content-Type: application/gzip\r\n\r\n"
|
|
7520
|
+
).encode("utf-8")
|
|
7521
|
+
|
|
7522
|
+
body += payload
|
|
7523
|
+
body += f"\r\n--{boundary}--\r\n".encode("utf-8")
|
|
7524
|
+
|
|
7525
|
+
endpoint = f"{_registry_url()}/api/v1/upload"
|
|
7526
|
+
|
|
7527
|
+
request = urllib.request.Request(
|
|
7528
|
+
endpoint,
|
|
7529
|
+
data=body,
|
|
7530
|
+
headers={
|
|
7531
|
+
"Content-Type": f"multipart/form-data; boundary={boundary}",
|
|
7532
|
+
"Accept": "application/json",
|
|
7533
|
+
"User-Agent": f"ChemQL/{__version__}",
|
|
7534
|
+
},
|
|
7535
|
+
method="POST",
|
|
7536
|
+
)
|
|
7537
|
+
|
|
7538
|
+
try:
|
|
7539
|
+
with urllib.request.urlopen(request, timeout=60) as response:
|
|
7540
|
+
result = json.loads(response.read(1_000_001))
|
|
7541
|
+
except urllib.error.HTTPError as exc:
|
|
7542
|
+
try:
|
|
7543
|
+
result = json.loads(exc.read(1_000_001))
|
|
7544
|
+
message = result.get("error", exc.reason)
|
|
7545
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
7546
|
+
message = exc.reason
|
|
7547
|
+
|
|
7548
|
+
raise RuntimeError(
|
|
7549
|
+
f"Upload failed (HTTP {exc.code}): {message}"
|
|
7550
|
+
) from exc
|
|
7551
|
+
except urllib.error.URLError as exc:
|
|
7552
|
+
raise RuntimeError(
|
|
7553
|
+
f"Could not connect to ChemQL registry: {exc.reason}"
|
|
7554
|
+
) from exc
|
|
7555
|
+
except (json.JSONDecodeError, UnicodeDecodeError) as exc:
|
|
7556
|
+
raise RuntimeError(
|
|
7557
|
+
"Registry returned an invalid response."
|
|
7558
|
+
) from exc
|
|
7559
|
+
|
|
7560
|
+
print(f"Published {name} v{version}!")
|
|
7561
|
+
print(result.get("message", "Upload successful."))
|
|
7562
|
+
return 0
|
|
7406
7563
|
|
|
7407
7564
|
def _build_project(directory="."):
|
|
7408
7565
|
"""Build a ChemQL package without requiring a package-local build.py."""
|
|
@@ -7539,6 +7696,7 @@ def main(ANSI: bool = True):
|
|
|
7539
7696
|
subparsers = parser.add_subparsers(dest="subcommand")
|
|
7540
7697
|
|
|
7541
7698
|
p_install = subparsers.add_parser("install", help="Install a package from the ChemQL registry")
|
|
7699
|
+
subparsers.add_parser("upload", help="Upload the current ChemQL package to the registry")
|
|
7542
7700
|
p_install.add_argument("name")
|
|
7543
7701
|
p_install.add_argument("--version", default=None)
|
|
7544
7702
|
|
|
@@ -7580,6 +7738,11 @@ def main(ANSI: bool = True):
|
|
|
7580
7738
|
action="store_true",
|
|
7581
7739
|
help="Strip ANSI escape sequences from output.",
|
|
7582
7740
|
)
|
|
7741
|
+
parser.add_argument(
|
|
7742
|
+
"--suppress-warnings",
|
|
7743
|
+
action="store_true",
|
|
7744
|
+
help="Suppress warning messages.",
|
|
7745
|
+
)
|
|
7583
7746
|
|
|
7584
7747
|
parser.add_argument(
|
|
7585
7748
|
"-o", "--output",
|
|
@@ -7625,8 +7788,10 @@ def main(ANSI: bool = True):
|
|
|
7625
7788
|
)
|
|
7626
7789
|
sys.exit(upgrade_package(version_target, args.force))
|
|
7627
7790
|
|
|
7628
|
-
global NO_ANSI
|
|
7791
|
+
global NO_ANSI, SUPPRESS_WARNINGS
|
|
7792
|
+
|
|
7629
7793
|
NO_ANSI = not ANSI or args.no_ansi or bool(args.output)
|
|
7794
|
+
SUPPRESS_WARNINGS = args.suppress_warnings
|
|
7630
7795
|
|
|
7631
7796
|
if args.output and args.command is None and args.file is None:
|
|
7632
7797
|
parser.error("-o/--output requires -c/--command or -f/--file")
|
chemql/codecql.py
CHANGED
|
@@ -9,8 +9,9 @@ import sys
|
|
|
9
9
|
import shutil
|
|
10
10
|
import subprocess
|
|
11
11
|
import time
|
|
12
|
+
from .highlighting import HighlightProfiles
|
|
12
13
|
|
|
13
|
-
VERSION = "0.0.
|
|
14
|
+
VERSION = "0.0.11"
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
def _write_system_clipboard(text):
|
|
@@ -320,7 +321,7 @@ AUTO_INDENT = re.compile(
|
|
|
320
321
|
)
|
|
321
322
|
|
|
322
323
|
|
|
323
|
-
def _token_style(kind, value):
|
|
324
|
+
def _token_style(kind, value, profiles=None, filename=None):
|
|
324
325
|
if kind == "comment":
|
|
325
326
|
return HIGHLIGHT_STYLES["comment"]
|
|
326
327
|
if kind == "string":
|
|
@@ -334,6 +335,11 @@ def _token_style(kind, value):
|
|
|
334
335
|
if kind == "delimiter":
|
|
335
336
|
return HIGHLIGHT_STYLES["command"] if value == "?" else HIGHLIGHT_STYLES["delimiter"]
|
|
336
337
|
word = value.casefold()
|
|
338
|
+
if profiles and filename:
|
|
339
|
+
configured_style = profiles.get_keyword_style(filename, word)
|
|
340
|
+
|
|
341
|
+
if configured_style:
|
|
342
|
+
return parse_highlight_style(configured_style)
|
|
337
343
|
if word in {"true", "false", "none"}:
|
|
338
344
|
category = "constant"
|
|
339
345
|
elif word in {"enum", "struct", "record"}:
|
|
@@ -367,6 +373,34 @@ MANIFEST_TOKEN_RE = re.compile(
|
|
|
367
373
|
r"|(?P<delimiter>[\[\]{}:,.])",
|
|
368
374
|
re.IGNORECASE,
|
|
369
375
|
)
|
|
376
|
+
def load_highlighting_profiles():
|
|
377
|
+
profiles = HighlightProfiles()
|
|
378
|
+
|
|
379
|
+
main_config = os.path.join(
|
|
380
|
+
os.path.dirname(os.path.abspath(__file__)),
|
|
381
|
+
"highlighting.json",
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
local_config = os.path.join(
|
|
385
|
+
os.getcwd(),
|
|
386
|
+
"highlighting.json",
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
loaded_paths = set()
|
|
390
|
+
|
|
391
|
+
for path in (main_config, local_config):
|
|
392
|
+
path = os.path.abspath(path)
|
|
393
|
+
|
|
394
|
+
# Avoid loading the same file twice.
|
|
395
|
+
if path in loaded_paths:
|
|
396
|
+
continue
|
|
397
|
+
|
|
398
|
+
loaded_paths.add(path)
|
|
399
|
+
|
|
400
|
+
if os.path.isfile(path):
|
|
401
|
+
profiles.load(path)
|
|
402
|
+
|
|
403
|
+
return profiles
|
|
370
404
|
|
|
371
405
|
def _apply_manifest_syntax_highlighting(text, filename):
|
|
372
406
|
"""Highlight JSON/TOML package manifests with the editor's existing theme."""
|
|
@@ -421,8 +455,68 @@ def _apply_manifest_syntax_highlighting(text, filename):
|
|
|
421
455
|
parts.append(text[last_end:])
|
|
422
456
|
return "".join(parts)
|
|
423
457
|
|
|
458
|
+
def parse_highlight_style(style):
|
|
459
|
+
parts = style.split()
|
|
460
|
+
codes = []
|
|
461
|
+
|
|
462
|
+
named_styles = {
|
|
463
|
+
"keyword": HIGHLIGHT_STYLES["keyword"],
|
|
464
|
+
"command": HIGHLIGHT_STYLES["command"],
|
|
465
|
+
"operator": HIGHLIGHT_STYLES["operator"],
|
|
466
|
+
"string": HIGHLIGHT_STYLES["string"],
|
|
467
|
+
"comment": HIGHLIGHT_STYLES["comment"],
|
|
468
|
+
"number": HIGHLIGHT_STYLES["number"],
|
|
469
|
+
"constant": HIGHLIGHT_STYLES["constant"],
|
|
470
|
+
"type": HIGHLIGHT_STYLES["type"],
|
|
471
|
+
"source": HIGHLIGHT_STYLES["source"],
|
|
472
|
+
"condition": HIGHLIGHT_STYLES["condition"],
|
|
473
|
+
"property": HIGHLIGHT_STYLES["property"],
|
|
474
|
+
"builtin": HIGHLIGHT_STYLES["builtin"],
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
formatting = {
|
|
478
|
+
"bold": "1",
|
|
479
|
+
"dim": "2",
|
|
480
|
+
"italic": "3",
|
|
481
|
+
"underline": "4",
|
|
482
|
+
"reverse": "7",
|
|
483
|
+
"strike": "9",
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
for part in parts:
|
|
487
|
+
part = part.casefold()
|
|
424
488
|
|
|
425
|
-
|
|
489
|
+
if part in named_styles:
|
|
490
|
+
# Existing theme styles are already ANSI escape sequences.
|
|
491
|
+
codes.append(named_styles[part])
|
|
492
|
+
|
|
493
|
+
elif part in formatting:
|
|
494
|
+
codes.append(f"\033[{formatting[part]}m")
|
|
495
|
+
|
|
496
|
+
elif re.fullmatch(r"#[0-9a-f]{6}", part):
|
|
497
|
+
r, g, b = (
|
|
498
|
+
int(part[i:i + 2], 16)
|
|
499
|
+
for i in (1, 3, 5)
|
|
500
|
+
)
|
|
501
|
+
codes.append(f"\033[38;2;{r};{g};{b}m")
|
|
502
|
+
|
|
503
|
+
elif part.startswith("bg:#") and re.fullmatch(
|
|
504
|
+
r"bg:#[0-9a-f]{6}", part
|
|
505
|
+
):
|
|
506
|
+
color = part[3:]
|
|
507
|
+
r, g, b = (
|
|
508
|
+
int(color[i:i + 2], 16)
|
|
509
|
+
for i in (1, 3, 5)
|
|
510
|
+
)
|
|
511
|
+
codes.append(f"\033[48;2;{r};{g};{b}m")
|
|
512
|
+
|
|
513
|
+
else:
|
|
514
|
+
raise ValueError(f"Unknown highlight style component: {part}")
|
|
515
|
+
|
|
516
|
+
return "".join(codes)
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def apply_syntax_highlighting(text, filename=None, profiles=None):
|
|
426
520
|
if filename:
|
|
427
521
|
manifest_highlighting = _apply_manifest_syntax_highlighting(text, filename)
|
|
428
522
|
if manifest_highlighting is not None:
|
|
@@ -433,7 +527,7 @@ def apply_syntax_highlighting(text, filename=None):
|
|
|
433
527
|
parts.append(text[last_end:match.start()])
|
|
434
528
|
kind = match.lastgroup
|
|
435
529
|
token = match.group()
|
|
436
|
-
style = _token_style(kind, token) if kind else ""
|
|
530
|
+
style = _token_style(kind, token, profiles, filename) if kind else ""
|
|
437
531
|
if kind == "identifier" and re.search(
|
|
438
532
|
r"\b(?:enum|struct|record|namespace)\s+$", text[:match.start()], re.IGNORECASE
|
|
439
533
|
):
|
|
@@ -475,6 +569,7 @@ class Editor:
|
|
|
475
569
|
content = f.read().splitlines()
|
|
476
570
|
self.lines = content if content else [""]
|
|
477
571
|
self.saved_lines = tuple(self.lines)
|
|
572
|
+
self.highlight_profiles = load_highlighting_profiles()
|
|
478
573
|
|
|
479
574
|
def run(self):
|
|
480
575
|
sys.stdout.write("\x1b[?1049h\x1b[?2004h")
|
|
@@ -564,7 +659,7 @@ class Editor:
|
|
|
564
659
|
current = (match_start, match_end) == self.current_search_match
|
|
565
660
|
overlays.append((start, end, "\x1b[7m" if current else "\x1b[43m", 1))
|
|
566
661
|
if not overlays:
|
|
567
|
-
return apply_syntax_highlighting(visible_text, self.filename)
|
|
662
|
+
return apply_syntax_highlighting(visible_text, self.filename, self.highlight_profiles)
|
|
568
663
|
|
|
569
664
|
boundaries = {line_start, line_end}
|
|
570
665
|
for start, end, _, _ in overlays:
|
|
@@ -589,7 +684,11 @@ class Editor:
|
|
|
589
684
|
style = max(active, key=lambda overlay: overlay[3])[2]
|
|
590
685
|
parts.extend((style, segment, RESET))
|
|
591
686
|
else:
|
|
592
|
-
parts.append(
|
|
687
|
+
parts.append(
|
|
688
|
+
apply_syntax_highlighting(
|
|
689
|
+
segment, self.filename, self.highlight_profiles
|
|
690
|
+
)
|
|
691
|
+
)
|
|
593
692
|
return "".join(parts)
|
|
594
693
|
|
|
595
694
|
def _keep_cursor_visible(self, height, width, visual_cursor_x):
|
|
@@ -1317,10 +1416,43 @@ def main(argv: list[str] | None = None) -> None:
|
|
|
1317
1416
|
)
|
|
1318
1417
|
parser.add_argument("-V", "--version", action="version", version=f"codecql {VERSION}")
|
|
1319
1418
|
parser.add_argument("file", nargs="?", help="ChemQL file to edit")
|
|
1419
|
+
parser.add_argument(
|
|
1420
|
+
"--main-highlighting",
|
|
1421
|
+
action="store_true",
|
|
1422
|
+
help="Open CodecQL's main highlighting configuration",
|
|
1423
|
+
)
|
|
1424
|
+
|
|
1425
|
+
parser.add_argument(
|
|
1426
|
+
"--local-highlighting",
|
|
1427
|
+
action="store_true",
|
|
1428
|
+
help="Open the current directory's highlighting configuration",
|
|
1429
|
+
)
|
|
1320
1430
|
arguments = parser.parse_args(argv)
|
|
1321
|
-
if
|
|
1431
|
+
if arguments.main_highlighting and arguments.local_highlighting:
|
|
1432
|
+
parser.error("Choose only one highlighting configuration")
|
|
1433
|
+
|
|
1434
|
+
if arguments.main_highlighting:
|
|
1435
|
+
filename = os.path.join(
|
|
1436
|
+
os.path.dirname(os.path.abspath(__file__)),
|
|
1437
|
+
"highlighting.json",
|
|
1438
|
+
)
|
|
1439
|
+
|
|
1440
|
+
elif arguments.local_highlighting:
|
|
1441
|
+
filename = os.path.join(os.getcwd(), "highlighting.json")
|
|
1442
|
+
|
|
1443
|
+
else:
|
|
1444
|
+
filename = arguments.file
|
|
1445
|
+
|
|
1446
|
+
if not filename:
|
|
1322
1447
|
parser.error("the following arguments are required: file")
|
|
1323
|
-
|
|
1448
|
+
|
|
1449
|
+
if arguments.main_highlighting or arguments.local_highlighting:
|
|
1450
|
+
if not os.path.exists(filename):
|
|
1451
|
+
with open(filename, "w", encoding="utf-8") as f:
|
|
1452
|
+
json.dump({}, f, indent=4)
|
|
1453
|
+
f.write("\n")
|
|
1454
|
+
|
|
1455
|
+
Editor(filename).run()
|
|
1324
1456
|
|
|
1325
1457
|
if __name__ == "__main__":
|
|
1326
1458
|
main()
|