Path:
tasks.py
Lines:
1260
Non-empty lines:
1105
Non-empty lines covered with requirements:
1105 / 1105 (100.0%)
Functions:
43
Functions covered by requirements:
43 / 43 (100.0%)
1
import os
2
import re
3
import shutil
4
import sys
5
import tempfile
6
from enum import Enum
7
from pathlib import Path
8
from typing import Dict, Optional
9
10
import invoke
11
from invoke import task
12
13
from developer.git.commit_validator import (
14
validate_commits_locally_or_ci,
15
)16
from strictdoc.core.environment import (
17
BINARY_HTML_TEMPLATES_DIR,
18
HTML_TEMPLATE_DIRS,
19
)20
21
# Specifying encoding because Windows crashes otherwise when running Invoke22
# tasks below:23
# UnicodeEncodeError: 'charmap' codec can't encode character '\ufffd'24
# in position 16: character maps to <undefined>25
# People say, it might also be possible to export PYTHONIOENCODING=utf8 but this26
# seems to work.27
# FIXME: If you are a Windows user and expert, please advise on how to do this28
# properly.29
sys.stdout = open(1, "w", encoding="utf-8", closefd=False, buffering=1)
30
31
STRICTDOC_TMP_DIR = os.path.join(tempfile.gettempdir(), "strictdoc_tmp_dir")
32
TEST_REPORTS_DIR = "build/test_reports"
33
34
35
def get_pyinstaller_html_template_data_options() -> str:
36
return "\n".join(
37
f'--add-data "{template_dir}{os.pathsep}{BINARY_HTML_TEMPLATES_DIR}"'
38
for template_dir in HTML_TEMPLATE_DIRS
39
)40
41
42
def get_nuitka_html_template_data_options() -> str:
43
return "\n".join(
44
f'--include-data-dir="{template_dir}={BINARY_HTML_TEMPLATES_DIR}"'
45
for template_dir in HTML_TEMPLATE_DIRS
46
)47
48
49
# To prevent all tasks from building to the same virtual environment.50
# All values correspond to the configuration in the tox.ini config file.51
class ToxEnvironment(str, Enum):
52
DEVELOPMENT = "development"
53
CHECK = "check"
54
DOCUMENTATION = "documentation"
55
RELEASE = "release"
56
RELEASE_LOCAL = "release-local"
57
PYINSTALLER = "pyinstaller"
58
59
60
def run_invoke(
61
context,
62
cmd,
63
environment: Optional[dict] = None,
64
pty: bool = False,
65
warn: bool = False,
66
) -> invoke.runners.Result:
67
def one_line_command(string):
68
return re.sub("\\s+", " ", string).strip()
69
70
return context.run(
71
one_line_command(cmd),
72
env=environment,
73
hide=False,
74
warn=warn,
75
pty=pty,
76
echo=True,
77
)78
79
80
def run_invoke_with_tox(
81
context,
82
environment_type: ToxEnvironment,
83
command: str,
84
environment: Optional[Dict] = None,
85
pty: bool = False,
86
) -> invoke.runners.Result:
87
assert isinstance(environment_type, ToxEnvironment)
88
assert isinstance(command, str)
89
90
tox_py_version = f"py{sys.version_info.major}{sys.version_info.minor}"
91
92
return run_invoke(
93
context,
94
f"""
95
tox96
-e {tox_py_version}-{environment_type.value} --
97
{command}
98
""",
99
environment=environment,
100
pty=pty,
101
)102
103
104
@task(default=True)
105
def list_tasks(context):
106
clean_command = """
107
invoke --list108
"""109
run_invoke(context, clean_command)
110
111
112
@task113
def clean(context):
114
# https://unix.stackexchange.com/a/689930/77389115
clean_command = """
116
rm -rf output/ docs/sphinx/build/117
"""118
run_invoke(context, clean_command)
119
120
121
@task(aliases=["s"])
122
def server(context, input_path=".", config=None):
123
assert os.path.isdir(input_path), input_path
124
if config is not None:
125
assert os.path.isfile(config), config
126
config_argument = f"--config {config}" if config is not None else ""
127
run_invoke_with_tox(
128
context,
129
ToxEnvironment.DEVELOPMENT,
130
f"""
131
python -m strictdoc.cli.main132
--debug133
server {input_path} {config_argument}
134
--host 127.0.0.1135
--reload136
""",
137
)138
139
140
@task(aliases=["d"])
141
def docs(context):
142
run_invoke_with_tox(
143
context,
144
ToxEnvironment.DOCUMENTATION,
145
"""
146
python3 -m strictdoc.cli.main147
export .148
--formats=html149
--output-dir output/strictdoc_website150
--project-title "StrictDoc"151
""",
152
)153
154
run_invoke_with_tox(
155
context,
156
ToxEnvironment.DOCUMENTATION,
157
"""
158
python3 -m strictdoc.cli.main159
export ./160
--formats=rst161
--output-dir output/sphinx162
--project-title "StrictDoc"163
""",
164
)165
166
run_invoke_with_tox(
167
context,
168
ToxEnvironment.DOCUMENTATION,
169
"""
170
cp -r output/sphinx/rst/docs/* docs/sphinx/source/ &&171
cp -r output/sphinx/rst/docs_extra/* docs/sphinx/source/ &&172
mkdir -p docs/sphinx/source/_assets/ &&173
cp -v docs/_assets/* docs/sphinx/source/_assets/174
""",
175
)176
177
run_invoke_with_tox(
178
context,
179
ToxEnvironment.DOCUMENTATION,
180
"""
181
make --directory docs/sphinx html latexpdf SPHINXOPTS="-W --keep-going"182
""",
183
)184
185
run_invoke(
186
context,
187
(188
"""
189
open docs/sphinx/build/latex/strictdoc.pdf190
"""191
),192
)193
194
195
@task(aliases=["tus"])
196
def test_unit_server(context, focus=None):
197
focus_argument = f"-k {focus}" if focus is not None else ""
198
199
Path(TEST_REPORTS_DIR).mkdir(parents=True, exist_ok=True)
200
201
cwd = os.getcwd()
202
203
path_to_coverage_file = f"{cwd}/build/coverage/unit_server/.coverage"
204
205
run_invoke_with_tox(
206
context,
207
ToxEnvironment.CHECK,
208
f"""
209
coverage run210
--rcfile=.coveragerc.unit_server211
--data-file={path_to_coverage_file}
212
-m pytest213
tests/unit_server/214
{focus_argument}
215
--junit-xml={TEST_REPORTS_DIR}/tests_unit_server.pytest.junit.xml
216
-o junit_suite_name="StrictDoc Web Server Unit Tests"217
-o cache_dir=build/pytest_unit_server218
""",
219
)220
221
222
@task(test_unit_server, aliases=["tusc"])
223
def test_unit_server_report(context):
224
cwd = os.getcwd()
225
226
path_to_coverage_file = f"{cwd}/build/coverage/unit_server/.coverage"
227
228
run_invoke_with_tox(
229
context,
230
ToxEnvironment.CHECK,
231
f"""
232
coverage html233
--rcfile=.coveragerc.unit_server234
--data-file={path_to_coverage_file}
235
""",
236
)237
238
239
@task(aliases=["te"])
240
def test_end2end(
241
context,
242
*,
243
focus=None,
244
exit_first=False,
245
parallelize=False,
246
long_timeouts=False,
247
headless=False,
248
headed=False,
249
shard=None,
250
test_path=None,
251
coverage: bool = False,
252
):253
"""
254
@relation(SDOC-SRS-46, scope=function)255
"""256
257
environment = {}
258
259
coverage_command_or_none = ""
260
coverage_argument_or_none = ""
261
262
if coverage:
263
cwd = os.getcwd()
264
coverage_file_dir = f"{cwd}/build/coverage/end2end/"
265
coverage_file_dir2 = f"{cwd}/build/coverage/end2end_strictdoc/"
266
coverage_file = os.path.join(coverage_file_dir, ".coverage")
267
coverage_rc = os.path.join(cwd, ".coveragerc.end2end")
268
shutil.rmtree(coverage_file_dir, ignore_errors=True)
269
shutil.rmtree(coverage_file_dir2, ignore_errors=True)
270
coverage_command_or_none = f"""
271
coverage run272
--rcfile={coverage_rc}
273
--data-file={coverage_file}
274
-m275
"""276
coverage_argument_or_none = "--strictdoc-coverage"
277
278
long_timeouts_argument = (
279
"--strictdoc-long-timeouts" if long_timeouts else ""
280
)281
282
parallelize_argument = ""
283
if parallelize:
284
print( # noqa: T201
285
"warning: "286
"Running parallelized end-2-end tests is supported "287
"but is not stable."288
)289
parallelize_argument = "--numprocesses=2 --strictdoc-parallelize"
290
291
assert shard is None or re.match(r"[1-9][0-9]*/[1-9][0-9]*", shard), (
292
f"--shard argument has an incorrect format: {shard}."
293
)294
shard_argument = f"--strictdoc-shard={shard}" if shard else ""
295
296
focus_argument = f"-k {focus}" if focus is not None else ""
297
exit_first_argument = "--exitfirst" if exit_first else ""
298
headless_argument = "--headless2" if headless and not headed else "--gui"
299
test_command = f"""
300
{coverage_command_or_none}
301
pytest302
--failed-first303
--capture=no304
--reuse-session305
{parallelize_argument}
306
{shard_argument}
307
{coverage_argument_or_none}
308
{focus_argument}
309
{exit_first_argument}
310
{long_timeouts_argument}
311
{headless_argument}
312
--junit-xml={TEST_REPORTS_DIR}/tests_end2end.pytest.junit.xml
313
-o junit_suite_name="StrictDoc End-to-End Tests"314
-o cache_dir=build/pytest_end2end315
tests/end2end316
"""317
if test_path:
318
test_command = test_command.rstrip() + f"/{test_path}"
319
320
Path(TEST_REPORTS_DIR).mkdir(parents=True, exist_ok=True)
321
322
# On Windows, GitHub Actions fails with:323
# response = {'status': 500, 'value':324
# '{"value":{"error":"unknown error",325
# "message":"unknown error: cannot find Chrome binary", # noqa: ERA001326
# This very likely has to do with PATH isolation that Tox does.327
# FIXME: If you are a Windows expert, please fix this to run on Tox.328
if os.name == "nt":
329
run_invoke(context, test_command)
330
return331
332
run_invoke_with_tox(
333
context,
334
ToxEnvironment.CHECK,
335
test_command,
336
environment=environment,
337
)338
339
340
@task(aliases=["tu"])
341
def test_unit(context, coverage=False, focus=None, path=None, output=False):
342
"""
343
@relation(SDOC-SRS-44, scope=function)344
"""345
346
Path(TEST_REPORTS_DIR).mkdir(parents=True, exist_ok=True)
347
348
focus_argument = f"-k {focus}" if focus is not None else ""
349
output_argument = "--capture=no" if output else ""
350
351
cwd = os.getcwd()
352
353
if path is None:
354
path = "tests/unit"
355
else:
356
assert "tests/unit" in path, path
357
358
path_to_coverage_file = f"{cwd}/build/coverage/unit/.coverage"
359
360
pytest_command = (
361
"""
362
coverage run363
--rcfile=.coveragerc.unit364
--data-file={path_to_coverage_file}365
-m pytest366
"""367
if coverage
368
else "pytest"
369
)370
371
run_invoke_with_tox(
372
context,
373
ToxEnvironment.CHECK,
374
f"""
375
{pytest_command}
376
{focus_argument}
377
{output_argument}
378
--junit-xml={TEST_REPORTS_DIR}/tests_unit.pytest.junit.xml
379
-o cache_dir=build/pytest_unit_with_coverage380
-o junit_suite_name="StrictDoc Unit Tests"381
-p no:seleniumbase382
{path}
383
""",
384
)385
if coverage and not focus and path == "tests/unit":
386
run_invoke_with_tox(
387
context,
388
ToxEnvironment.CHECK,
389
f"""
390
coverage report391
--sort=cover392
--rcfile=.coveragerc.unit393
--data-file={path_to_coverage_file}
394
""",
395
)396
397
398
@task(test_unit, aliases=["tuc"])
399
def test_unit_report(context):
400
cwd = os.getcwd()
401
402
path_to_coverage_file = f"{cwd}/build/coverage/unit/.coverage"
403
404
run_invoke_with_tox(
405
context,
406
ToxEnvironment.CHECK,
407
f"""
408
coverage html409
--rcfile=.coveragerc.unit410
--data-file={path_to_coverage_file}
411
""",
412
)413
414
415
@task(aliases=["ti"])
- "15.8.2. CLI interface black-box integration testing" (REQUIREMENT)
416
def test_integration(
417
context,
418
focus=None,
419
debug=False,
420
no_parallelization=False,
421
fail_first=False,
422
coverage=False,
423
strictdoc=None,
424
html2pdf=False,
425
shard=None,
426
environment=ToxEnvironment.CHECK,
427
):428
"""
429
@relation(SDOC-SRS-45, scope=function)430
"""431
432
cwd = os.getcwd()
433
434
if strictdoc is None:
435
strictdoc_exec = "python3 -m strictdoc.cli.main"
436
else:
437
strictdoc_exec = strictdoc
438
439
coverage_path_argument = ""
440
if coverage:
441
path_to_coverage_rc = f"{cwd}/.coveragerc.integration"
442
strictdoc_exec = (
443
f"coverage run --rcfile={path_to_coverage_rc} -m strictdoc.cli.main"
444
)445
if html2pdf:
446
path_to_coverage_dir = f"{cwd}/build/coverage/integration_html2pdf/"
447
else:
448
path_to_coverage_dir = f"{cwd}/build/coverage/integration/"
449
path_to_coverage = os.path.join(path_to_coverage_dir, ".coverage")
450
shutil.rmtree(path_to_coverage_dir, ignore_errors=True)
451
coverage_path_argument = (
452
f'--param COVERAGE_FILE="{path_to_coverage}" '
453
f'--param COVERAGE_PROCESS_START="{path_to_coverage_rc}"'
454
)455
456
debug_opts = "-vv --show-all" if debug else ""
457
focus_or_none = f"--filter {focus}" if focus else ""
458
fail_first_argument = "--max-failures 1" if fail_first else ""
459
junit_xml_report_argument = (
460
"--xunit-xml-output build/test_reports/tests_integration_html2pdf.lit.junit.xml"461
if html2pdf
462
else "--xunit-xml-output build/test_reports/tests_integration.lit.junit.xml"
463
)464
465
# Allow partitioning of integration and html2pdf tests466
partition_opts = ""
467
if shard is not None:
468
match = re.match(r"([1-9][0-9]*)/([1-9][0-9]*)", shard)
469
assert match, f"--shard argument has an incorrect format: {shard}."
470
run_shard = int(match.group(1))
471
num_shards = int(match.group(2))
472
partition_opts = f"--num-shards={num_shards} --run-shard={run_shard}"
473
474
# HTML2PDF tests are running Chrome Driver which does not seem to be475
# parallelizable, or at least not in the way StrictDoc uses it.476
# If HTML2PDF option is provided, do not parallelize and only run the477
# HTML2PDF-specific tests.478
# HTML2PDF tests can be safely partitioned.479
chromedriver_param = ""
480
if not html2pdf:
481
parallelize_opts = "" if not no_parallelization else "--threads 1"
482
html2pdf_param = ""
483
test_folder = f"{cwd}/tests/integration"
484
test_output_dir = "build/tests_integration"
485
else:
486
parallelize_opts = "--threads 1"
487
html2pdf_param = "--param TEST_HTML2PDF=1"
488
chromedriver_path = os.environ.get("CHROMEWEBDRIVER")
489
if chromedriver_path is not None:
490
# NOTE: isfile() check does not work on GitHub Actions / Linux,491
# the exists() check works.492
assert os.path.exists(chromedriver_path), chromedriver_path
493
chromedriver_param = f"--param CHROMEDRIVER={os.path.join(chromedriver_path, 'chromedriver')}"
494
if os.name == "nt":
495
# On Windows, its chromdriver.exe496
chromedriver_param = chromedriver_param + ".exe"
497
test_folder = f"{cwd}/tests/integration/features/html2pdf"
498
test_output_dir = "build/tests_integration_html2pdf"
499
500
# The command sometimes exits with 1 even if the files are deleted.501
# warn=True ensures that the execution continues.502
run_invoke(
503
context,
504
f"""
505
rm -rf {test_output_dir}
506
""",
507
warn=True,
508
)509
510
run_invoke(
511
context,
512
f"""
513
rm -rf {STRICTDOC_TMP_DIR}
514
""",
515
)516
517
Path(STRICTDOC_TMP_DIR).mkdir(exist_ok=True)
518
Path(TEST_REPORTS_DIR).mkdir(parents=True, exist_ok=True)
519
520
itest_command = f"""
521
lit522
--param STRICTDOC_EXEC="{strictdoc_exec}"
523
--param STRICTDOC_TMP_DIR="{STRICTDOC_TMP_DIR}"
524
--param TEST_OUTPUT_DIR="{test_output_dir}"
525
--timeout 180526
--order smart527
{junit_xml_report_argument}
528
{coverage_path_argument}
529
{html2pdf_param}
530
{chromedriver_param}
531
-v532
{debug_opts}
533
{focus_or_none}
534
{fail_first_argument}
535
{parallelize_opts}
536
{partition_opts}
537
{test_folder}
538
"""539
540
# It looks like LIT does not open the RUN: subprocesses in the same541
# environment from which it itself is run from. This issue has been known by542
# us for a couple of years by now. Not using Tox on Windows for the time543
# being.544
if os.name == "nt":
545
run_invoke(context, itest_command)
546
return547
548
run_invoke_with_tox(
549
context,
550
environment,
551
itest_command,
552
environment={"STRICTDOC_CACHE_DIR": "Output/_cache"},
553
)554
555
556
@task557
def coverage_combine(context):
558
run_invoke_with_tox(
559
context,
560
ToxEnvironment.CHECK,
561
"""
562
coverage combine563
--data-file build/coverage/.coverage.combined564
--keep565
build/coverage/end2end_strictdoc/.coverage.*566
build/coverage/integration/.coverage.*567
build/coverage/integration_html2pdf/.coverage.*568
build/coverage/unit/.coverage569
build/coverage/unit_server/.coverage570
""",
571
)572
run_invoke_with_tox(
573
context,
574
ToxEnvironment.CHECK,
575
"""
576
coverage html577
--rcfile .coveragerc.combined578
--data-file build/coverage/.coverage.combined579
""",
580
)581
run_invoke_with_tox(
582
context,
583
ToxEnvironment.CHECK,
584
"""
585
coverage json586
--rcfile .coveragerc.combined587
--data-file build/coverage/.coverage.combined588
--pretty-print589
-o build/coverage/coverage.combined.json590
""",
591
)592
593
594
@task- "15.6.1. Compliance with Python community practices (PEP8 etc)" (REQUIREMENT)
595
def lint_ruff_format(context):
596
"""
597
@relation(SDOC-SRS-42, scope=function)598
"""599
600
result: invoke.runners.Result = run_invoke_with_tox(
601
context,
602
ToxEnvironment.CHECK,
603
"""
604
ruff605
format606
--cache-dir build/ruff607
*.py608
developer/609
docs/610
strictdoc/611
tools/ecss612
tests/unit/613
tests/unit_server/614
tests/integration/*.py615
tests/end2end/616
""",
617
)618
# Ruff always exits with 0, so we handle the output.619
if "reformatted" in result.stdout:
620
print("invoke: ruff format found issues") # noqa: T201
621
result.exited = 1
622
raise invoke.exceptions.UnexpectedExit(result)
623
624
625
@task(aliases=["lr"])
- "15.6.1. Compliance with Python community practices (PEP8 etc)" (REQUIREMENT)
626
def lint_ruff(context):
627
"""
628
@relation(SDOC-SRS-42, scope=function)629
"""630
631
run_invoke_with_tox(
632
context,
633
ToxEnvironment.CHECK,
634
"""
635
ruff check . --fix --exit-non-zero-on-fix --cache-dir build/ruff636
""",
637
)638
639
640
@task(aliases=["lm"])
- "15.5.2. Use of type annotations in Python code" (REQUIREMENT)
- "15.7.1. Static type checking" (REQUIREMENT)
641
def lint_mypy(context):
642
"""
643
@relation(SDOC-SRS-41, SDOC-SRS-43, scope=function)644
"""645
646
# These checks do not seem to be useful:647
# - import648
# --disallow-any-expr649
# --disallow-any-explicit650
# --disallow-any-unimported # noqa: ERA001651
# --disallow-any-decorated652
# - type-abstract. It is ignored on purpose because of assert_cast()653
# implementation. See https://stackoverflow.com/a/74073453/598057.654
run_invoke_with_tox(
655
context,
656
ToxEnvironment.CHECK,
657
"""
658
mypy docs/659
strictdoc/660
tests/unit/strictdoc/backend/sdoc_source_code/test_marker_lexer.py661
662
--show-error-codes663
--disable-error-code=import664
--disable-error-code=type-abstract665
--cache-dir=build/mypy666
--extra-checks667
668
--strict669
--strict-optional670
--strict-equality671
672
--check-untyped-defs673
--disallow-any-generics674
--disallow-incomplete-defs675
--disallow-subclassing-any676
--disallow-untyped-calls677
--disallow-untyped-decorators678
--disallow-untyped-defs679
--no-implicit-optional680
--warn-no-return681
--warn-redundant-casts682
--warn-return-any683
--warn-unreachable684
--warn-unused-ignores685
686
--python-version=3.10687
""",
688
)689
690
691
@task692
def lint_format_js(context):
693
# NOTE: Could not find the '--' equivalent for -w80.694
result: invoke.runners.Result = run_invoke_with_tox(
695
context,
696
ToxEnvironment.CHECK,
697
"""
698
js-beautify699
--indent-size=2700
--end-with-newline701
--replace702
-w100703
strictdoc/export/html/_static/static_html_search.js704
strictdoc/export/html/_static/stable_uri_forwarder.js705
""",
706
)707
# Ruff always exits with 0, so we handle the output.708
if "reformatted" in result.stdout:
709
print("invoke: ruff format found issues") # noqa: T201
710
result.exited = 1
711
raise invoke.exceptions.UnexpectedExit(result)
712
713
714
@task(aliases=["lc"])
715
def lint_commit(context): # noqa: ARG001
716
try:
717
validate_commits_locally_or_ci()
718
except ValueError as e:
719
raise invoke.exceptions.Exit(message=str(e), code=1) from None
720
721
722
@task(aliases=["lf"])
723
def lint_fixit(context, fix=False, auto=False, path="strictdoc/"):
724
if fix:
725
auto_argument = "--automatic" if auto else ""
726
run_invoke_with_tox(
727
context,
728
ToxEnvironment.CHECK,
729
f"""
730
fixit fix {path} {auto_argument}
731
""",
732
pty=True,
733
)734
else:
735
run_invoke_with_tox(
736
context,
737
ToxEnvironment.CHECK,
738
f"""
739
fixit lint --diff {path}
740
""",
741
)742
743
744
@task(aliases=["l"])
745
def lint(context):
746
lint_commit(context)
747
lint_ruff_format(context)
748
lint_ruff(context)
749
lint_mypy(context)
750
751
752
@task(aliases=["t"])
753
def test(context, shard=None):
754
test_unit(context)
755
test_unit_server(context)
756
test_integration(context, shard=shard)
757
758
759
@task(aliases=["ta"])
760
def test_all(context, coverage=False, headless=False):
761
test_unit(context, coverage=coverage)
762
test_unit_server(context)
763
test_integration(context, coverage=coverage)
764
test_integration(context, coverage=coverage, html2pdf=True)
765
test_end2end(context, coverage=coverage, headless=headless)
766
767
768
@task(aliases=["c"])
769
def check(context):
770
lint(context)
771
test(context)
772
773
774
# https://github.com/github-changelog-generator/github-changelog-generator775
# gem install github_changelog_generator776
@task777
def changelog(context, github_token):
778
# The alpha release tags are excluded from the changelog.779
command = f"""
780
github_changelog_generator781
--token {github_token}
782
--user strictdoc-project783
--exclude-tags-regex ".*a\\d+"
784
--project strictdoc785
"""786
run_invoke(context, command)
787
788
789
@task790
def check_dead_links(context):
791
run_invoke_with_tox(
792
context,
793
ToxEnvironment.CHECK,
794
"""
795
python3 tools/link_health.py docs/strictdoc_01_user_guide.sdoc796
""",
797
)798
run_invoke_with_tox(
799
context,
800
ToxEnvironment.CHECK,
801
"""
802
python3 tools/link_health.py docs/strictdoc_02_feature_map.sdoc803
""",
804
)805
run_invoke_with_tox(
806
context,
807
ToxEnvironment.CHECK,
808
"""
809
python3 tools/link_health.py docs/strictdoc_03_faq.sdoc810
""",
811
)812
run_invoke_with_tox(
813
context,
814
ToxEnvironment.CHECK,
815
"""
816
python3 tools/link_health.py docs/strictdoc_04_release_notes.sdoc817
""",
818
)819
run_invoke_with_tox(
820
context,
821
ToxEnvironment.CHECK,
822
"""
823
python3 tools/link_health.py docs/strictdoc_05_troubleshooting.sdoc824
""",
825
)826
run_invoke_with_tox(
827
context,
828
ToxEnvironment.CHECK,
829
"""
830
python3 tools/link_health.py docs/strictdoc_10_contributing.sdoc831
""",
832
)833
run_invoke_with_tox(
834
context,
835
ToxEnvironment.CHECK,
836
"""
837
python3 tools/link_health.py docs/strictdoc_11_developer_guide.sdoc838
""",
839
)840
run_invoke_with_tox(
841
context,
842
ToxEnvironment.CHECK,
843
"""
844
python3 tools/link_health.py docs/strictdoc_24_development_plan.sdoc845
""",
846
)847
run_invoke_with_tox(
848
context,
849
ToxEnvironment.CHECK,
850
"""
851
python3 tools/link_health.py docs/strictdoc_20_l1_system_requirements.sdoc852
""",
853
)854
run_invoke_with_tox(
855
context,
856
ToxEnvironment.CHECK,
857
"""
858
python3 tools/link_health.py docs/strictdoc_21_l2_high_level_requirements.sdoc859
""",
860
)861
run_invoke_with_tox(
862
context,
863
ToxEnvironment.CHECK,
864
"""
865
python3 tools/link_health.py docs/strictdoc_25_design.sdoc866
""",
867
)868
run_invoke_with_tox(
869
context,
870
ToxEnvironment.CHECK,
871
"""
872
python3 tools/link_health.py CONTRIBUTING.md873
""",
874
)875
run_invoke_with_tox(
876
context,
877
ToxEnvironment.CHECK,
878
"""
879
python3 tools/link_health.py NOTICE880
""",
881
)882
run_invoke_with_tox(
883
context,
884
ToxEnvironment.CHECK,
885
"""
886
python3 tools/link_health.py README.md887
""",
888
)889
890
891
@task892
def release_local(context):
893
run_invoke(
894
context,
895
"""
896
rm -rfv dist/ build/897
""",
898
)899
run_invoke(
900
context,
901
"""
902
pip uninstall strictdoc -y903
""",
904
)905
run_invoke_with_tox(
906
context,
907
ToxEnvironment.RELEASE_LOCAL,
908
"""
909
python -m build910
""",
911
)912
run_invoke_with_tox(
913
context,
914
ToxEnvironment.RELEASE_LOCAL,
915
"""
916
twine check dist/*917
""",
918
)919
run_invoke_with_tox(
920
context,
921
ToxEnvironment.RELEASE_LOCAL,
922
"""
923
pip install dist/*.tar.gz924
""",
925
)926
test_integration(
927
context, strictdoc="strictdoc", environment=ToxEnvironment.RELEASE_LOCAL
928
)929
930
931
@task932
def release(context, test_pypi=False, username=None, password=None):
933
"""
934
A release can be made to PyPI or test package index (TestPyPI):935
https://pypi.org/project/strictdoc/936
https://test.pypi.org/project/strictdoc/937
"""938
939
env_user = os.environ.get("TWINE_USERNAME")
940
env_pass = os.environ.get("TWINE_PASSWORD")
941
942
assert not ((username or password) and (env_user or env_pass)), (
943
username,
944
password,
945
env_user,
946
env_pass,
947
)948
assert (username and password) or (env_user and env_pass), (
949
username,
950
password,
951
env_user,
952
env_pass,
953
)954
if env_user:
955
assert env_user == "__token__"
956
957
repository_argument_or_none = ""
958
if username is not None and password is not None:
959
repository_argument_or_none = (
960
""961
if username
962
else (
963
"--repository strictdoc_test"964
if test_pypi
965
else "--repository strictdoc_release"
966
)967
)968
user_password = f"-u{username} -p{password}" if username is not None else ""
969
970
run_invoke(
971
context,
972
"""
973
rm -rfv dist/974
""",
975
)976
run_invoke_with_tox(
977
context,
978
ToxEnvironment.RELEASE,
979
"""
980
python3 -m build981
""",
982
)983
run_invoke_with_tox(
984
context,
985
ToxEnvironment.RELEASE,
986
"""
987
twine check dist/*988
""",
989
)990
# The token is in a core developer's .pypirc file.991
# https://test.pypi.org/manage/account/token/992
# https://packaging.python.org/en/latest/specifications/pypirc/#pypirc993
run_invoke_with_tox(
994
context,
995
ToxEnvironment.RELEASE,
996
f"""
997
twine upload dist/strictdoc-*.tar.gz dist/strictdoc-*.whl998
{repository_argument_or_none}
999
{user_password}
1000
""",
1001
)1002
1003
1004
@task1005
def release_pyinstaller(context):
1006
path_to_pyi_dist = "/tmp/strictdoc"
1007
html_template_data_options = get_pyinstaller_html_template_data_options()
1008
1009
# The --hidden-import strictdoc.server.app flag is needed because without1010
# it, the following is produced:1011
# ERROR: Error loading ASGI app. Could not import1012
# module "strictdoc.server.app".1013
# Solution found here: https://stackoverflow.com/a/71340437/5980571014
# This behavior is not surprising because that's how the uvicorn loads the1015
# application separately from the parent process.1016
#1017
# Compatibility modules can be imported by user-provided statistics1018
# generators at runtime. PyInstaller cannot discover these imports1019
# statically because the generators live outside of StrictDoc's package.1020
command = f"""
1021
pyinstaller1022
--clean1023
--name strictdoc1024
--noconfirm1025
--additional-hooks-dir developer/pyinstaller_hooks1026
--distpath {path_to_pyi_dist}
1027
--hidden-import strictdoc.core.statistics.metric1028
--hidden-import strictdoc.export.html.generators.project_statistics1029
--hidden-import strictdoc.export.html.generators.view_objects.project_statistics_view_object1030
--hidden-import strictdoc.export.html.generators.view_objects.project_tree_stats1031
--hidden-import strictdoc.export.rst.strictdoc_lexer1032
--hidden-import strictdoc.server.app1033
{html_template_data_options}
1034
--add-data strictdoc/export/rst/templates:templates/rst1035
--add-data strictdoc/export/html/_static:_static1036
--add-data strictdoc/export/html/_static_extra:_static_extra1037
strictdoc/cli/main.py1038
"""1039
1040
run_invoke_with_tox(
1041
context,
1042
ToxEnvironment.PYINSTALLER,
1043
"""
1044
pyinstaller --version1045
""",
1046
)1047
1048
run_invoke_with_tox(context, ToxEnvironment.PYINSTALLER, command)
1049
1050
1051
@task1052
def watch(context, sdocs_path="."):
1053
strictdoc_command = f"""
1054
python -m strictdoc.cli.main1055
export1056
{sdocs_path}
1057
--output-dir output/1058
"""1059
1060
run_invoke_with_tox(
1061
context,
1062
ToxEnvironment.DEVELOPMENT,
1063
f"""
1064
{strictdoc_command}
1065
""",
1066
)1067
1068
paths_to_watch = "."
1069
run_invoke_with_tox(
1070
context,
1071
ToxEnvironment.DEVELOPMENT,
1072
f"""
1073
watchmedo shell-command1074
--patterns="*.py;*.sdoc;*.jinja;*.html;*.css;*.js"1075
--recursive1076
--ignore-pattern='output/;tests/integration'1077
--command='{strictdoc_command}'
1078
--drop1079
{paths_to_watch}
1080
""",
1081
)1082
1083
1084
@task1085
def run(context, command):
1086
run_invoke_with_tox(
1087
context,
1088
ToxEnvironment.DEVELOPMENT,
1089
f"""
1090
{command}
1091
""",
1092
)1093
1094
1095
@task1096
def nuitka(context):
1097
html_template_data_options = get_nuitka_html_template_data_options()
1098
1099
run_invoke(
1100
context,
1101
f"""
1102
PYTHONPATH="{os.getcwd()}"
1103
python -m nuitka1104
--static-libpython=no1105
--standalone1106
--include-module=textx1107
--include-module=strictdoc.server.app1108
--include-module=docutils1109
--include-module=docutils.readers.standalone1110
--include-module=docutils.parsers.rst1111
{html_template_data_options}
1112
--include-data-dir=strictdoc/export/rst/templates=templates/rst1113
--include-data-dir=strictdoc/export/html/_static=_static1114
--include-data-dir=strictdoc/export/html/_static_extra/mathjax=_static_extra/mathjax1115
--include-package-data=docutils1116
strictdoc/cli/main.py1117
""",
1118
)1119
1120
1121
# https://github.com/jrfonseca/gprof2dot1122
# pip install gprof2dot1123
@task()
1124
def performance(context):
1125
command = """
1126
python -m cProfile -o output/profile.prof1127
-m strictdoc.cli.main export . --no-parallelization &&1128
gprof2dot -f pstats output/profile.prof | dot -Tpng -o output/output.png1129
"""1130
run_invoke(context, command)
1131
1132
1133
@task(performance)
1134
def performance_snakeviz(context):
1135
command = """
1136
snakeviz output/profile.prof1137
"""1138
run_invoke(context, command)
1139
1140
1141
@task(aliases=["bd"])
1142
def build_docker(
1143
context,
1144
image: str = "strictdoc:latest",
1145
no_cache: bool = False,
1146
source="pypi",
1147
):1148
no_cache_argument = "--no-cache" if no_cache else ""
1149
run_invoke(
1150
context,
1151
f"""
1152
docker build .1153
--build-arg STRICTDOC_SOURCE={source}
1154
-t {image}
1155
{no_cache_argument}
1156
""",
1157
)1158
1159
1160
@task(aliases=["rd"])
1161
def run_docker(
1162
context, image: str = "strictdoc:latest", command: Optional[str] = None
1163
):1164
command_argument = (
1165
f'/bin/bash -c "{command}"' if command is not None else ""
1166
)1167
1168
run_invoke(
1169
context,
1170
f"""
1171
docker run1172
--name strictdoc1173
--rm1174
-it1175
-e HOST_UID=$(id -u) -e HOST_GID=$(id -g)1176
-v "$(pwd):/data"1177
{image}
1178
{command_argument}
1179
""",
1180
pty=True,
1181
)1182
1183
1184
@task(aliases=["td"])
1185
def test_docker(context, image: str = "strictdoc:latest"):
1186
run_invoke(
1187
context,
1188
"""
1189
rm -rf output/ && mkdir -p output/ && chmod 777 output/1190
""",
1191
)1192
run_docker(
1193
context,
1194
image=image,
1195
command="strictdoc export --formats=html,html2pdf .",
1196
)1197
1198
def check_file_owner(filepath):
1199
import pwd # noqa: PLC0415
1200
1201
file_owner = pwd.getpwuid(os.stat(filepath).st_uid).pw_name
1202
current_user = os.environ.get("USER", "")
1203
return file_owner == current_user
1204
1205
assert check_file_owner(
1206
"output/html2pdf/pdf/docs/strictdoc_01_user_guide.pdf"1207
)1208
1209
1210
@task(aliases=["q"])
1211
def qualification(context):
1212
test_all(context, coverage=True, headless=True)
1213
coverage_combine(context)
1214
1215
1216
@task()
1217
def drawio(context):
1218
if sys.platform == "darwin":
1219
path_to_drawio = "/Applications/draw.io.app/Contents/MacOS/draw.io"
1220
elif sys.platform.startswith("linux"):
1221
path_to_drawio = "drawio"
1222
else:
1223
raise NotImplementedError(
1224
"drawio task is supported only on macOS and Linux."1225
)1226
1227
artifacts = [
1228
(1229
"developer/drawio/Architecture.drawio",
1230
"docs/_assets/StrictDoc_Workspace-Architecture.drawio.png",
1231
),1232
(1233
"developer/drawio/Backlog.drawio",
1234
"docs/_assets/StrictDoc_Workspace-Backlog.drawio.png",
1235
),1236
(1237
"developer/drawio/Roadmap.drawio",
1238
"docs/_assets/StrictDoc_Workspace-Roadmap.drawio.png",
1239
),1240
]1241
1242
for path_to_drawio_, path_to_png_ in artifacts:
1243
print(f"Copying: {path_to_drawio_} -> {path_to_png_}") # noqa: T201
1244
1245
# Basic safety for now to avoid writing wrong files.1246
assert os.path.isfile(path_to_drawio_), path_to_drawio_
1247
assert os.path.isfile(path_to_png_), path_to_png_
1248
1249
run_invoke(
1250
context,
1251
f"""
1252
{path_to_drawio}
1253
--export1254
--format png1255
-o {path_to_png_}
1256
--page-index 01257
{path_to_drawio_}
1258
""",
1259
pty=True,
1260
)