|
| 1 | +#!/usr/bin/env python |
1 | 2 | """
|
2 |
| -Script to automatically generate grafana asynchronous code from the |
| 3 | +Program to automatically generate asynchronous code from the |
3 | 4 | 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`. |
9 | 19 | """
|
10 | 20 |
|
11 | 21 | import os
|
12 | 22 | import re
|
| 23 | +import shutil |
13 | 24 | import subprocess
|
14 |
| -from glob import glob |
| 25 | +import sys |
| 26 | +from pathlib import Path |
| 27 | +from tempfile import TemporaryDirectory |
15 | 28 |
|
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 | + """ |
17 | 86 |
|
18 |
| -if __name__ == "__main__": |
19 | 87 | module_processed = []
|
20 |
| - module_generated = [] |
21 | 88 |
|
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"]: |
24 | 95 | continue
|
25 | 96 |
|
26 | 97 | print(f"Processing {module_path}...")
|
|
39 | 110 | module_dump = module_dump.replace("= self.", "= await self.")
|
40 | 111 |
|
41 | 112 | 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: |
48 | 115 | fp.write(module_dump)
|
49 | 116 |
|
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"]] |
56 | 119 |
|
57 | 120 | remove_module_count = 0
|
58 | 121 |
|
59 | 122 | for existing_module in existing_modules:
|
60 | 123 | if existing_module not in relevant_modules:
|
61 | 124 | print(f"Removing module {existing_module}...")
|
62 |
| - os.remove(f"{BASE_PATH}/grafana_client/elements/_async/{existing_module}") |
| 125 | + (Path(target) / existing_module).unlink() |
63 | 126 | remove_module_count += 1
|
64 | 127 |
|
65 | 128 | if not remove_module_count:
|
66 | 129 | print("No modules to remove.. pursuing..")
|
67 | 130 |
|
68 |
| - with open(f"{BASE_PATH}/grafana_client/elements/__init__.py", "r") as fp: |
| 131 | + with open(f"{source}/__init__.py", "r") as fp: |
69 | 132 | top_level_content = fp.read()
|
70 | 133 |
|
71 | 134 | print("Updating _async top level import content")
|
|
99 | 162 |
|
100 | 163 | top_level_content_patch.append(line)
|
101 | 164 |
|
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: |
103 | 166 | fp.write("\n".join(top_level_content_patch) + "\n")
|
104 | 167 |
|
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