-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.py
More file actions
397 lines (342 loc) · 14 KB
/
main.py
File metadata and controls
397 lines (342 loc) · 14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
from ._version import __version__
from .helpgen import print_help
from .parser import MakefileParser
from .templates import ci_template
from .templates import get_template_environment
from .templates import template
from .topics import collect_missing_dependencies
from .topics import Domain
from .topics import get_domain
from .topics import get_topic
from .topics import load_topics
from .topics import resolve_domain_dependencies
from .topics import set_domain_runtime_depends
from operator import attrgetter
from pathlib import Path
from textwrap import indent
import argparse
import inquirer
import logging
import mxdev
import sys
import typing
import yaml
logger = logging.getLogger("mxmake")
parser = argparse.ArgumentParser()
parser.add_argument(
"-v",
"--version",
action="version",
version=f"%(prog)s {__version__}",
)
command_parsers = parser.add_subparsers(dest="command", required=True)
##############################################################################
# list
##############################################################################
def list_command(args: argparse.Namespace):
if not args.topic:
topics = load_topics()
sys.stdout.write("Topics:\n")
for topic_ in topics:
sys.stdout.write(f" - {topic_.name}\n")
return
topic = get_topic(args.topic)
if topic is None:
sys.stdout.write(f"Requested topic not found: {args.topic}\n")
sys.exit(1)
if not args.domain:
sys.stdout.write(f"Domains in topic {topic.name}:\n")
for domain_ in topic.domains:
description = indent(domain_.description, 4 * " ").strip()
sys.stdout.write(f" - {domain_.name}: {description}\n")
return
domain = topic.domain(args.domain)
if domain is None:
sys.stdout.write(f"Requested domain not found: {args.domain}\n")
sys.exit(1)
assert domain is not None # type narrowing for ty
sys.stdout.write(f"Domain {topic.name}.{domain.name}:\n")
depends = ", ".join(domain.depends) if domain.depends else "No dependencies"
sys.stdout.write(f" Depends: {depends}\n")
sys.stdout.write(" Targets:")
targets = domain.targets
if not targets:
sys.stdout.write(" No targets provided\n")
else:
sys.stdout.write("\n")
for target in targets:
description = indent(target.description, 6 * " ").strip()
sys.stdout.write(f" {target.name}: {description}\n")
sys.stdout.write(" Settings:")
settings = domain.settings
if not settings:
sys.stdout.write(" No settings provided\n")
else:
sys.stdout.write("\n")
for setting in settings:
description = indent(setting.description, 8 * " ").strip()
sys.stdout.write(
f" - {setting.name}: {setting.description}\n"
f" - default value: {setting.default}\n"
)
list_parser = command_parsers.add_parser("list", help="List stuff")
list_parser.set_defaults(func=list_command)
list_parser.add_argument("-t", "--topic", help="Topic name")
list_parser.add_argument("-d", "--domain", help="Domain name")
##############################################################################
# init/update
##############################################################################
def create_config(prompt: bool, preseeds: dict[str, typing.Any] | None):
if prompt and preseeds:
sys.stdout.write("Either use prompt or preseeds, not both\n")
sys.exit(1)
# obtain target folder
target_folder = Path.cwd()
# parse existing makefile
parser = MakefileParser(target_folder / "Makefile")
# obtain topics to include
topics = load_topics()
if preseeds:
sys.stdout.write("Collect topics from preseeds.\n")
topic_choice = {"topic": list(preseeds.get("topics", {}).keys())}
elif not prompt:
sys.stdout.write("Update Makefile without prompting for settings.\n")
topic_choice = {"topic": list(parser.topics)}
else:
topic_choice = inquirer.prompt(
[
inquirer.Checkbox(
"topic",
message="Include topics",
choices=[d.name for d in topics],
default=list(parser.topics),
)
]
)
if topic_choice is None:
sys.stdout.write("No topics selected. Abort\n")
sys.exit(1)
# obtain domains to include
domains: list[Domain] = []
for topic_name in topic_choice["topic"]:
topic = get_topic(topic_name)
all_fqns = [domain.fqn for domain in topic.domains]
# use already configured domains from topic if present in existing
# domain
if parser.topics.get(topic_name):
selected_fqns = [
f"{topic_name}.{name}" for name in parser.topics[topic_name]
]
# fallback to all domains of topic if not configured yet or no
# domain generated yet
else:
selected_fqns = [domain.fqn for domain in topic.domains]
if preseeds:
selected_fqns = [
f"{topic_name}.{domain_name}"
for domain_name in preseeds["topics"][topic_name]
]
sys.stdout.write(f"Collect domains for topic {topic_name} from preseeds.\n")
domains.extend(get_domain(fqn) for fqn in selected_fqns)
continue
if not prompt:
sys.stdout.write(
f"- update topic {topic_name} with domains "
f"{', '.join([fqdn.split('.')[1] for fqdn in selected_fqns])}.\n"
)
domains.extend(get_domain(fqn) for fqn in selected_fqns)
continue
domains_choice = inquirer.prompt(
[
inquirer.Checkbox(
"domains",
message=f'Include domains from topic "{topic_name}"',
choices=all_fqns,
default=selected_fqns,
)
]
)
if domains_choice is None:
sys.stdout.write("No domains selected. Abort\n")
sys.exit(1)
for fqn in domains_choice["domains"]:
domains.append(get_domain(fqn))
domains = collect_missing_dependencies(domains)
set_domain_runtime_depends(domains)
domains = resolve_domain_dependencies(domains)
# obtain settings
domain_settings = {}
for domain in sorted(domains, key=attrgetter("fqn")):
settings = domain.settings
if not settings:
continue
settings_question = []
for setting in settings:
sfqn = f"{domain.fqn}.{setting.name}"
setting_default: typing.Any | object = setting.default
# use default setting from preseeds
if preseeds:
unset = object()
preseed_topic = preseeds.get("topics", {}).get(domain.topic)
preseed_domain = preseed_topic.get(domain.name) if preseed_topic else {}
preseed_value = (
preseed_domain.get(setting.name, unset) if preseed_domain else unset
)
setting_default = (
preseed_value if preseed_value is not unset else setting_default
)
# use configured setting from parser if set
elif sfqn in parser.settings:
setting_default = parser.settings[sfqn]
domain_settings[sfqn] = setting_default
if not prompt or preseeds:
continue
settings_question.append(
inquirer.Text(sfqn, message=sfqn, default=setting_default)
)
if prompt:
sys.stdout.write(f"Edit Settings for {domain.fqn}?\n")
yn = inquirer.text(message="y/N")
if yn in ["Y", "y"]:
domain_settings.update(inquirer.prompt(settings_question))
sys.stdout.write("\n")
if domains:
# generate makefile
factory = template.lookup("makefile")
makefile_template = factory(
target_folder, domains, domain_settings, get_template_environment()
)
makefile_template.write()
else:
sys.stdout.write("Skip generation of Makefile, nothing selected\n")
# mx ini generation
if prompt and not (target_folder / "mx.ini").exists():
sys.stdout.write("\n``mx.ini`` configuration file not exists. Create One?\n")
yn = inquirer.text(message="Y/n")
if yn not in ["n", "N"]:
factory = template.lookup("mx.ini")
mx_ini_template = factory(
target_folder, domains, get_template_environment(), domain_settings
)
mx_ini_template.write()
elif not prompt and not preseeds and not (target_folder / "mx.ini").exists():
sys.stdout.write(
"No generation of mx configuration on update (file does not exist).\n"
)
elif preseeds and "mx-ini" in preseeds and not (target_folder / "mx.ini").exists():
sys.stdout.write("Generate mx configuration file\n")
factory = template.lookup("mx.ini")
mx_ini_template = factory(target_folder, domains, get_template_environment(), domain_settings)
mx_ini_template.write()
else:
sys.stdout.write(
"Skip generation of mx configuration file, file already exists\n"
)
# ci generation
if prompt:
sys.stdout.write("\nDo you want to create CI related files?\n")
yn = inquirer.text(message="y/N")
if yn in ["y", "Y"]:
# ci_template
ci_choice = inquirer.prompt(
[
inquirer.Checkbox(
"ci", message="Generate CI files", choices=ci_template.templates
)
]
)
for template_name in ci_choice["ci"]:
factory = template.lookup(template_name)
factory(get_template_environment(), domain_settings).write()
elif preseeds and "ci-templates" in preseeds:
for template_name in preseeds["ci-templates"]:
sys.stdout.write(f"Generate CI file from {template_name} template\n")
factory = template.lookup(template_name)
factory(get_template_environment(), domain_settings).write()
def auto_detect_project_path_python() -> str | None:
"""Auto-detect Python project in subdirectories if not in current directory."""
cwd = Path.cwd()
# Check if pyproject.toml exists in current directory
if (cwd / "pyproject.toml").exists():
return None # Project is in same directory as Makefile
# Search immediate subdirectories for pyproject.toml
for subdir in cwd.iterdir():
if subdir.is_dir() and (subdir / "pyproject.toml").exists():
return subdir.name
return None # No Python project detected
def init_command(args: argparse.Namespace):
sys.stdout.write("\n#######################\n")
sys.stdout.write("# mxmake initialization\n")
sys.stdout.write("#######################\n\n")
prompt = True
preseeds = None
if args.preseeds:
prompt = False
with open(args.preseeds) as fd:
preseeds = yaml.load(fd.read(), yaml.SafeLoader)
# Handle project-path-python from CLI or auto-detection
project_path_python = args.project_path_python
if project_path_python is None and not args.preseeds:
# Try auto-detection only if not using preseeds
detected_path = auto_detect_project_path_python()
if detected_path:
sys.stdout.write(
f"Auto-detected Python project in subdirectory: {detected_path}\n"
)
if prompt:
yn = inquirer.text(
message=f"Use '{detected_path}' as PROJECT_PATH_PYTHON? (Y/n)"
)
if yn not in ["n", "N"]:
project_path_python = detected_path
else:
project_path_python = detected_path
# Inject project-path-python into preseeds if specified or detected
if project_path_python:
if preseeds is None:
preseeds = {}
if "topics" not in preseeds:
preseeds["topics"] = {}
if "core" not in preseeds["topics"]:
preseeds["topics"]["core"] = {}
if "base" not in preseeds["topics"]["core"]:
preseeds["topics"]["core"]["base"] = {}
preseeds["topics"]["core"]["base"]["PROJECT_PATH_PYTHON"] = project_path_python
sys.stdout.write(f"Setting PROJECT_PATH_PYTHON={project_path_python}\n\n")
create_config(prompt=prompt, preseeds=preseeds)
init_parser = command_parsers.add_parser("init", help="Initialize project")
init_parser.set_defaults(func=init_command)
init_parser.add_argument("-p", "--preseeds", help="Preseeds file")
init_parser.add_argument(
"--project-path-python",
help="Path to Python project relative to Makefile (for monorepo setups)",
default=None,
)
def update_command(args: argparse.Namespace):
sys.stdout.write("\n###############\n")
sys.stdout.write("# mxmake update\n")
sys.stdout.write("###############\n\n")
if not Path("Makefile").exists():
sys.stdout.write("Makefile does not exist, abort\n")
sys.exit(1)
create_config(prompt=False, preseeds=None)
update_parser = command_parsers.add_parser("update", help="Update Makefile")
update_parser.set_defaults(func=update_command)
def help_generator_command(args: argparse.Namespace):
sys.stdout.write("Help for Makefile\n")
makefile = Path("Makefile")
if not makefile.exists():
sys.stdout.write("Makefile does not exist, abort\n")
sys.exit(1)
print_help(makefile)
help_generator_parser = command_parsers.add_parser(
"help-generator", help="Help for Makefile"
)
help_generator_parser.set_defaults(func=help_generator_command)
##############################################################################
# main
##############################################################################
def main() -> None:
mxdev.setup_logger(logging.INFO)
args = parser.parse_args()
args.func(args)