chemql-python 1.5.0__tar.gz → 1.5.1__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.0/src/chemql_python.egg-info → chemql_python-1.5.1}/PKG-INFO +18 -2
- chemql_python-1.5.0/PKG-INFO → chemql_python-1.5.1/README.md +17 -14
- {chemql_python-1.5.0 → chemql_python-1.5.1}/pyproject.toml +3 -1
- {chemql_python-1.5.0 → chemql_python-1.5.1}/src/chemql/__init__.py +1 -1
- chemql_python-1.5.1/src/chemql/codecql.py +477 -0
- chemql_python-1.5.1/src/chemql/colors.json +77 -0
- chemql_python-1.5.0/README.md → chemql_python-1.5.1/src/chemql_python.egg-info/PKG-INFO +30 -1
- {chemql_python-1.5.0 → chemql_python-1.5.1}/src/chemql_python.egg-info/SOURCES.txt +3 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/src/chemql_python.egg-info/entry_points.txt +1 -0
- chemql_python-1.5.1/tests/test_codecql.py +23 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/LICENSE +0 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/setup.cfg +0 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/src/chemql/__main__.py +0 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/src/chemql/data/BondEnergies.json +0 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/src/chemql/data/MoleculeBonds.json +0 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/src/chemql/data/Molecules.json +0 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/src/chemql/data/PeriodicTableJSON.json +0 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/src/chemql/data/Reactions.json +0 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/src/chemql/data/THIRD_PARTY_LICENSES/PeriodicTableJSON.txt +0 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/src/chemql/data/THIRD_PARTY_LICENSES/RHEA.txt +0 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/src/chemql/html/periodic_table.html +0 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/src/chemql/stdlib/README.md +0 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/src/chemql/stdlib/collections.cql +0 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/src/chemql/stdlib/datetime.cql +0 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/src/chemql/stdlib/decimal.cql +0 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/src/chemql/stdlib/fractions.cql +0 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/src/chemql/stdlib/json.cql +0 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/src/chemql/stdlib/math/statistics.cql +0 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/src/chemql/stdlib/math.cql +0 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/src/chemql/stdlib/pathlib.cql +0 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/src/chemql/stdlib/random.cql +0 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/src/chemql/stdlib/text.cql +0 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/src/chemql_python.egg-info/dependency_links.txt +0 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/src/chemql_python.egg-info/requires.txt +0 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/src/chemql_python.egg-info/top_level.txt +0 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/tests/test_external_molecule_lookup.py +0 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/tests/test_public_api.py +0 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/tests/test_reactions.py +0 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/tests/test_stdlib.py +0 -0
- {chemql_python-1.5.0 → chemql_python-1.5.1}/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.1
|
|
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.1`
|
|
23
23
|
- **PyPI distribution:** `chemql-python`
|
|
24
24
|
- **Python import / CLI name:** `chemql`
|
|
25
25
|
- **Author:** Mohammad Rakibul Islam
|
|
@@ -143,6 +143,18 @@ exit
|
|
|
143
143
|
- `list all` shows available sources and fields.
|
|
144
144
|
- `exit` leaves the prompt.
|
|
145
145
|
|
|
146
|
+
### Terminal editor
|
|
147
|
+
|
|
148
|
+
Install the package, then open a ChemQL file with the `codecql` command:
|
|
149
|
+
|
|
150
|
+
``` bash
|
|
151
|
+
codecql script.cql
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The editor supports syntax highlighting, direct text editing, undo (`Ctrl+Z`),
|
|
155
|
+
redo (`Ctrl+Y`), save (`Ctrl+S`), and quit (`Ctrl+Q`). Its syntax and UI
|
|
156
|
+
colors are configurable in the packaged `chemql/colors.json` file.
|
|
157
|
+
|
|
146
158
|
## Query language
|
|
147
159
|
|
|
148
160
|
A typical query starts with `search`, a source, optional conditions, and
|
|
@@ -951,6 +963,10 @@ Supported forms include `dump all|good|bad FILE`,
|
|
|
951
963
|
`dump all|good|bad queries FILE`, and `dump all|good|bad results FILE`.
|
|
952
964
|
`good` and `bad` select successful and failed executions respectively.
|
|
953
965
|
|
|
966
|
+
## CLI code editor
|
|
967
|
+
|
|
968
|
+
You can edit code in the terminal using `codecql script.cql` if your script is named `script.cql`
|
|
969
|
+
|
|
954
970
|
## Python API
|
|
955
971
|
|
|
956
972
|
``` python
|
|
@@ -1,16 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: chemql-python
|
|
3
|
-
Version: 1.5.0
|
|
4
|
-
Summary: A chemistry-focused query language and REPL
|
|
5
|
-
Author-email: Mohammad Rakibul Islam <rakibulofficial1017@gmail.com>
|
|
6
|
-
Requires-Python: >=3.10
|
|
7
|
-
Description-Content-Type: text/markdown
|
|
8
|
-
License-File: LICENSE
|
|
9
|
-
Requires-Dist: prompt_toolkit>=3.0
|
|
10
|
-
Provides-Extra: test
|
|
11
|
-
Requires-Dist: pytest>=8.0; extra == "test"
|
|
12
|
-
Dynamic: license-file
|
|
13
|
-
|
|
14
1
|
# ChemQL
|
|
15
2
|
|
|
16
3
|
**ChemQL** is a Python-powered chemistry query language and interactive
|
|
@@ -19,7 +6,7 @@ SQL-inspired syntax, inspect elements and molecules, look up reactions,
|
|
|
19
6
|
balance equations, and combine chemistry queries with Python and shell
|
|
20
7
|
commands.
|
|
21
8
|
|
|
22
|
-
- **Version:** `1.5.
|
|
9
|
+
- **Version:** `1.5.1`
|
|
23
10
|
- **PyPI distribution:** `chemql-python`
|
|
24
11
|
- **Python import / CLI name:** `chemql`
|
|
25
12
|
- **Author:** Mohammad Rakibul Islam
|
|
@@ -143,6 +130,18 @@ exit
|
|
|
143
130
|
- `list all` shows available sources and fields.
|
|
144
131
|
- `exit` leaves the prompt.
|
|
145
132
|
|
|
133
|
+
### Terminal editor
|
|
134
|
+
|
|
135
|
+
Install the package, then open a ChemQL file with the `codecql` command:
|
|
136
|
+
|
|
137
|
+
``` bash
|
|
138
|
+
codecql script.cql
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The editor supports syntax highlighting, direct text editing, undo (`Ctrl+Z`),
|
|
142
|
+
redo (`Ctrl+Y`), save (`Ctrl+S`), and quit (`Ctrl+Q`). Its syntax and UI
|
|
143
|
+
colors are configurable in the packaged `chemql/colors.json` file.
|
|
144
|
+
|
|
146
145
|
## Query language
|
|
147
146
|
|
|
148
147
|
A typical query starts with `search`, a source, optional conditions, and
|
|
@@ -951,6 +950,10 @@ Supported forms include `dump all|good|bad FILE`,
|
|
|
951
950
|
`dump all|good|bad queries FILE`, and `dump all|good|bad results FILE`.
|
|
952
951
|
`good` and `bad` select successful and failed executions respectively.
|
|
953
952
|
|
|
953
|
+
## CLI code editor
|
|
954
|
+
|
|
955
|
+
You can edit code in the terminal using `codecql script.cql` if your script is named `script.cql`
|
|
956
|
+
|
|
954
957
|
## Python API
|
|
955
958
|
|
|
956
959
|
``` python
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "chemql-python"
|
|
7
|
-
version = "1.5.
|
|
7
|
+
version = "1.5.1"
|
|
8
8
|
authors = [
|
|
9
9
|
{ name = "Mohammad Rakibul Islam", email = "rakibulofficial1017@gmail.com" }
|
|
10
10
|
]
|
|
@@ -20,6 +20,7 @@ test = ["pytest>=8.0"]
|
|
|
20
20
|
|
|
21
21
|
[project.scripts]
|
|
22
22
|
chemql = "chemql:main"
|
|
23
|
+
codecql = "chemql.codecql:main"
|
|
23
24
|
|
|
24
25
|
[tool.setuptools]
|
|
25
26
|
package-dir = {"" = "src"}
|
|
@@ -29,6 +30,7 @@ where = ["src"]
|
|
|
29
30
|
|
|
30
31
|
[tool.setuptools.package-data]
|
|
31
32
|
chemql = [
|
|
33
|
+
"colors.json",
|
|
32
34
|
"data/*.json",
|
|
33
35
|
"data/THIRD_PARTY_LICENSES/*.txt",
|
|
34
36
|
"html/*.html",
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import json
|
|
4
|
+
import argparse
|
|
5
|
+
import keyword
|
|
6
|
+
import builtins
|
|
7
|
+
import sys
|
|
8
|
+
# Cross-platform terminal setup for ANSI Processing
|
|
9
|
+
if os.name == 'nt':
|
|
10
|
+
try:
|
|
11
|
+
import colorama
|
|
12
|
+
colorama.init()
|
|
13
|
+
except ImportError:
|
|
14
|
+
import ctypes
|
|
15
|
+
kernel32 = ctypes.windll.kernel32
|
|
16
|
+
# Enable virtual terminal processing flags (ENABLE_PROCESSED_OUTPUT, ENABLE_WRAP_AT_EOL_OUTPUT, ENABLE_VIRTUAL_TERMINAL_PROCESSING)
|
|
17
|
+
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
|
18
|
+
kernel32.SetConsoleMode(kernel32.GetStdHandle(-10), 1) # Enable echo input / window input flags if needed, or minimal raw setup
|
|
19
|
+
|
|
20
|
+
# Cross-platform raw keystroke capture
|
|
21
|
+
if os.name == 'nt':
|
|
22
|
+
import msvcrt
|
|
23
|
+
def get_key():
|
|
24
|
+
ch = msvcrt.getch()
|
|
25
|
+
if ch in (b'\x00', b'\xe0'): # Arrow key prefixes on Windows
|
|
26
|
+
ch2 = msvcrt.getch()
|
|
27
|
+
if ch2 == b'\x53':
|
|
28
|
+
return "delete"
|
|
29
|
+
return f"arrow_{ch2.hex()}"
|
|
30
|
+
return ch.decode('utf-8', errors='ignore')
|
|
31
|
+
else:
|
|
32
|
+
import tty
|
|
33
|
+
import termios
|
|
34
|
+
import select
|
|
35
|
+
|
|
36
|
+
def get_key():
|
|
37
|
+
fd = sys.stdin.fileno()
|
|
38
|
+
old_settings = termios.tcgetattr(fd)
|
|
39
|
+
try:
|
|
40
|
+
tty.setraw(fd)
|
|
41
|
+
first = os.read(fd, 1)
|
|
42
|
+
if first == b"\x1b":
|
|
43
|
+
sequence = bytearray()
|
|
44
|
+
while select.select([fd], [], [], 0.05)[0]:
|
|
45
|
+
sequence.extend(os.read(fd, 1))
|
|
46
|
+
final = bytes(sequence[-1:])
|
|
47
|
+
if len(sequence) > 1 and 0x40 <= sequence[-1] <= 0x7e:
|
|
48
|
+
break
|
|
49
|
+
if sequence.startswith(b"["):
|
|
50
|
+
final = bytes(sequence[-1:]) if sequence else b""
|
|
51
|
+
if final in {b"A", b"B", b"C", b"D", b"H", b"F"}:
|
|
52
|
+
return {
|
|
53
|
+
b"A": "arrow_up", b"B": "arrow_down",
|
|
54
|
+
b"C": "arrow_right", b"D": "arrow_left",
|
|
55
|
+
b"H": "arrow_home", b"F": "arrow_end",
|
|
56
|
+
}[final]
|
|
57
|
+
if final == b"~":
|
|
58
|
+
code = bytes(sequence[1:-1]).split(b";", 1)[0]
|
|
59
|
+
if code in {b"1", b"7"}:
|
|
60
|
+
return "arrow_home"
|
|
61
|
+
if code in {b"4", b"8"}:
|
|
62
|
+
return "arrow_end"
|
|
63
|
+
if code == b"3":
|
|
64
|
+
return "delete"
|
|
65
|
+
elif sequence.startswith(b"O") and bytes(sequence[-1:]) in {b"A", b"B", b"C", b"D", b"H", b"F"}:
|
|
66
|
+
return {
|
|
67
|
+
b"A": "arrow_up", b"B": "arrow_down",
|
|
68
|
+
b"C": "arrow_right", b"D": "arrow_left",
|
|
69
|
+
b"H": "arrow_home", b"F": "arrow_end",
|
|
70
|
+
}[bytes(sequence[-1:])]
|
|
71
|
+
return "\x1b"
|
|
72
|
+
if not first:
|
|
73
|
+
return ""
|
|
74
|
+
char_length = (
|
|
75
|
+
1 if first[0] < 0x80 else
|
|
76
|
+
2 if first[0] < 0xe0 else
|
|
77
|
+
3 if first[0] < 0xf0 else 4
|
|
78
|
+
)
|
|
79
|
+
char_bytes = bytearray(first)
|
|
80
|
+
while len(char_bytes) < char_length and select.select([fd], [], [], 0.05)[0]:
|
|
81
|
+
char_bytes.extend(os.read(fd, 1))
|
|
82
|
+
return char_bytes.decode("utf-8", errors="replace")
|
|
83
|
+
finally:
|
|
84
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
|
85
|
+
|
|
86
|
+
# ANSI styling uses a muted, high-contrast palette that works in common terminals.
|
|
87
|
+
RESET = "\x1b[0m"
|
|
88
|
+
DIM = "\x1b[2m"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def load_color_styles():
|
|
92
|
+
config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "colors.json")
|
|
93
|
+
with open(config_path, encoding="utf-8") as config_file:
|
|
94
|
+
config = json.load(config_file)
|
|
95
|
+
groups = {}
|
|
96
|
+
for group_name in ("syntax", "ui"):
|
|
97
|
+
colors = config.get(group_name)
|
|
98
|
+
if not isinstance(colors, dict):
|
|
99
|
+
raise ValueError(f"Expected a '{group_name}' object in {config_path}")
|
|
100
|
+
styles = {}
|
|
101
|
+
for category, details in colors.items():
|
|
102
|
+
if not isinstance(details, dict):
|
|
103
|
+
raise ValueError(f"Color definition for '{category}' must be an object")
|
|
104
|
+
color = details.get("ansi256")
|
|
105
|
+
channel = details.get("channel", "foreground")
|
|
106
|
+
if not isinstance(color, int) or isinstance(color, bool) or not 0 <= color <= 255:
|
|
107
|
+
raise ValueError(f"Color definition for '{category}' needs an ANSI 256 color number")
|
|
108
|
+
if channel not in {"foreground", "background"}:
|
|
109
|
+
raise ValueError(f"Color definition for '{category}' has an invalid channel")
|
|
110
|
+
ansi_channel = 38 if channel == "foreground" else 48
|
|
111
|
+
styles[category] = f"\x1b[{ansi_channel};5;{color}m"
|
|
112
|
+
groups[group_name] = styles
|
|
113
|
+
return groups
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
COLOR_STYLES = load_color_styles()
|
|
117
|
+
HIGHLIGHT_STYLES = COLOR_STYLES["syntax"]
|
|
118
|
+
UI_STYLES = COLOR_STYLES["ui"]
|
|
119
|
+
MUTED = UI_STYLES["line_numbers"]
|
|
120
|
+
STATUS_BG = UI_STYLES["status_background"]
|
|
121
|
+
STATUS_FG = UI_STYLES["status_foreground"]
|
|
122
|
+
|
|
123
|
+
COMMANDS = {
|
|
124
|
+
"search", "source", "list", "clear", "return", "exit", "findel",
|
|
125
|
+
"findmol", "findre", "set", "add", "remove", "react", "balance",
|
|
126
|
+
"conditions", "view", "help", "dump", "version", "let", "unset",
|
|
127
|
+
"variables", "func", "define", "load", "import", "signal", "fix",
|
|
128
|
+
"cleanup",
|
|
129
|
+
}
|
|
130
|
+
CONTROL_KEYWORDS = {
|
|
131
|
+
"if", "elif", "else", "endif", "for", "while", "in", "endfor",
|
|
132
|
+
"endwhile", "break", "continue", "endblock", "from", "as", "public",
|
|
133
|
+
"private", "required", "struct", "record", "enum", "endstruct",
|
|
134
|
+
"endrecord", "endenum",
|
|
135
|
+
}
|
|
136
|
+
WORD_OPERATORS = {
|
|
137
|
+
"and", "or", "has", "like", "like!", "likep", "sort", "limit",
|
|
138
|
+
"count", "first", "last", "asc", "desc", "union", "intersection",
|
|
139
|
+
}
|
|
140
|
+
SOURCES = {"elements", "molecules", "reactions"}
|
|
141
|
+
CONDITIONS = {
|
|
142
|
+
"temperature", "pressure", "catalyst", "catalysts", "standard",
|
|
143
|
+
"room", "c", "f", "k", "pa", "kpa", "mpa", "hpa", "bar",
|
|
144
|
+
"atm", "torr", "mmhg", "psi",
|
|
145
|
+
}
|
|
146
|
+
PROPERTIES = set("""
|
|
147
|
+
name appearance atomic_mass boil category density discovered_by melt
|
|
148
|
+
molar_heat named_by number period group phase source bohr_model_image
|
|
149
|
+
bohr_model_3d spectral_img summary symbol xpos ypos wxpos wypos shells
|
|
150
|
+
electron_configuration electron_configuration_semantic electron_affinity
|
|
151
|
+
electronegativity_pauling ionization_energies cpk_hex cpk-hex image block
|
|
152
|
+
formula elements molecular_weight iupac_name smiles cid melting_point
|
|
153
|
+
boiling_point state bonds total_bond_energy id equation reaction_type
|
|
154
|
+
reversible reactants products conditions notes
|
|
155
|
+
""".split())
|
|
156
|
+
PYTHON_BUILTINS = set(dir(builtins))
|
|
157
|
+
TOKEN_RE = re.compile(
|
|
158
|
+
r"""(?P<comment>//.*$|#.*$)"""
|
|
159
|
+
r"""|(?P<string>'''(?:\\.|[^\\])*?'''|\"\"\"(?:\\.|[^\\])*?\"\"\"|"""
|
|
160
|
+
r"""'(?:\\.|[^'\\])*'?|"(?:\\.|[^"\\])*"?)"""
|
|
161
|
+
r"""|(?P<formula>(?<![A-Za-z0-9_])(?-i:(?:[A-Z][a-z]?\d*)+)(?![A-Za-z0-9_]))"""
|
|
162
|
+
r"""|(?P<number>(?<![\w.])[+-]?(?:\d+(?:\.\d*)?|\.\d+)"""
|
|
163
|
+
r"""\s*(?:°\s*[cfk]|nm\^-?2|n/m\^?2|pa|kpa|mpa|hpa|bar|atm|torr|mmhg|psi|[cfk])?(?!\w))"""
|
|
164
|
+
r"""|(?P<identifier>[A-Za-z_]\w*!?)"""
|
|
165
|
+
r"""|(?P<operator><->|->|!=|==|>=|<=|:=|[-+*/%=<>])"""
|
|
166
|
+
r"""|(?P<delimiter>\{\{|\}\}|\[\[\[|\]\]\]|\{|\}|\[|\]|\(|\)|,|:|\.|;|\\|\?)""",
|
|
167
|
+
re.IGNORECASE,
|
|
168
|
+
)
|
|
169
|
+
AUTO_INDENT = re.compile(
|
|
170
|
+
r"^\s*(?:(?:func|define)\b.*(?:\{\}|\{)$|"
|
|
171
|
+
r"(?:struct|record|enum)\s+\w+(?:\s+from\b.*)?$|"
|
|
172
|
+
r"(?:if|elif|else|for|while)\b.*|.*\\)$",
|
|
173
|
+
re.IGNORECASE,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _token_style(kind, value):
|
|
178
|
+
if kind == "comment":
|
|
179
|
+
return HIGHLIGHT_STYLES["comment"]
|
|
180
|
+
if kind == "string":
|
|
181
|
+
return HIGHLIGHT_STYLES["string"]
|
|
182
|
+
if kind == "number":
|
|
183
|
+
return HIGHLIGHT_STYLES["number"]
|
|
184
|
+
if kind == "formula" and (any(char.isdigit() for char in value) or len(re.findall(r"[A-Z]", value)) > 1):
|
|
185
|
+
return HIGHLIGHT_STYLES["formula"]
|
|
186
|
+
if kind == "operator":
|
|
187
|
+
return HIGHLIGHT_STYLES["operator"]
|
|
188
|
+
if kind == "delimiter":
|
|
189
|
+
return HIGHLIGHT_STYLES["command"] if value == "?" else HIGHLIGHT_STYLES["delimiter"]
|
|
190
|
+
word = value.casefold()
|
|
191
|
+
if word in {"true", "false", "none"}:
|
|
192
|
+
category = "constant"
|
|
193
|
+
elif word in {"enum", "struct", "record"}:
|
|
194
|
+
category = "type"
|
|
195
|
+
elif word in COMMANDS or word == "?":
|
|
196
|
+
category = "command"
|
|
197
|
+
elif word in CONTROL_KEYWORDS or keyword.iskeyword(word):
|
|
198
|
+
category = "keyword"
|
|
199
|
+
elif word in WORD_OPERATORS:
|
|
200
|
+
category = "operator"
|
|
201
|
+
elif word in SOURCES:
|
|
202
|
+
category = "source"
|
|
203
|
+
elif word in CONDITIONS:
|
|
204
|
+
category = "condition"
|
|
205
|
+
elif word in PROPERTIES:
|
|
206
|
+
category = "property"
|
|
207
|
+
elif word in PYTHON_BUILTINS:
|
|
208
|
+
category = "builtin"
|
|
209
|
+
else:
|
|
210
|
+
return ""
|
|
211
|
+
return HIGHLIGHT_STYLES[category]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def apply_syntax_highlighting(text):
|
|
215
|
+
parts = []
|
|
216
|
+
last_end = 0
|
|
217
|
+
for match in TOKEN_RE.finditer(text):
|
|
218
|
+
parts.append(text[last_end:match.start()])
|
|
219
|
+
kind = match.lastgroup
|
|
220
|
+
token = match.group()
|
|
221
|
+
style = _token_style(kind, token) if kind else ""
|
|
222
|
+
if kind == "identifier" and re.search(
|
|
223
|
+
r"\b(?:enum|struct|record)\s+$", text[:match.start()], re.IGNORECASE
|
|
224
|
+
):
|
|
225
|
+
style = HIGHLIGHT_STYLES["type"]
|
|
226
|
+
if style:
|
|
227
|
+
parts.extend((style, token, RESET))
|
|
228
|
+
else:
|
|
229
|
+
parts.append(token)
|
|
230
|
+
last_end = match.end()
|
|
231
|
+
parts.append(text[last_end:])
|
|
232
|
+
return "".join(parts)
|
|
233
|
+
|
|
234
|
+
class Editor:
|
|
235
|
+
HISTORY_LIMIT = 200
|
|
236
|
+
|
|
237
|
+
def __init__(self, filename):
|
|
238
|
+
self.filename = filename
|
|
239
|
+
self.lines = [""]
|
|
240
|
+
self.cursor_x = 0
|
|
241
|
+
self.cursor_y = 0
|
|
242
|
+
self.scroll_x = 0
|
|
243
|
+
self.scroll_y = 0
|
|
244
|
+
self.dirty = False
|
|
245
|
+
self.quit_requested = False
|
|
246
|
+
self.message = f"Opened {filename}" if filename else "New file"
|
|
247
|
+
self.undo_stack = []
|
|
248
|
+
self.redo_stack = []
|
|
249
|
+
|
|
250
|
+
if filename and os.path.exists(filename):
|
|
251
|
+
with open(filename, 'r', encoding='utf-8') as f:
|
|
252
|
+
content = f.read().splitlines()
|
|
253
|
+
self.lines = content if content else [""]
|
|
254
|
+
self.saved_lines = tuple(self.lines)
|
|
255
|
+
|
|
256
|
+
def run(self):
|
|
257
|
+
sys.stdout.write("\x1b[?1049h")
|
|
258
|
+
sys.stdout.flush()
|
|
259
|
+
try:
|
|
260
|
+
while True:
|
|
261
|
+
self.draw_screen()
|
|
262
|
+
key = get_key()
|
|
263
|
+
if self.handle_key(key):
|
|
264
|
+
break
|
|
265
|
+
finally:
|
|
266
|
+
sys.stdout.write("\x1b[?1049l")
|
|
267
|
+
sys.stdout.flush()
|
|
268
|
+
|
|
269
|
+
def draw_screen(self):
|
|
270
|
+
try:
|
|
271
|
+
cols, rows = os.get_terminal_size()
|
|
272
|
+
except OSError:
|
|
273
|
+
cols, rows = 80, 24
|
|
274
|
+
cols = max(cols, 20)
|
|
275
|
+
rows = max(rows, 4)
|
|
276
|
+
gutter_width = len(str(max(len(self.lines), self.cursor_y + 1))) + 2
|
|
277
|
+
viewport_height = rows - 2
|
|
278
|
+
viewport_width = max(1, cols - gutter_width)
|
|
279
|
+
visual_cursor_x = len(self.lines[self.cursor_y][:self.cursor_x].expandtabs(4))
|
|
280
|
+
self._keep_cursor_visible(viewport_height, viewport_width, visual_cursor_x)
|
|
281
|
+
|
|
282
|
+
output = ["\x1b[H"]
|
|
283
|
+
for screen_y in range(viewport_height):
|
|
284
|
+
line_y = self.scroll_y + screen_y
|
|
285
|
+
if line_y < len(self.lines):
|
|
286
|
+
line_number = f"{line_y + 1:>{gutter_width - 2}} "
|
|
287
|
+
visible_text = self.lines[line_y].expandtabs(4)[
|
|
288
|
+
self.scroll_x:self.scroll_x + viewport_width
|
|
289
|
+
]
|
|
290
|
+
output.extend((
|
|
291
|
+
DIM, MUTED, line_number, RESET,
|
|
292
|
+
apply_syntax_highlighting(visible_text), "\x1b[K\r\n",
|
|
293
|
+
))
|
|
294
|
+
else:
|
|
295
|
+
output.extend((DIM, MUTED, " " * (gutter_width - 1), "~", RESET, "\x1b[K\r\n"))
|
|
296
|
+
|
|
297
|
+
title = f" ChemQL {os.path.basename(self.filename) if self.filename else 'untitled'}"
|
|
298
|
+
status = f"Ln {self.cursor_y + 1}, Col {self.cursor_x + 1} | {len(self.lines)} lines"
|
|
299
|
+
if self.dirty:
|
|
300
|
+
status += " | Unsaved"
|
|
301
|
+
status_text = f"{title:<{max(0, cols - len(status))}}{status}"
|
|
302
|
+
output.extend((STATUS_BG, STATUS_FG, status_text[:cols].ljust(cols), RESET, "\r\n"))
|
|
303
|
+
footer = self.message or "Ctrl+S Save Ctrl+Z Undo Ctrl+Y Redo Ctrl+Q Quit"
|
|
304
|
+
output.extend((DIM, footer[:cols].ljust(cols), RESET))
|
|
305
|
+
cursor_row = self.cursor_y - self.scroll_y + 1
|
|
306
|
+
cursor_col = gutter_width + visual_cursor_x - self.scroll_x
|
|
307
|
+
output.append(f"\x1b[{cursor_row};{cursor_col}H")
|
|
308
|
+
|
|
309
|
+
sys.stdout.write("".join(output))
|
|
310
|
+
sys.stdout.flush()
|
|
311
|
+
|
|
312
|
+
def _keep_cursor_visible(self, height, width, visual_cursor_x):
|
|
313
|
+
if self.cursor_y < self.scroll_y:
|
|
314
|
+
self.scroll_y = self.cursor_y
|
|
315
|
+
elif self.cursor_y >= self.scroll_y + height:
|
|
316
|
+
self.scroll_y = self.cursor_y - height + 1
|
|
317
|
+
if visual_cursor_x < self.scroll_x:
|
|
318
|
+
self.scroll_x = visual_cursor_x
|
|
319
|
+
elif visual_cursor_x >= self.scroll_x + width:
|
|
320
|
+
self.scroll_x = visual_cursor_x - width + 1
|
|
321
|
+
|
|
322
|
+
def handle_key(self, key):
|
|
323
|
+
if key == "\x13":
|
|
324
|
+
self.save_file()
|
|
325
|
+
return False
|
|
326
|
+
if key == "\x11":
|
|
327
|
+
if self.dirty and not self.quit_requested:
|
|
328
|
+
self.message = "Unsaved changes. Press Ctrl+Q again to quit without saving."
|
|
329
|
+
self.quit_requested = True
|
|
330
|
+
return False
|
|
331
|
+
return True
|
|
332
|
+
if key == "\x1a":
|
|
333
|
+
self.undo()
|
|
334
|
+
return False
|
|
335
|
+
if key == "\x19":
|
|
336
|
+
self.redo()
|
|
337
|
+
return False
|
|
338
|
+
if key in ("arrow_up", "arrow_H", "arrow_A", "arrow_48"):
|
|
339
|
+
self.cursor_y = max(0, self.cursor_y - 1)
|
|
340
|
+
elif key in ("arrow_down", "arrow_P", "arrow_B", "arrow_50"):
|
|
341
|
+
self.cursor_y = min(len(self.lines) - 1, self.cursor_y + 1)
|
|
342
|
+
elif key in ("arrow_left", "arrow_K", "arrow_D", "arrow_4b"):
|
|
343
|
+
self.cursor_x = max(0, self.cursor_x - 1)
|
|
344
|
+
elif key in ("arrow_right", "arrow_M", "arrow_C", "arrow_4d"):
|
|
345
|
+
self.cursor_x = min(len(self.lines[self.cursor_y]), self.cursor_x + 1)
|
|
346
|
+
elif key in ("arrow_end", "arrow_4f", "arrow_F"):
|
|
347
|
+
self.cursor_x = len(self.lines[self.cursor_y])
|
|
348
|
+
elif key in ("arrow_home", "arrow_47", "arrow_G"):
|
|
349
|
+
self.cursor_x = 0
|
|
350
|
+
elif key in ("\x7f", "\x08"):
|
|
351
|
+
self._backspace()
|
|
352
|
+
elif key in ("delete", "\x1b[3~"):
|
|
353
|
+
self._delete_forward()
|
|
354
|
+
elif key in ("\r", "\n"):
|
|
355
|
+
self._insert_newline()
|
|
356
|
+
elif key == "\t":
|
|
357
|
+
self._insert_text("\t")
|
|
358
|
+
elif key and key.isprintable():
|
|
359
|
+
self._insert_text(key)
|
|
360
|
+
if key not in ("\x11",):
|
|
361
|
+
self.quit_requested = False
|
|
362
|
+
if key != "\x11":
|
|
363
|
+
self.message = ""
|
|
364
|
+
self.cursor_y = min(self.cursor_y, len(self.lines) - 1)
|
|
365
|
+
self.cursor_x = min(self.cursor_x, len(self.lines[self.cursor_y]))
|
|
366
|
+
return False
|
|
367
|
+
|
|
368
|
+
def _insert_text(self, text):
|
|
369
|
+
current_line = self.lines[self.cursor_y]
|
|
370
|
+
self._record_history()
|
|
371
|
+
self.lines[self.cursor_y] = (
|
|
372
|
+
current_line[:self.cursor_x] + text + current_line[self.cursor_x:]
|
|
373
|
+
)
|
|
374
|
+
self.cursor_x += len(text)
|
|
375
|
+
self.dirty = True
|
|
376
|
+
|
|
377
|
+
def _insert_newline(self):
|
|
378
|
+
current_line = self.lines[self.cursor_y]
|
|
379
|
+
left_side = current_line[:self.cursor_x]
|
|
380
|
+
right_side = current_line[self.cursor_x:]
|
|
381
|
+
indentation = re.match(r"^\s*", current_line).group() if re.match(r"^\s*", current_line) is not None else "" # type:ignore
|
|
382
|
+
if AUTO_INDENT.match(left_side.rstrip()):
|
|
383
|
+
indentation += "\t"
|
|
384
|
+
self._record_history()
|
|
385
|
+
self.lines[self.cursor_y] = left_side
|
|
386
|
+
self.lines.insert(self.cursor_y + 1, indentation + right_side)
|
|
387
|
+
self.cursor_y += 1
|
|
388
|
+
self.cursor_x = len(indentation)
|
|
389
|
+
self.dirty = True
|
|
390
|
+
|
|
391
|
+
def _backspace(self):
|
|
392
|
+
current_line = self.lines[self.cursor_y]
|
|
393
|
+
if self.cursor_x > 0:
|
|
394
|
+
self._record_history()
|
|
395
|
+
self.lines[self.cursor_y] = (
|
|
396
|
+
current_line[:self.cursor_x - 1] + current_line[self.cursor_x:]
|
|
397
|
+
)
|
|
398
|
+
self.cursor_x -= 1
|
|
399
|
+
self.dirty = True
|
|
400
|
+
elif self.cursor_y > 0:
|
|
401
|
+
self._record_history()
|
|
402
|
+
previous_length = len(self.lines[self.cursor_y - 1])
|
|
403
|
+
self.lines[self.cursor_y - 1] += current_line
|
|
404
|
+
self.lines.pop(self.cursor_y)
|
|
405
|
+
self.cursor_y -= 1
|
|
406
|
+
self.cursor_x = previous_length
|
|
407
|
+
self.dirty = True
|
|
408
|
+
|
|
409
|
+
def _delete_forward(self):
|
|
410
|
+
current_line = self.lines[self.cursor_y]
|
|
411
|
+
if self.cursor_x < len(current_line):
|
|
412
|
+
self._record_history()
|
|
413
|
+
self.lines[self.cursor_y] = (
|
|
414
|
+
current_line[:self.cursor_x] + current_line[self.cursor_x + 1:]
|
|
415
|
+
)
|
|
416
|
+
self.dirty = True
|
|
417
|
+
elif self.cursor_y < len(self.lines) - 1:
|
|
418
|
+
self._record_history()
|
|
419
|
+
self.lines[self.cursor_y] += self.lines.pop(self.cursor_y + 1)
|
|
420
|
+
self.dirty = True
|
|
421
|
+
|
|
422
|
+
def _current_state(self):
|
|
423
|
+
return tuple(self.lines), self.cursor_x, self.cursor_y
|
|
424
|
+
|
|
425
|
+
def _restore_state(self, state):
|
|
426
|
+
lines, self.cursor_x, self.cursor_y = state
|
|
427
|
+
self.lines = list(lines)
|
|
428
|
+
self.dirty = tuple(self.lines) != self.saved_lines
|
|
429
|
+
|
|
430
|
+
def _record_history(self):
|
|
431
|
+
self.undo_stack.append(self._current_state())
|
|
432
|
+
if len(self.undo_stack) > self.HISTORY_LIMIT:
|
|
433
|
+
self.undo_stack.pop(0)
|
|
434
|
+
self.redo_stack.clear()
|
|
435
|
+
|
|
436
|
+
def undo(self):
|
|
437
|
+
if not self.undo_stack:
|
|
438
|
+
self.message = "Nothing to undo"
|
|
439
|
+
return
|
|
440
|
+
self.redo_stack.append(self._current_state())
|
|
441
|
+
self._restore_state(self.undo_stack.pop())
|
|
442
|
+
self.message = "Undone"
|
|
443
|
+
|
|
444
|
+
def redo(self):
|
|
445
|
+
if not self.redo_stack:
|
|
446
|
+
self.message = "Nothing to redo"
|
|
447
|
+
return
|
|
448
|
+
self.undo_stack.append(self._current_state())
|
|
449
|
+
self._restore_state(self.redo_stack.pop())
|
|
450
|
+
self.message = "Redone"
|
|
451
|
+
|
|
452
|
+
def save_file(self):
|
|
453
|
+
if not self.filename:
|
|
454
|
+
self.message = "Error: No file specified. Run with an argument name!"
|
|
455
|
+
return
|
|
456
|
+
try:
|
|
457
|
+
with open(self.filename, 'w', encoding='utf-8') as f:
|
|
458
|
+
f.write("\n".join(self.lines))
|
|
459
|
+
self.saved_lines = tuple(self.lines)
|
|
460
|
+
self.dirty = False
|
|
461
|
+
self.message = f"Saved {self.filename}"
|
|
462
|
+
except Exception as e:
|
|
463
|
+
self.message = f"Failed to save file: {str(e)}"
|
|
464
|
+
|
|
465
|
+
def main(argv: list[str] | None = None) -> None:
|
|
466
|
+
"""Launch the ChemQL terminal editor for the requested file."""
|
|
467
|
+
parser = argparse.ArgumentParser(
|
|
468
|
+
prog="codecql",
|
|
469
|
+
description="Open and edit a ChemQL source file.",
|
|
470
|
+
)
|
|
471
|
+
parser.add_argument("file", help="ChemQL file to edit")
|
|
472
|
+
arguments = parser.parse_args(argv)
|
|
473
|
+
Editor(arguments.file).run()
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
if __name__ == "__main__":
|
|
477
|
+
main()
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"syntax": {
|
|
3
|
+
"comment": {
|
|
4
|
+
"ansi256": 108,
|
|
5
|
+
"description": "Line comments"
|
|
6
|
+
},
|
|
7
|
+
"string": {
|
|
8
|
+
"ansi256": 114,
|
|
9
|
+
"description": "Quoted text and string literals"
|
|
10
|
+
},
|
|
11
|
+
"number": {
|
|
12
|
+
"ansi256": 141,
|
|
13
|
+
"description": "Numeric values and measurements"
|
|
14
|
+
},
|
|
15
|
+
"command": {
|
|
16
|
+
"ansi256": 81,
|
|
17
|
+
"description": "ChemQL commands and help markers"
|
|
18
|
+
},
|
|
19
|
+
"keyword": {
|
|
20
|
+
"ansi256": 177,
|
|
21
|
+
"description": "Control flow and declaration keywords"
|
|
22
|
+
},
|
|
23
|
+
"operator": {
|
|
24
|
+
"ansi256": 215,
|
|
25
|
+
"description": "Comparison, logic, and query operators"
|
|
26
|
+
},
|
|
27
|
+
"source": {
|
|
28
|
+
"ansi256": 80,
|
|
29
|
+
"description": "Built-in data sources"
|
|
30
|
+
},
|
|
31
|
+
"property": {
|
|
32
|
+
"ansi256": 153,
|
|
33
|
+
"description": "Chemistry record property names"
|
|
34
|
+
},
|
|
35
|
+
"condition": {
|
|
36
|
+
"ansi256": 220,
|
|
37
|
+
"description": "Reaction conditions and units"
|
|
38
|
+
},
|
|
39
|
+
"constant": {
|
|
40
|
+
"ansi256": 203,
|
|
41
|
+
"description": "Boolean and null constants"
|
|
42
|
+
},
|
|
43
|
+
"type": {
|
|
44
|
+
"ansi256": 222,
|
|
45
|
+
"description": "Struct, record, enum, and declared type names"
|
|
46
|
+
},
|
|
47
|
+
"builtin": {
|
|
48
|
+
"ansi256": 117,
|
|
49
|
+
"description": "Python built-in names in embedded expressions"
|
|
50
|
+
},
|
|
51
|
+
"formula": {
|
|
52
|
+
"ansi256": 86,
|
|
53
|
+
"description": "Chemical formulas"
|
|
54
|
+
},
|
|
55
|
+
"delimiter": {
|
|
56
|
+
"ansi256": 245,
|
|
57
|
+
"description": "Brackets, separators, and punctuation"
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"ui": {
|
|
61
|
+
"line_numbers": {
|
|
62
|
+
"ansi256": 245,
|
|
63
|
+
"channel": "foreground",
|
|
64
|
+
"description": "Muted line-number gutter"
|
|
65
|
+
},
|
|
66
|
+
"status_background": {
|
|
67
|
+
"ansi256": 236,
|
|
68
|
+
"channel": "background",
|
|
69
|
+
"description": "Editor status-bar background"
|
|
70
|
+
},
|
|
71
|
+
"status_foreground": {
|
|
72
|
+
"ansi256": 252,
|
|
73
|
+
"channel": "foreground",
|
|
74
|
+
"description": "Editor status-bar text"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chemql-python
|
|
3
|
+
Version: 1.5.1
|
|
4
|
+
Summary: A chemistry-focused query language and REPL
|
|
5
|
+
Author-email: Mohammad Rakibul Islam <rakibulofficial1017@gmail.com>
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: prompt_toolkit>=3.0
|
|
10
|
+
Provides-Extra: test
|
|
11
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
12
|
+
Dynamic: license-file
|
|
13
|
+
|
|
1
14
|
# ChemQL
|
|
2
15
|
|
|
3
16
|
**ChemQL** is a Python-powered chemistry query language and interactive
|
|
@@ -6,7 +19,7 @@ SQL-inspired syntax, inspect elements and molecules, look up reactions,
|
|
|
6
19
|
balance equations, and combine chemistry queries with Python and shell
|
|
7
20
|
commands.
|
|
8
21
|
|
|
9
|
-
- **Version:** `1.5.
|
|
22
|
+
- **Version:** `1.5.1`
|
|
10
23
|
- **PyPI distribution:** `chemql-python`
|
|
11
24
|
- **Python import / CLI name:** `chemql`
|
|
12
25
|
- **Author:** Mohammad Rakibul Islam
|
|
@@ -130,6 +143,18 @@ exit
|
|
|
130
143
|
- `list all` shows available sources and fields.
|
|
131
144
|
- `exit` leaves the prompt.
|
|
132
145
|
|
|
146
|
+
### Terminal editor
|
|
147
|
+
|
|
148
|
+
Install the package, then open a ChemQL file with the `codecql` command:
|
|
149
|
+
|
|
150
|
+
``` bash
|
|
151
|
+
codecql script.cql
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
The editor supports syntax highlighting, direct text editing, undo (`Ctrl+Z`),
|
|
155
|
+
redo (`Ctrl+Y`), save (`Ctrl+S`), and quit (`Ctrl+Q`). Its syntax and UI
|
|
156
|
+
colors are configurable in the packaged `chemql/colors.json` file.
|
|
157
|
+
|
|
133
158
|
## Query language
|
|
134
159
|
|
|
135
160
|
A typical query starts with `search`, a source, optional conditions, and
|
|
@@ -938,6 +963,10 @@ Supported forms include `dump all|good|bad FILE`,
|
|
|
938
963
|
`dump all|good|bad queries FILE`, and `dump all|good|bad results FILE`.
|
|
939
964
|
`good` and `bad` select successful and failed executions respectively.
|
|
940
965
|
|
|
966
|
+
## CLI code editor
|
|
967
|
+
|
|
968
|
+
You can edit code in the terminal using `codecql script.cql` if your script is named `script.cql`
|
|
969
|
+
|
|
941
970
|
## Python API
|
|
942
971
|
|
|
943
972
|
``` python
|
|
@@ -3,6 +3,8 @@ README.md
|
|
|
3
3
|
pyproject.toml
|
|
4
4
|
src/chemql/__init__.py
|
|
5
5
|
src/chemql/__main__.py
|
|
6
|
+
src/chemql/codecql.py
|
|
7
|
+
src/chemql/colors.json
|
|
6
8
|
src/chemql/data/BondEnergies.json
|
|
7
9
|
src/chemql/data/MoleculeBonds.json
|
|
8
10
|
src/chemql/data/Molecules.json
|
|
@@ -28,6 +30,7 @@ src/chemql_python.egg-info/dependency_links.txt
|
|
|
28
30
|
src/chemql_python.egg-info/entry_points.txt
|
|
29
31
|
src/chemql_python.egg-info/requires.txt
|
|
30
32
|
src/chemql_python.egg-info/top_level.txt
|
|
33
|
+
tests/test_codecql.py
|
|
31
34
|
tests/test_external_molecule_lookup.py
|
|
32
35
|
tests/test_public_api.py
|
|
33
36
|
tests/test_reactions.py
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from chemql import codecql
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_codecql_launches_editor_for_requested_file(monkeypatch):
|
|
5
|
+
launched = {}
|
|
6
|
+
|
|
7
|
+
class FakeEditor:
|
|
8
|
+
def __init__(self, filename):
|
|
9
|
+
launched["filename"] = filename
|
|
10
|
+
|
|
11
|
+
def run(self):
|
|
12
|
+
launched["ran"] = True
|
|
13
|
+
|
|
14
|
+
monkeypatch.setattr(codecql, "Editor", FakeEditor)
|
|
15
|
+
|
|
16
|
+
codecql.main(["script.cql"])
|
|
17
|
+
|
|
18
|
+
assert launched == {"filename": "script.cql", "ran": True}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_codecql_uses_packaged_color_categories():
|
|
22
|
+
assert codecql.HIGHLIGHT_STYLES["command"]
|
|
23
|
+
assert codecql.UI_STYLES["status_background"]
|
|
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
|