proj-flow 0.19.0__py3-none-any.whl → 0.20.1__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.
proj_flow/__init__.py CHANGED
@@ -6,4 +6,4 @@ The **proj_flow** contains only ``__version__`` to be updated, nothing more.
6
6
  This is in an attempt to make this module easy to load initially.
7
7
  """
8
8
 
9
- __version__ = "0.19.0"
9
+ __version__ = "0.20.1"
proj_flow/api/makefile.py CHANGED
@@ -73,7 +73,7 @@ class Rule(ABC):
73
73
  @abstractmethod
74
74
  def command(self, statement: Statement) -> List[str]: ...
75
75
 
76
- def run(self, statement: Statement, rt: Runtime):
76
+ def run(self, statement: Statement, rt: Runtime) -> int:
77
77
  return 1
78
78
 
79
79
  @classmethod
@@ -0,0 +1,13 @@
1
+ # Copyright (c) 2026 Marcin Zdun
2
+ # This file is licensed under MIT license (see LICENSE for details)
3
+
4
+ from proj_flow.api import arg
5
+ from proj_flow.ext.tools.pragma_once import pragma_once
6
+ from proj_flow.ext.tools.run_linter import run_linter
7
+
8
+ __all__ = ["tools", "pragma_once", "run_linter"]
9
+
10
+
11
+ @arg.command("tools")
12
+ def tools():
13
+ """Run various C++ tools and helpers"""
@@ -0,0 +1,40 @@
1
+ # Copyright (c) 2026 Marcin Zdun
2
+ # This file is licensed under MIT license (see LICENSE for details)
3
+
4
+ from pathlib import Path
5
+
6
+ from proj_flow.api import arg
7
+
8
+
9
+ def _pragma_present(path: Path):
10
+ with path.open(encoding="UTF-8") as text:
11
+ for line in text:
12
+ line = line.rstrip()
13
+ if line == "#pragma once":
14
+ return True
15
+ return False
16
+
17
+
18
+ @arg.command("tools", "pragma-once")
19
+ def pragma_once():
20
+ """Check presence and absence of ``#pragma once``, as expected"""
21
+
22
+ code_exts = [".c", ".c++", ".cc", ".cpp", ".cxx"]
23
+ header_exts = [".h", ".h++", ".hh", ".hpp", ".hxx"]
24
+
25
+ paths: dict[bool, list[Path]] = {True: [], False: []}
26
+
27
+ for root, dirs, files in Path(".").walk():
28
+ dirs[:] = [dir_name for dir_name in dirs if dir_name not in [".git", "build"]]
29
+ for filename in files:
30
+ path = root / filename
31
+ ext = path.suffix
32
+ if ext in code_exts:
33
+ paths[False].append(path)
34
+ elif ext in header_exts:
35
+ paths[True].append(path)
36
+
37
+ for expected in paths:
38
+ for path in paths[expected]:
39
+ if _pragma_present(path) != expected:
40
+ print(path.as_posix())
@@ -0,0 +1,228 @@
1
+ # Copyright (c) 2026 Marcin Zdun
2
+ # This code is licensed under MIT license (see LICENSE for details)
3
+
4
+ import shlex
5
+ import subprocess
6
+ import urllib.parse
7
+ from dataclasses import dataclass
8
+ from pathlib import Path
9
+ from typing import Annotated, Any, cast
10
+
11
+ import requests_cache
12
+ from yaml import load, safe_load
13
+
14
+ from proj_flow.api.arg import Argument, command
15
+ from proj_flow.api.env import Runtime
16
+
17
+ try:
18
+ from yaml import CLoader as Loader
19
+ except ImportError:
20
+ from yaml import Loader
21
+
22
+ IMAGE = "github/super-linter"
23
+ PREFIX = f"{IMAGE}/"
24
+
25
+
26
+ @dataclass
27
+ class Image:
28
+ repo: str
29
+ name: str
30
+ version: str
31
+
32
+ @property
33
+ def full_name(self):
34
+ return f"{self.repo}/{self.name}"
35
+
36
+ @property
37
+ def label(self):
38
+ return f"{self.repo}/{self.name}:{self.version}"
39
+
40
+ def super_linter_args(self, env: dict[str, str], log_level: str):
41
+ _env = env.copy()
42
+ _env["RUN_LOCAL"] = "true"
43
+ _env["LOG_LEVEL"] = log_level
44
+ _env["USE_FIND_ALGORITHM"] = "true"
45
+ _env["VALIDATE_ALL_CODEBASE"] = "true"
46
+
47
+ args = [
48
+ arg
49
+ for pair in (["-e", f"{key}={_env[key]}"] for key in _env)
50
+ for arg in pair
51
+ ]
52
+ return [
53
+ "docker",
54
+ "run",
55
+ "--rm",
56
+ *args,
57
+ "-v" f"{Path().absolute().as_posix()}:/tmp/lint",
58
+ self.label,
59
+ ]
60
+
61
+
62
+ @dataclass
63
+ class Step:
64
+ name: str
65
+ image: Image
66
+ env: dict[str, str]
67
+
68
+
69
+ @dataclass
70
+ class Job:
71
+ name: str
72
+ steps: list[Step]
73
+
74
+
75
+ @command("tools", "run-linter")
76
+ def run_linter(
77
+ log_level: Annotated[
78
+ str,
79
+ Argument(
80
+ help="Select one of log levels (defaults to INFO)",
81
+ choices=["ERROR", "WARN", "NOTICE", "INFO", "DEBUG"],
82
+ default="INFO",
83
+ opt=True,
84
+ ),
85
+ ],
86
+ rt: Runtime,
87
+ ):
88
+ """Run dockerized super-linter with all the settings from local GitHub workflows"""
89
+
90
+ WORKFLOWS = Path(".github/workflows")
91
+ if not WORKFLOWS.is_dir():
92
+ if not rt.silent:
93
+ print("This project has no GitHub workflows")
94
+ return 0
95
+
96
+ db_path = Path("build")
97
+ db_path.mkdir(parents=True, exist_ok=True)
98
+ db_path = db_path / "cache_db"
99
+
100
+ session = requests_cache.CachedSession(db_path)
101
+ JOBS: list[Job] = []
102
+ for root, dirs, files in WORKFLOWS.walk():
103
+ for filename in files:
104
+ path = root / filename
105
+ if path.suffix not in [".yml", ".yaml"]:
106
+ continue
107
+
108
+ with path.open(encoding="UTF-8") as f:
109
+ data = load(f, Loader=Loader)
110
+ try:
111
+ yml_name = cast(str, data["name"])
112
+ except KeyError:
113
+ yml_name = path.stem
114
+ JOBS.extend(_get_linter_actions(data, yml_name, session, rt.verbose))
115
+
116
+ for job in JOBS:
117
+ job_name, steps = job.name, job.steps
118
+ print(f"job: \033[1;49;95m{job_name}\033[m")
119
+ for step in steps:
120
+ print(
121
+ f"step: \033[1;49;96m{step.name}\033[m \033[2;49;32m({step.image.full_name}:\033[0;49;32m{step.image.version}\033[2;49;32m)\033[m"
122
+ )
123
+ args = step.image.super_linter_args(step.env, log_level=log_level)
124
+ print("\033[0;49;30m{}\033[m".format(shlex.join(args)))
125
+ subprocess.run(args, shell=False)
126
+ print()
127
+
128
+
129
+ def _download_docker_image_id(
130
+ action: str,
131
+ version: str | None,
132
+ session: requests_cache.CachedSession,
133
+ verbose: bool,
134
+ ):
135
+ if action != IMAGE and not action.startswith(PREFIX):
136
+ return None
137
+
138
+ repo = "https://raw.githubusercontent.com/github/super-linter"
139
+ refs = f"refs/tags/{version}" if version else "refs/heads/main"
140
+ filename = "action.yml"
141
+ if action.startswith(PREFIX):
142
+ filename = f"{action[len(PREFIX):]}/{filename}"
143
+
144
+ url = f"{repo}/{refs}/{filename}"
145
+ if verbose:
146
+ print("-- run-linter: Downloading", url)
147
+ response = session.get(url)
148
+ response.raise_for_status()
149
+ data = cast(dict[str, dict[str, str]], safe_load(response.text))
150
+ runs = data.get("runs", {})
151
+ if runs.get("using") != "docker":
152
+ return None
153
+
154
+ image = runs.get("image")
155
+ if not image:
156
+ return None
157
+
158
+ url = urllib.parse.urlparse(image)
159
+ name, version = url.path[1:].split(":", 1)
160
+ return Image(url.netloc, name, version)
161
+
162
+
163
+ def _get_linter_action(
164
+ step: dict, session: requests_cache.CachedSession, verbose: bool
165
+ ):
166
+ try:
167
+ id_parts = cast(str, step["uses"]).split("@", 1)
168
+ except KeyError:
169
+ return
170
+
171
+ action = id_parts[0]
172
+ action_version = id_parts[1] if len(id_parts) > 1 else None
173
+ image = _download_docker_image_id(action, action_version, session, verbose)
174
+ if not image:
175
+ return
176
+
177
+ try:
178
+ name = cast(str, step["name"])
179
+ except KeyError:
180
+ name = cast(str, step["uses"])
181
+
182
+ if verbose:
183
+ print(f'-- run-linter: Found {image.label} used by "{name}"')
184
+
185
+ try:
186
+ env = cast(dict[str, Any], step["env"])
187
+ except KeyError:
188
+ env = {}
189
+
190
+ _env: dict[str, str] = {}
191
+ for key in env:
192
+ value = env[key]
193
+ if isinstance(value, bool):
194
+ _env[key] = "true" if value else "false"
195
+ continue
196
+ if isinstance(value, str) and "${{" in value:
197
+ continue
198
+ _env[key] = value
199
+
200
+ return Step(name=name, image=image, env=_env)
201
+
202
+
203
+ def _get_linter_actions(
204
+ data: dict, yml_name: str, session: requests_cache.CachedSession, verbose: bool
205
+ ):
206
+ result: list[Job] = []
207
+ for job_id, job in cast(dict[str, dict], data["jobs"]).items():
208
+ steps: list[Step] = []
209
+ for step_def in cast(list[dict], job["steps"]):
210
+ step = _get_linter_action(step_def, session, verbose)
211
+ if step is None:
212
+ continue
213
+ steps.append(step)
214
+
215
+ if not steps:
216
+ continue
217
+
218
+ try:
219
+ name = cast(str, job["name"])
220
+ except KeyError:
221
+ name = job_id
222
+
223
+ if name != yml_name:
224
+ name = f"{yml_name} » {name}"
225
+
226
+ result.append(Job(name=name, steps=steps))
227
+
228
+ return result
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: proj-flow
3
- Version: 0.19.0
3
+ Version: 0.20.1
4
4
  Summary: C++ project maintenance, automated
5
5
  Project-URL: Changelog, https://github.com/mzdun/proj-flow/blob/main/CHANGELOG.rst
6
6
  Project-URL: Documentation, https://proj-flow.readthedocs.io/en/latest/
@@ -22,6 +22,7 @@ Requires-Dist: chevron2021
22
22
  Requires-Dist: prompt-toolkit
23
23
  Requires-Dist: pywebidl2
24
24
  Requires-Dist: pyyaml
25
+ Requires-Dist: requests-cache
25
26
  Requires-Dist: toml
26
27
  Description-Content-Type: text/markdown
27
28
 
@@ -1,4 +1,4 @@
1
- proj_flow/__init__.py,sha256=mcoLRnfbs2cN5EIdEz5liGKLp_d_SkFreWMn94VFFiU,277
1
+ proj_flow/__init__.py,sha256=ghRf_ta3bOGtcUvsUvM3PlPP3np2ais8jHQHW2Y76bg,277
2
2
  proj_flow/__main__.py,sha256=HUar_qQ9Ndmchmryegtzu__5wukwCLrFN_SGRl5Ol_M,233
3
3
  proj_flow/dependency.py,sha256=CpcnR6El8AO9hlLc9lQtYQADYlkx3GMHlkLYbEAtdMI,4639
4
4
  proj_flow/api/__init__.py,sha256=gV2f6kll_5JXtvkGASvnx7CbOWr34PHOdck-4ce-qEk,378
@@ -7,7 +7,7 @@ proj_flow/api/completers.py,sha256=NapNVu6QAQ_iF6dqcAzOV5kDHKD9MAMVX209Bklq-Mw,2
7
7
  proj_flow/api/ctx.py,sha256=IJu0q0Chivo6b2M4MKkAlV09oi7Cn9VxtDFeAeL_tnc,6646
8
8
  proj_flow/api/env.py,sha256=rbNtMB6bBRhUb6bolNduWVWZ7a5CIsnv8EEZsamT9Ek,12678
9
9
  proj_flow/api/init.py,sha256=p4ZDGfq6fw4bXbJu2iq0vEmVxbS7nALIhZfe-XnEs44,565
10
- proj_flow/api/makefile.py,sha256=AxtGOvmypBtSvoyMEDJq1bGoaVD5yW9YYRZSdunUEeg,3856
10
+ proj_flow/api/makefile.py,sha256=q0fBSsWTWfR5YwunwiRjWJKtiLeHdSKUgUTEgo5I7dE,3863
11
11
  proj_flow/api/release.py,sha256=IM4axJ6dfyilCmpwL_Z8q43XGKsfHaPoMAdqSziwT7s,2810
12
12
  proj_flow/api/step.py,sha256=C6LfjAN5edYjlpkhd6k9eMO9SLfLtyS2ZG1Rg_dLcjg,4926
13
13
  proj_flow/base/__cmake_version__.py,sha256=imja0GnhpBvS8Crz-64eOUKhc4i6FeRrjBGRB68x_p0,239
@@ -46,6 +46,9 @@ proj_flow/ext/python/version.py,sha256=pnyuKATyZwBh1p0gf9KmqbRSZx8hJ5285CiFK_tHE
46
46
  proj_flow/ext/sign/__init__.py,sha256=b9AN1_BalPtVy7YLBjvGhLamTujHEKcZP7npgDDDuSI,4296
47
47
  proj_flow/ext/sign/api.py,sha256=l5SO5RHiHTwxg0aexkGOfApRdojWDcIBY_cfbKSKsC0,2286
48
48
  proj_flow/ext/sign/win32.py,sha256=yMAmO-DdIWZdOi_NxycRym8XM9WIsrWKtFANdIwthJ4,4968
49
+ proj_flow/ext/tools/__init__.py,sha256=m9iJeCkdmrqLjBWNx9hp4o1H2kgzIXpgypHiSf3SzKE,373
50
+ proj_flow/ext/tools/pragma_once.py,sha256=BiNvX5X5pX_bGPZwBaKISlL6KkxJJGWFeZGSO-UDAgo,1218
51
+ proj_flow/ext/tools/run_linter.py,sha256=_EdmTfITc_hwI5hgMc_mKFkbw4unnDGkWaFohs0RDr4,6145
49
52
  proj_flow/ext/webidl/__init__.py,sha256=StPcfYM0l6Q3QNoXj5DfDpes6WwvhgfNnARQlhEoZco,316
50
53
  proj_flow/ext/webidl/registry.py,sha256=x-UeflSrbUBMCH0nTSjpHisSnxZkQUNWfyXoSlDcavA,585
51
54
  proj_flow/ext/webidl/base/__init__.py,sha256=VJs_ODnF7-8DE0MbYKX-Bs-JDNqHgGdMkk2w142bcYQ,101
@@ -157,8 +160,8 @@ proj_flow/template/licenses/MIT.mustache,sha256=NncPoQaNsuy-WmRmboik3fyhJJ8m5pc2
157
160
  proj_flow/template/licenses/Unlicense.mustache,sha256=awOCsWJ58m_2kBQwBUGWejVqZm6wuRtCL2hi9rfa0X4,1211
158
161
  proj_flow/template/licenses/WTFPL.mustache,sha256=lvF4V_PrKKfZPa2TC8CZo8tlqaKvs3Bpv9G6XsWWQ4k,483
159
162
  proj_flow/template/licenses/Zlib.mustache,sha256=uIj-mhSjes2HJ3rRapyy2ALflKRz4xQgS4mVM9827C0,868
160
- proj_flow-0.19.0.dist-info/METADATA,sha256=0SYn6uyzQ0xfGeduquoEvAK7eLVJ-OlUt1ITiNnwQHc,3005
161
- proj_flow-0.19.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
162
- proj_flow-0.19.0.dist-info/entry_points.txt,sha256=d_OmGKZzpY7FCWz0sZ4wnBAPZC75oMEzTgJZWtpDELo,49
163
- proj_flow-0.19.0.dist-info/licenses/LICENSE,sha256=vpOQJ5QlrTedF3coEWvA4wJzVJH304f66ZitR7Od4iU,1068
164
- proj_flow-0.19.0.dist-info/RECORD,,
163
+ proj_flow-0.20.1.dist-info/METADATA,sha256=1Ln8vCw1vobgp1FsGIiocIy2jVtkkp4y6bLYRJhCJkc,3035
164
+ proj_flow-0.20.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
165
+ proj_flow-0.20.1.dist-info/entry_points.txt,sha256=d_OmGKZzpY7FCWz0sZ4wnBAPZC75oMEzTgJZWtpDELo,49
166
+ proj_flow-0.20.1.dist-info/licenses/LICENSE,sha256=vpOQJ5QlrTedF3coEWvA4wJzVJH304f66ZitR7Od4iU,1068
167
+ proj_flow-0.20.1.dist-info/RECORD,,