proj-flow 0.8.1__py3-none-any.whl → 0.9.0__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 +1 -1
- proj_flow/__main__.py +1 -1
- proj_flow/api/__init__.py +11 -2
- proj_flow/api/arg.py +14 -6
- proj_flow/api/env.py +15 -35
- proj_flow/api/release.py +99 -0
- proj_flow/api/step.py +12 -2
- proj_flow/base/__init__.py +2 -2
- proj_flow/base/inspect.py +15 -44
- proj_flow/base/plugins.py +41 -2
- proj_flow/base/registry.py +105 -0
- proj_flow/cli/__init__.py +55 -0
- proj_flow/cli/argument.py +447 -0
- proj_flow/{flow/cli → cli}/finder.py +1 -1
- proj_flow/ext/__init__.py +6 -0
- proj_flow/ext/github/__init__.py +11 -0
- proj_flow/ext/github/cli.py +118 -0
- proj_flow/ext/github/hosting.py +19 -0
- proj_flow/ext/markdown_changelist.py +14 -0
- proj_flow/ext/python/__init__.py +10 -0
- proj_flow/ext/python/rtdocs.py +238 -0
- proj_flow/ext/python/steps.py +71 -0
- proj_flow/ext/python/version.py +98 -0
- proj_flow/ext/re_structured_changelist.py +14 -0
- proj_flow/flow/__init__.py +3 -3
- proj_flow/flow/configs.py +21 -5
- proj_flow/flow/dependency.py +8 -6
- proj_flow/flow/steps.py +6 -9
- proj_flow/log/__init__.py +10 -2
- proj_flow/log/commit.py +19 -4
- proj_flow/log/error.py +31 -0
- proj_flow/log/hosting/github.py +10 -6
- proj_flow/log/msg.py +23 -0
- proj_flow/log/release.py +112 -21
- proj_flow/log/rich_text/__init__.py +0 -12
- proj_flow/log/rich_text/api.py +10 -4
- proj_flow/minimal/__init__.py +11 -0
- proj_flow/{plugins/commands → minimal}/bootstrap.py +2 -2
- proj_flow/{plugins/commands → minimal}/list.py +12 -10
- proj_flow/{plugins/commands → minimal}/run.py +20 -11
- proj_flow/{plugins/commands → minimal}/system.py +2 -2
- proj_flow/plugins/__init__.py +1 -1
- proj_flow/template/layers/base/.flow/matrix.yml +1 -1
- proj_flow/template/layers/cmake/CMakeLists.txt.mustache +1 -1
- proj_flow/template/layers/github_actions/.github/workflows/build.yml +1 -1
- proj_flow/template/layers/github_social/.github/ISSUE_TEMPLATE/feature_request.md.mustache +1 -1
- {proj_flow-0.8.1.dist-info → proj_flow-0.9.0.dist-info}/METADATA +6 -5
- {proj_flow-0.8.1.dist-info → proj_flow-0.9.0.dist-info}/RECORD +51 -37
- proj_flow-0.9.0.dist-info/entry_points.txt +2 -0
- proj_flow/flow/cli/__init__.py +0 -66
- proj_flow/flow/cli/cmds.py +0 -385
- proj_flow-0.8.1.dist-info/entry_points.txt +0 -2
- {proj_flow-0.8.1.dist-info → proj_flow-0.9.0.dist-info}/WHEEL +0 -0
- {proj_flow-0.8.1.dist-info → proj_flow-0.9.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
# Copyright (c) 2025 Marcin Zdun
|
|
2
|
+
# This code is licensed under MIT license (see LICENSE for details)
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
The **proj_flow.cli.arguments** provides command-line builders and runners,
|
|
6
|
+
supporting the functions decorated with :func:`@arg.command()
|
|
7
|
+
<proj_flow.api.arg.command>`.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import typing
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
|
|
14
|
+
from proj_flow import __version__
|
|
15
|
+
from proj_flow.api import arg, completers, env, step
|
|
16
|
+
from proj_flow.base import inspect as _inspect
|
|
17
|
+
from proj_flow.base import registry
|
|
18
|
+
from proj_flow.flow import configs
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Completer(typing.Protocol):
|
|
22
|
+
def __call__(self, **kwarg) -> typing.Any: ...
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Action(typing.Protocol):
|
|
26
|
+
completer: Completer
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Subparsers(typing.Protocol):
|
|
30
|
+
parent: "Parser"
|
|
31
|
+
|
|
32
|
+
def add_parser(*args, **kwargs) -> "Parser": ...
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Parser(argparse.ArgumentParser):
|
|
36
|
+
flow: env.FlowConfig
|
|
37
|
+
menu: typing.List["Command"]
|
|
38
|
+
shortcuts: typing.Dict[str, typing.List[str]]
|
|
39
|
+
|
|
40
|
+
def __init__(self, **kwargs):
|
|
41
|
+
super().__init__(**kwargs)
|
|
42
|
+
|
|
43
|
+
def add_subparsers(self, **kwargs) -> Subparsers:
|
|
44
|
+
return typing.cast(Subparsers, super().add_subparsers(**kwargs))
|
|
45
|
+
|
|
46
|
+
def find_and_run_command(self, args: argparse.Namespace):
|
|
47
|
+
commands = {entry.name for entry in self.menu}
|
|
48
|
+
aliases = self.flow.aliases
|
|
49
|
+
|
|
50
|
+
rt = env.Runtime(args, self.flow)
|
|
51
|
+
|
|
52
|
+
if rt.verbose:
|
|
53
|
+
verbose_info(self.menu)
|
|
54
|
+
step.verbose_info()
|
|
55
|
+
registry.verbose_info()
|
|
56
|
+
|
|
57
|
+
if args.command in commands:
|
|
58
|
+
command = _first(lambda command: command.name == args.command, self.menu)
|
|
59
|
+
if command:
|
|
60
|
+
return command.run(args, rt)
|
|
61
|
+
elif args.command in {alias.name for alias in aliases}:
|
|
62
|
+
command = _first(lambda command: command.name == "run", self.menu)
|
|
63
|
+
alias = _first(lambda alias: alias.name == args.command, aliases)
|
|
64
|
+
if command and alias:
|
|
65
|
+
args.cli_steps.append(",".join(alias.steps))
|
|
66
|
+
return command.run(args, rt)
|
|
67
|
+
|
|
68
|
+
lines = ["the command arguments are required; known commands:", ""]
|
|
69
|
+
for command in self.menu:
|
|
70
|
+
lines.append(f" - {command.name}: {command.doc}")
|
|
71
|
+
for alias in aliases:
|
|
72
|
+
lines.append(f" - {alias.name}: {alias.doc}")
|
|
73
|
+
|
|
74
|
+
self.error("\n".join(lines))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def build_argparser(flow_cfg: env.FlowConfig):
|
|
78
|
+
parser = Parser(
|
|
79
|
+
prog="proj-flow",
|
|
80
|
+
description="C++ project maintenance, automated",
|
|
81
|
+
add_help=False,
|
|
82
|
+
)
|
|
83
|
+
parser.add_argument(
|
|
84
|
+
"-h", "--help", action="help", help="Show this help message and exit"
|
|
85
|
+
)
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"-v",
|
|
88
|
+
"--version",
|
|
89
|
+
action="version",
|
|
90
|
+
default=argparse.SUPPRESS,
|
|
91
|
+
version=f"%(prog)s version {__version__}",
|
|
92
|
+
help="Show proj-flow's version and exit",
|
|
93
|
+
)
|
|
94
|
+
parser.add_argument(
|
|
95
|
+
"-C",
|
|
96
|
+
metavar="dir",
|
|
97
|
+
nargs="?",
|
|
98
|
+
help="Run as if proj-flow was started in <dir> instead of the current "
|
|
99
|
+
"working directory. This directory must exist.",
|
|
100
|
+
).completer = completers.cd_completer # type: ignore
|
|
101
|
+
|
|
102
|
+
menu = _build_menu(arg.get_commands(), None)
|
|
103
|
+
_argparse_visit_all(parser, flow_cfg, menu.children)
|
|
104
|
+
return parser
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def expand_shortcuts(parser: Parser, args: argparse.Namespace):
|
|
108
|
+
args_kwargs = dict(args._get_kwargs())
|
|
109
|
+
shortcuts: typing.Dict[str, typing.List[str]] = parser.shortcuts
|
|
110
|
+
for key in shortcuts:
|
|
111
|
+
try:
|
|
112
|
+
if not args_kwargs[key]:
|
|
113
|
+
continue
|
|
114
|
+
typing.cast(typing.List[str], args.configs).extend(shortcuts[key])
|
|
115
|
+
break
|
|
116
|
+
except KeyError:
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass
|
|
121
|
+
class AdditionalArgument:
|
|
122
|
+
name: str
|
|
123
|
+
ctor: callable # type: ignore
|
|
124
|
+
|
|
125
|
+
def create(self, rt: env.Runtime, args: argparse.Namespace, menu: "Command"):
|
|
126
|
+
if self.ctor == env.Runtime:
|
|
127
|
+
return rt
|
|
128
|
+
if self.ctor == Command:
|
|
129
|
+
return menu
|
|
130
|
+
return self.ctor(rt, args)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@dataclass
|
|
134
|
+
class AnnotatedArgument:
|
|
135
|
+
name: str
|
|
136
|
+
argument: arg.Argument
|
|
137
|
+
|
|
138
|
+
def argparse_visit(self, parser: argparse.ArgumentParser):
|
|
139
|
+
return self.argument.visit(parser, self.name)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
AnyArgument = typing.Union[AdditionalArgument, AnnotatedArgument]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass
|
|
146
|
+
class Command:
|
|
147
|
+
name: str
|
|
148
|
+
doc: str
|
|
149
|
+
entry: callable # type: ignore
|
|
150
|
+
annotated: typing.List[AnnotatedArgument]
|
|
151
|
+
additional: typing.List[AdditionalArgument]
|
|
152
|
+
parent: typing.Optional["Command"]
|
|
153
|
+
children: typing.List["Command"] = field(default_factory=list)
|
|
154
|
+
|
|
155
|
+
def argparse_visit(
|
|
156
|
+
self,
|
|
157
|
+
subparsers: Subparsers,
|
|
158
|
+
alias: typing.Optional[str] = None,
|
|
159
|
+
doc: typing.Optional[str] = None,
|
|
160
|
+
level=0,
|
|
161
|
+
):
|
|
162
|
+
if not doc:
|
|
163
|
+
doc = self.doc
|
|
164
|
+
if not alias:
|
|
165
|
+
alias = self.name
|
|
166
|
+
|
|
167
|
+
parser = subparsers.add_parser(
|
|
168
|
+
alias, help=doc.split("\n\n")[0], description=doc, add_help=False
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
parent = subparsers.parent
|
|
172
|
+
parser.flow = parent.flow
|
|
173
|
+
parser.shortcuts = parent.shortcuts
|
|
174
|
+
|
|
175
|
+
assert parent.flow is not None
|
|
176
|
+
assert parent.shortcuts is not None
|
|
177
|
+
|
|
178
|
+
_argparse_runtime_visit(parser)
|
|
179
|
+
|
|
180
|
+
has_config = False
|
|
181
|
+
for additional in self.additional:
|
|
182
|
+
if additional.ctor == configs.Configs:
|
|
183
|
+
has_config = True
|
|
184
|
+
break
|
|
185
|
+
|
|
186
|
+
if has_config:
|
|
187
|
+
_argparse_config_visit(parser)
|
|
188
|
+
|
|
189
|
+
for arg in self.annotated:
|
|
190
|
+
arg.argparse_visit(parser)
|
|
191
|
+
|
|
192
|
+
if len(self.children):
|
|
193
|
+
subparsers = parser.add_subparsers(
|
|
194
|
+
dest=f"command_{level}",
|
|
195
|
+
metavar="{command}",
|
|
196
|
+
help="Known command name, see below",
|
|
197
|
+
)
|
|
198
|
+
subparsers.parent = parser # type: ignore
|
|
199
|
+
|
|
200
|
+
for entry in self.children:
|
|
201
|
+
entry.argparse_visit(subparsers, level=level + 1)
|
|
202
|
+
|
|
203
|
+
def run(self, args: argparse.Namespace, rt: env.Runtime, level=0):
|
|
204
|
+
if level == 0 and rt.only_host:
|
|
205
|
+
rt.only_host = self.name == "run"
|
|
206
|
+
|
|
207
|
+
subcommand_name = None
|
|
208
|
+
|
|
209
|
+
if len(self.children):
|
|
210
|
+
subcommand_attribute = f"command_{level}"
|
|
211
|
+
if hasattr(args, subcommand_attribute):
|
|
212
|
+
subcommand_name = getattr(args, subcommand_attribute)
|
|
213
|
+
|
|
214
|
+
if subcommand_name is not None:
|
|
215
|
+
subcommand = _first(
|
|
216
|
+
lambda command: command.name == subcommand_name, self.children
|
|
217
|
+
)
|
|
218
|
+
if not subcommand:
|
|
219
|
+
rt.fatal(f"cannot find {subcommand_name}")
|
|
220
|
+
|
|
221
|
+
return subcommand.run(args, rt, level=level + 1)
|
|
222
|
+
|
|
223
|
+
kwargs = {}
|
|
224
|
+
for arg in self.annotated:
|
|
225
|
+
kwargs[arg.name] = getattr(args, arg.name, None)
|
|
226
|
+
|
|
227
|
+
for additional in self.additional:
|
|
228
|
+
arg = additional.create(rt, args, self)
|
|
229
|
+
kwargs[additional.name] = arg
|
|
230
|
+
|
|
231
|
+
result = self.entry(**kwargs)
|
|
232
|
+
return 0 if result is None else result
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
T = typing.TypeVar("T")
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _first(
|
|
239
|
+
fltr: typing.Callable[[T], bool], items: typing.Iterable[T]
|
|
240
|
+
) -> typing.Optional[T]:
|
|
241
|
+
try:
|
|
242
|
+
return next(filter(fltr, items))
|
|
243
|
+
except StopIteration:
|
|
244
|
+
return None
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _argparse_visit_all(
|
|
248
|
+
parser: Parser, cfg: env.FlowConfig, menu: typing.List["Command"]
|
|
249
|
+
):
|
|
250
|
+
shortcut_configs = _build_run_shortcuts(cfg)
|
|
251
|
+
|
|
252
|
+
parser.menu = menu
|
|
253
|
+
parser.flow = cfg
|
|
254
|
+
parser.shortcuts = shortcut_configs
|
|
255
|
+
|
|
256
|
+
subparsers = parser.add_subparsers(
|
|
257
|
+
dest="command", metavar="{command}", help="Known command name, see below"
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
subparsers.parent = parser # type: ignore
|
|
261
|
+
|
|
262
|
+
run: typing.Optional[Command] = None
|
|
263
|
+
for entry in menu:
|
|
264
|
+
entry.argparse_visit(subparsers)
|
|
265
|
+
if entry.name == "run":
|
|
266
|
+
run = entry
|
|
267
|
+
|
|
268
|
+
if run is not None and len(cfg.aliases) > 0:
|
|
269
|
+
commands = {entry.name for entry in menu}
|
|
270
|
+
cfg.aliases = [alias for alias in cfg.aliases if alias.name not in commands]
|
|
271
|
+
for alias in cfg.aliases:
|
|
272
|
+
run.argparse_visit(subparsers, alias=alias.name, doc=alias.doc)
|
|
273
|
+
else:
|
|
274
|
+
cfg.aliases = []
|
|
275
|
+
|
|
276
|
+
pass
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _build_menu(cmd: arg._Command, parent: typing.Optional[Command]):
|
|
280
|
+
name = cmd.name
|
|
281
|
+
doc = cmd.doc or ""
|
|
282
|
+
entry = cmd.entry or (lambda: 0)
|
|
283
|
+
|
|
284
|
+
args = _extract_args(entry)
|
|
285
|
+
additional = [entry for entry in args if isinstance(entry, AdditionalArgument)]
|
|
286
|
+
annotated = [entry for entry in args if isinstance(entry, AnnotatedArgument)]
|
|
287
|
+
|
|
288
|
+
current = Command(
|
|
289
|
+
name=name,
|
|
290
|
+
doc=doc,
|
|
291
|
+
entry=entry,
|
|
292
|
+
annotated=annotated,
|
|
293
|
+
additional=additional,
|
|
294
|
+
parent=parent,
|
|
295
|
+
)
|
|
296
|
+
for child in cmd.subs.values():
|
|
297
|
+
current.children.append(_build_menu(child, current))
|
|
298
|
+
|
|
299
|
+
return current
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _extract_arg(argument: _inspect.Argument):
|
|
303
|
+
for ctor in [configs.Configs, env.Runtime, Command]:
|
|
304
|
+
if argument.type is ctor:
|
|
305
|
+
return AdditionalArgument(argument.name, ctor)
|
|
306
|
+
|
|
307
|
+
metadata: typing.Optional[arg.Argument] = _first(
|
|
308
|
+
lambda meta: isinstance(meta, arg.Argument), argument.metadata
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
if metadata is None or argument.type is None:
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
optional = metadata.opt
|
|
315
|
+
if optional is None:
|
|
316
|
+
NoneType = type(None)
|
|
317
|
+
is_union = typing.get_origin(argument.type) is typing.Union
|
|
318
|
+
optional = is_union and NoneType in typing.get_args(argument.type)
|
|
319
|
+
metadata.opt = optional
|
|
320
|
+
|
|
321
|
+
return AnnotatedArgument(argument.name, metadata)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _extract_args(entry: callable): # type: ignore
|
|
325
|
+
args_with_possible_nones = map(_extract_arg, _inspect.signature(entry))
|
|
326
|
+
args = filter(lambda item: item is not None, args_with_possible_nones)
|
|
327
|
+
return typing.cast(typing.List[AnyArgument], list(args))
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _build_run_shortcuts(cfg):
|
|
331
|
+
shortcut_configs: typing.Dict[str, typing.List[str]] = {}
|
|
332
|
+
args: typing.List[typing.Tuple[str, typing.List[str], bool, bool]] = []
|
|
333
|
+
|
|
334
|
+
shortcuts = cfg.shortcuts
|
|
335
|
+
for shortcut_name in sorted(shortcuts.keys()):
|
|
336
|
+
has_os = False
|
|
337
|
+
has_compiler = False
|
|
338
|
+
shortcut = shortcuts[shortcut_name]
|
|
339
|
+
config: typing.List[str] = []
|
|
340
|
+
for key in sorted(shortcut.keys()):
|
|
341
|
+
has_os = has_os or key == "os"
|
|
342
|
+
has_compiler = has_compiler or key == "os"
|
|
343
|
+
value = shortcut[key]
|
|
344
|
+
if isinstance(value, list):
|
|
345
|
+
for v in value:
|
|
346
|
+
config.append(f"{key}={_shortcut_value(v)}")
|
|
347
|
+
else:
|
|
348
|
+
config.append(f"{key}={_shortcut_value(value)}")
|
|
349
|
+
if len(config) > 0:
|
|
350
|
+
args.append((shortcut_name, config, has_os, has_compiler))
|
|
351
|
+
|
|
352
|
+
if len(args):
|
|
353
|
+
os_prefix = f"os={env.platform}"
|
|
354
|
+
compiler_prefix = f"compiler={env.default_compiler()}"
|
|
355
|
+
|
|
356
|
+
for shortcut_name, config, has_os, has_compiler in args:
|
|
357
|
+
if not has_compiler:
|
|
358
|
+
config.insert(0, compiler_prefix)
|
|
359
|
+
if not has_os:
|
|
360
|
+
config.insert(0, os_prefix)
|
|
361
|
+
shortcut_configs[shortcut_name] = config
|
|
362
|
+
|
|
363
|
+
return shortcut_configs
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def _shortcut_value(value) -> str:
|
|
367
|
+
if isinstance(value, bool):
|
|
368
|
+
return "ON" if value else "OFF"
|
|
369
|
+
return str(value)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _argparse_runtime_visit(parser: Parser):
|
|
373
|
+
parser.add_argument(
|
|
374
|
+
"-h",
|
|
375
|
+
"--help",
|
|
376
|
+
action="help",
|
|
377
|
+
default=argparse.SUPPRESS,
|
|
378
|
+
help="Show this help message and exit",
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
parser.add_argument(
|
|
382
|
+
"--dry-run",
|
|
383
|
+
action="store_true",
|
|
384
|
+
required=False,
|
|
385
|
+
help="Print steps and commands, do nothing",
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
verbosity = parser.add_mutually_exclusive_group()
|
|
389
|
+
verbosity.add_argument(
|
|
390
|
+
"--silent",
|
|
391
|
+
action="store_true",
|
|
392
|
+
required=False,
|
|
393
|
+
help="Remove most of the output",
|
|
394
|
+
)
|
|
395
|
+
verbosity.add_argument(
|
|
396
|
+
"--verbose",
|
|
397
|
+
action="store_true",
|
|
398
|
+
required=False,
|
|
399
|
+
help="Add even more output",
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _argparse_config_visit(parser: Parser):
|
|
404
|
+
parser.add_argument(
|
|
405
|
+
"-D",
|
|
406
|
+
dest="configs",
|
|
407
|
+
metavar="key=value",
|
|
408
|
+
nargs="*",
|
|
409
|
+
action="store",
|
|
410
|
+
default=[],
|
|
411
|
+
help="Run only builds on matching configs. The key is one of "
|
|
412
|
+
'the keys into "matrix" object in .flow/matrix.yml definition '
|
|
413
|
+
"and the value is one of the possible values for that key. In "
|
|
414
|
+
"case of boolean flags, such as sanitizer, the true value is "
|
|
415
|
+
'one of "true", "on", "yes", "1" and "with-<key>", '
|
|
416
|
+
'i.e. "with-sanitizer" for sanitizer.'
|
|
417
|
+
" "
|
|
418
|
+
"If given key is never used, all values from .flow/matrix.yaml "
|
|
419
|
+
"for that key are used. Otherwise, only values from command "
|
|
420
|
+
"line are used.",
|
|
421
|
+
).completer = completers.matrix_completer # type: ignore
|
|
422
|
+
|
|
423
|
+
parser.add_argument(
|
|
424
|
+
"--official",
|
|
425
|
+
action="store_true",
|
|
426
|
+
required=False,
|
|
427
|
+
help="Cut matrix to release builds only",
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
if len(parser.shortcuts):
|
|
431
|
+
group = parser.add_mutually_exclusive_group()
|
|
432
|
+
|
|
433
|
+
for shortcut_name in sorted(parser.shortcuts.keys()):
|
|
434
|
+
config = parser.shortcuts[shortcut_name]
|
|
435
|
+
group.add_argument(
|
|
436
|
+
f"--{shortcut_name}",
|
|
437
|
+
required=False,
|
|
438
|
+
action="store_true",
|
|
439
|
+
help=f'Shortcut for "-D {" ".join(config)}"',
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
def verbose_info(commands: typing.List[Command], prefix = ""):
|
|
443
|
+
for command in commands:
|
|
444
|
+
cli = f"{prefix} {command.name}" if prefix else command.name
|
|
445
|
+
if command.entry is not None:
|
|
446
|
+
print(f"-- Command: adding `{cli}` from `{command.entry.__module__}.{command.entry.__name__}(...)`")
|
|
447
|
+
verbose_info(command.children, cli)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# This code is licensed under MIT license (see LICENSE for details)
|
|
3
3
|
|
|
4
4
|
"""
|
|
5
|
-
The **proj_flow.
|
|
5
|
+
The **proj_flow.cli.finder** extends the :py:class:`argcomplete.finders.CompletionFinder`
|
|
6
6
|
to be able to see proper :class:`api.env.FlowConfig`, when completed invocation
|
|
7
7
|
contains ``-C`` argument.
|
|
8
8
|
"""
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Copyright (c) 2025 Marcin Zdun
|
|
2
|
+
# This code is licensed under MIT license (see LICENSE for details)
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
The **proj_flow.ext.github** provides GitHub support through CLI and GitHub
|
|
6
|
+
hosting plugin.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from . import cli, hosting
|
|
10
|
+
|
|
11
|
+
__all__ = ["cli", "hosting"]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Copyright (c) 2025 Marcin Zdun
|
|
2
|
+
# This code is licensed under MIT license (see LICENSE for details)
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
The **proj_flow.ext.github.cli** adds the ``github`` command, replacing the
|
|
6
|
+
old ``ci`` code. Additionally, it provides ``github matrix`` and ``github
|
|
7
|
+
release`` commands. It will soon also have ``github publish``, finishing the
|
|
8
|
+
job started in ``release``.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
import typing
|
|
16
|
+
|
|
17
|
+
from proj_flow import log
|
|
18
|
+
from proj_flow.api import arg, env
|
|
19
|
+
from proj_flow.flow.configs import Configs
|
|
20
|
+
from proj_flow.log import commit, hosting, rich_text
|
|
21
|
+
|
|
22
|
+
FORCED_LEVEL_CHOICES = list(commit.FORCED_LEVEL.keys())
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _name_list(names: typing.List[str]) -> str:
|
|
26
|
+
if len(names) == 0:
|
|
27
|
+
return ""
|
|
28
|
+
|
|
29
|
+
prefix = ", ".join(names[:-1])
|
|
30
|
+
if prefix:
|
|
31
|
+
prefix += " and "
|
|
32
|
+
return f"{prefix}{names[-1]}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@arg.command("github")
|
|
36
|
+
def github():
|
|
37
|
+
"""Interact with GitHub workflows and releases"""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@arg.command("github", "matrix")
|
|
41
|
+
def matrix(
|
|
42
|
+
official: typing.Annotated[
|
|
43
|
+
bool, arg.FlagArgument(help="Cut matrix to release builds only")
|
|
44
|
+
],
|
|
45
|
+
rt: env.Runtime,
|
|
46
|
+
):
|
|
47
|
+
"""Supply data for GitHub Actions"""
|
|
48
|
+
|
|
49
|
+
configs = Configs(
|
|
50
|
+
rt,
|
|
51
|
+
argparse.Namespace(configs=[], matrix=True, official=official),
|
|
52
|
+
expand_compilers=False,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
usable = [usable.items for usable in configs.usable]
|
|
56
|
+
for config in usable:
|
|
57
|
+
if "--orig-compiler" in config:
|
|
58
|
+
orig_compiler = config["--orig-compiler"]
|
|
59
|
+
del config["--orig-compiler"]
|
|
60
|
+
config["compiler"] = orig_compiler
|
|
61
|
+
|
|
62
|
+
if "GITHUB_ACTIONS" in os.environ:
|
|
63
|
+
var = json.dumps({"include": usable})
|
|
64
|
+
GITHUB_OUTPUT = os.environ.get("GITHUB_OUTPUT")
|
|
65
|
+
if GITHUB_OUTPUT is not None:
|
|
66
|
+
with open(GITHUB_OUTPUT, "a", encoding="UTF-8") as github_output:
|
|
67
|
+
print(f"matrix={var}", file=github_output)
|
|
68
|
+
else:
|
|
69
|
+
print(f"matrix={var}")
|
|
70
|
+
else:
|
|
71
|
+
json.dump(usable, sys.stdout)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@arg.command("github", "release")
|
|
75
|
+
def release(
|
|
76
|
+
rt: env.Runtime,
|
|
77
|
+
all: typing.Annotated[
|
|
78
|
+
bool, arg.FlagArgument(help="Take all Conventional Commits.")
|
|
79
|
+
],
|
|
80
|
+
force: typing.Annotated[
|
|
81
|
+
typing.Optional[str],
|
|
82
|
+
arg.Argument(
|
|
83
|
+
help="Ignore the version change from changelog and instead use this value. "
|
|
84
|
+
f"Allowed values are: {_name_list(FORCED_LEVEL_CHOICES)}",
|
|
85
|
+
meta="level",
|
|
86
|
+
choices=FORCED_LEVEL_CHOICES,
|
|
87
|
+
),
|
|
88
|
+
],
|
|
89
|
+
):
|
|
90
|
+
"""
|
|
91
|
+
Bumps the project version based on current git logs, creates a "chore"
|
|
92
|
+
commit for the change, attaches an annotated tag with the version number
|
|
93
|
+
and pushes it all to GitHub.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
generator = (
|
|
97
|
+
rich_text.api.changelog_generators.first()
|
|
98
|
+
or rich_text.markdown.ChangelogGenerator()
|
|
99
|
+
)
|
|
100
|
+
forced_level = commit.FORCED_LEVEL.get(force) if force else None
|
|
101
|
+
git = commit.Git(rt)
|
|
102
|
+
gh_links = hosting.github.GitHub.from_repo(git) or commit.NoHosting()
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
log.release.add_release(
|
|
106
|
+
rt=rt,
|
|
107
|
+
forced_level=forced_level,
|
|
108
|
+
take_all=all,
|
|
109
|
+
draft=True,
|
|
110
|
+
generator=generator,
|
|
111
|
+
git=git,
|
|
112
|
+
hosting=gh_links,
|
|
113
|
+
)
|
|
114
|
+
except log.release.VersionNotAdvancing as err:
|
|
115
|
+
rt.message(err.message, level=env.Msg.STATUS)
|
|
116
|
+
return 0
|
|
117
|
+
except log.error.ReleaseError as err:
|
|
118
|
+
rt.fatal(err.message)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Copyright (c) 2025 Marcin Zdun
|
|
2
|
+
# This code is licensed under MIT license (see LICENSE for details)
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
The **proj_flow.ext.github.hosting** adds GitHub hosting environment.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from proj_flow.log import commit
|
|
11
|
+
from proj_flow.log.hosting.github import GitHub
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@commit.hosting_factories.add
|
|
15
|
+
class Plugin(commit.HostingFactory):
|
|
16
|
+
def from_repo(
|
|
17
|
+
self, git: commit.Git, remote: Optional[str] = None
|
|
18
|
+
) -> Optional[commit.Hosting]:
|
|
19
|
+
return GitHub.from_repo(git, remote)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Copyright (c) 2025 Marcin Zdun
|
|
2
|
+
# This code is licensed under MIT license (see LICENSE for details)
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
The **proj_flow.ext.markdown_changelist** .
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from proj_flow.log.rich_text.api import changelog_generators
|
|
9
|
+
from proj_flow.log.rich_text.markdown import ChangelogGenerator
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@changelog_generators.add
|
|
13
|
+
class Plugin(ChangelogGenerator):
|
|
14
|
+
pass
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Copyright (c) 2025 Marcin Zdun
|
|
2
|
+
# This code is licensed under MIT license (see LICENSE for details)
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
The **proj_flow.ext.python** defines steps and commands for pyproject packages.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from . import steps, version
|
|
9
|
+
|
|
10
|
+
__all__ = ["steps", "version"]
|