Skip to content

Commit c576e39

Browse files
amotlOusret
andauthored
CI: Check if async code is up-to-date (#162)
Co-authored-by: TAHRI Ahmed R. <[email protected]>
1 parent 5a1b160 commit c576e39

File tree

3 files changed

+133
-45
lines changed

3 files changed

+133
-45
lines changed

docs/development.md

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,36 +2,35 @@
22

33
## Sandbox
44
In order to create a development sandbox, you may want to follow this list of
5-
commands. When you see the software tests succeed, you should be ready to start
6-
hacking.
7-
5+
commands.
86
```shell
97
git clone https://github.com/panodata/grafana-client
108
cd grafana-client
119
python3 -m venv .venv
1210
source .venv/bin/activate
1311
pip install --editable=.[test,develop]
12+
```
1413

15-
# Run all tests.
16-
poe test
14+
## Software Tests
15+
When you see the software tests succeed, you should be ready to start
16+
hacking.
17+
```shell
18+
# Run linters and software tests.
19+
poe check
1720

1821
# Run specific tests.
19-
python -m unittest -k preference -vvv
22+
python -m unittest -vvv -k preference
2023
```
2124

22-
### Formatting
23-
24-
Before creating a PR, you can run `poe format`, in order to resolve code style issues.
25-
26-
### Async code
27-
28-
If you update any piece of code in `grafana_client/elements/*`, please run:
29-
30-
```
31-
python script/generate_async.py
25+
## Code Formatting
26+
Before submitting a PR, please format the code, in order to invoke the async
27+
translation program and to resolve code style issues.
28+
```shell
29+
poe format
3230
```
3331

34-
Do not edit files in `grafana_client/elements/_async/*` manually.
32+
The async translation program populates the `grafana_client/elements/_async`
33+
folder automatically. Please do not edit files there manually.
3534

3635
## Run Grafana
3736
```

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,12 @@ extend-exclude = [
4040

4141
[tool.poe.tasks]
4242
format = [
43+
{cmd="script/generate_async.py format"},
4344
{cmd="black ."},
4445
{cmd="isort ."},
4546
]
4647
lint = [
48+
{cmd="script/generate_async.py check"},
4749
{cmd="ruff ."},
4850
{cmd="black --check ."},
4951
{cmd="isort --check ."},

script/generate_async.py

100644100755
Lines changed: 115 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,97 @@
1+
#!/usr/bin/env python
12
"""
2-
Script to automatically generate grafana asynchronous code from the
3+
Program to automatically generate asynchronous code from the
34
synchronous counterpart.
4-
What does this program do:
5-
- For all module in grafana_client/elements/*.py excepted base and __init__
6-
Inject async/await keyword in all available methods / client interactions.
7-
- Then detect no longer needed module for removal.
8-
- Finally generate the _async top level code based on the elements/__init__.py one.
5+
6+
Synopsis:
7+
8+
# Check if generated code is up-to-date.
9+
python script/generate_async.py check
10+
11+
# Generate code.
12+
python script/generate_async.py format
13+
14+
What does this program does:
15+
- For each module in `grafana_client/elements/*.py`, except `base` and `__init__`:
16+
Inject async/await keyword in all available methods / client interactions.
17+
- Detect modules no longer in use, and remove them.
18+
- Generate the async top level code based on `elements/__init__.py`.
919
"""
1020

1121
import os
1222
import re
23+
import shutil
1324
import subprocess
14-
from glob import glob
25+
import sys
26+
from pathlib import Path
27+
from tempfile import TemporaryDirectory
1528

16-
BASE_PATH = ".." if os.path.exists("../grafana_client") else "."
29+
BASE_PATH = Path(".." if Path("../grafana_client").exists() else ".")
30+
31+
HERE = Path.cwd().absolute()
32+
PYPROJECT_TOML = HERE / "pyproject.toml"
33+
SOURCE = BASE_PATH / "grafana_client" / "elements"
34+
TARGET = BASE_PATH / "grafana_client" / "elements" / "_async"
35+
36+
37+
def run(action: str):
38+
"""
39+
Invoke either the `check`, or the `format` subcommand.
40+
41+
:check: Create amalgamated tree in temporary directory,
42+
and compare with original. This is suitable for
43+
running sanity checks on CI.
44+
:format: Generate amalgamated async code from synchronous
45+
reference code.
46+
"""
47+
48+
run_check = action == "check" or False
49+
run_format = action == "format" or False
50+
51+
source = SOURCE
52+
target = TARGET
53+
54+
if run_check:
55+
# Create temporary formatted exemplar for probing it,
56+
# compare with current state, and croak on deviations.
57+
58+
# Use this code for `delete=False`.
59+
# TemporaryDirectory._rmtree = lambda *more, **kwargs: None
60+
61+
with TemporaryDirectory() as tmpdir:
62+
target = tmpdir
63+
process(source, target)
64+
65+
command = f"diff -x __pycache__ -u {TARGET} {target}"
66+
exitcode = os.system(command)
67+
68+
if exitcode == 0:
69+
print(msg("INFO: Async code up-to-date. Excellent."))
70+
else:
71+
print(msg("ERROR: Async code not up-to-date. Please run `poe format`."))
72+
sys.exit(2)
73+
74+
elif run_format:
75+
Path(target).mkdir(exist_ok=True)
76+
process(source, target)
77+
78+
else:
79+
raise ValueError("Wrong or missing action, use either `check` or `format`.")
80+
81+
82+
def process(source: Path, target: Path):
83+
"""
84+
Process files, from input path (source) to output path (target).
85+
"""
1786

18-
if __name__ == "__main__":
1987
module_processed = []
20-
module_generated = []
2188

22-
for module_path in glob(f"{BASE_PATH}/grafana_client/elements/*.py"):
23-
if "__init__.py" in module_path or "base.py" in module_path:
89+
print(f"Input path: {source}")
90+
print(f"Output path: {target}")
91+
92+
# for module_path in glob(f"{source}/*.py"):
93+
for module_path in Path(source).glob("*.py"):
94+
if module_path.name in ["__init__.py", "base.py"]:
2495
continue
2596

2697
print(f"Processing {module_path}...")
@@ -39,33 +110,25 @@
39110
module_dump = module_dump.replace("= self.", "= await self.")
40111

41112
module_processed.append(module_path)
42-
target_path = module_path.replace("elements/", "elements/_async/")
43-
module_generated.append(target_path)
44-
45-
print(f"Writing to {target_path}...")
46-
47-
with open(module_path.replace("elements/", "elements/_async/"), "w") as fp:
113+
target_path = Path(str(module_path).replace(str(source), str(target)))
114+
with open(target_path, "w") as fp:
48115
fp.write(module_dump)
49116

50-
relevant_modules = [os.path.basename(_) for _ in module_processed]
51-
existing_modules = [
52-
os.path.basename(_)
53-
for _ in glob(f"{BASE_PATH}/grafana_client/elements/_async/*.py")
54-
if "base.py" not in _ and "__init__.py" not in _
55-
]
117+
relevant_modules = [_.name for _ in module_processed]
118+
existing_modules = [_.name for _ in Path(target).glob("*.py") if _.name not in ["base.py", "__init__.py"]]
56119

57120
remove_module_count = 0
58121

59122
for existing_module in existing_modules:
60123
if existing_module not in relevant_modules:
61124
print(f"Removing module {existing_module}...")
62-
os.remove(f"{BASE_PATH}/grafana_client/elements/_async/{existing_module}")
125+
(Path(target) / existing_module).unlink()
63126
remove_module_count += 1
64127

65128
if not remove_module_count:
66129
print("No modules to remove.. pursuing..")
67130

68-
with open(f"{BASE_PATH}/grafana_client/elements/__init__.py", "r") as fp:
131+
with open(f"{source}/__init__.py", "r") as fp:
69132
top_level_content = fp.read()
70133

71134
print("Updating _async top level import content")
@@ -99,7 +162,31 @@
99162

100163
top_level_content_patch.append(line)
101164

102-
with open(f"{BASE_PATH}/grafana_client/elements/_async/__init__.py", "w") as fp:
165+
with open(f"{target}/__init__.py", "w") as fp:
103166
fp.write("\n".join(top_level_content_patch) + "\n")
104167

105-
subprocess.call(["poe", "format"])
168+
# Run Black and isort, providing them with the same configuration as the project.
169+
shutil.copy(PYPROJECT_TOML, f"{target}")
170+
subprocess.call(["black", target])
171+
subprocess.call(["isort", target])
172+
Path(f"{target}/pyproject.toml").unlink()
173+
174+
175+
def msg(text: str):
176+
"""
177+
Return a colorful message, the color is determined by its text.
178+
"""
179+
green = "\033[92;1m"
180+
red = "\033[91;1m"
181+
reset = "\033[0m"
182+
color = ""
183+
if text.lower().startswith("info"):
184+
color = green
185+
elif text.lower().startswith("error"):
186+
color = red
187+
return f"{color}{text}{reset}"
188+
189+
190+
if __name__ == "__main__":
191+
subcommand = sys.argv[1:] and sys.argv[1] or None
192+
run(subcommand)

0 commit comments

Comments
 (0)