Use specific version output per subcommand
Marco Ricci

Marco Ricci commited on 2025-02-14 15:45:36
Zeige 4 geänderte Dateien mit 311 Einfügungen und 87 Löschungen.


This allows the version output to only calculate the status information
relevant to this subcommand.

As a consequence, the tests now are subcommand-specific, and much less
amenable to deduplication/parametrization.
... ...
@@ -952,7 +952,7 @@ def validate_length(
952 952
     return int_value
953 953
 
954 954
 
955
-def version_option_callback(
955
+def common_version_output(
956 956
     ctx: click.Context,
957 957
     param: click.Parameter,
958 958
     value: bool,  # noqa: FBT001
... ...
@@ -960,32 +960,6 @@ def version_option_callback(
960 960
     del param
961 961
     if value and not ctx.resilient_parsing:
962 962
         major_dependencies: list[str] = []
963
-        derivation_schemes = {'vault': True}
964
-        foreign_configuration_formats = {
965
-            'vault storeroom': False,
966
-            'vault v0.2': False,
967
-            'vault v0.3': False,
968
-        }
969
-        known_extras = {
970
-            'export': False,
971
-        }
972
-        try:
973
-            from derivepassphrase.exporter import storeroom, vault_native  # noqa: I001,PLC0415
974
-
975
-            foreign_configuration_formats[
976
-                'vault storeroom'
977
-            ] = not storeroom.STUBBED
978
-            foreign_configuration_formats[
979
-                'vault v0.2'
980
-            ] = not vault_native.STUBBED
981
-            foreign_configuration_formats[
982
-                'vault v0.3'
983
-            ] = not vault_native.STUBBED
984
-            known_extras['export'] = (
985
-                not storeroom.STUBBED and not vault_native.STUBBED
986
-            )
987
-        except ModuleNotFoundError:  # pragma: no cover
988
-            pass
989 963
         try:
990 964
             cryptography_version = importlib.metadata.version('cryptography')
991 965
         except ModuleNotFoundError:
... ...
@@ -1011,24 +985,14 @@ def version_option_callback(
1011 985
                 ),
1012 986
                 color=ctx.color,
1013 987
             )
1014
-        click.echo()
1015
-        version_info_types = {
1016
-            _msg.Label.SUPPORTED_DERIVATION_SCHEMES: [
1017
-                k for k, v in derivation_schemes.items() if v
1018
-            ],
1019
-            _msg.Label.KNOWN_DERIVATION_SCHEMES: [
1020
-                k for k, v in derivation_schemes.items() if not v
1021
-            ],
1022
-            _msg.Label.SUPPORTED_FOREIGN_CONFIGURATION_FORMATS: [
1023
-                k for k, v in foreign_configuration_formats.items() if v
1024
-            ],
1025
-            _msg.Label.KNOWN_FOREIGN_CONFIGURATION_FORMATS: [
1026
-                k for k, v in foreign_configuration_formats.items() if not v
1027
-            ],
1028
-            _msg.Label.ENABLED_PEP508_EXTRAS: [
1029
-                k for k, v in known_extras.items() if v
1030
-            ],
1031
-        }
988
+
989
+
990
+def print_version_info_types(
991
+    version_info_types: dict[_msg.Label, list[str]],
992
+    /,
993
+    *,
994
+    ctx: click.Context,
995
+) -> None:
1032 996
     for message_label, item_list in version_info_types.items():
1033 997
         if item_list:
1034 998
             current_length = len(str(_msg.TranslatedString(message_label)))
... ...
@@ -1038,21 +1002,14 @@ def version_option_callback(
1038 1002
                 space = ' '
1039 1003
                 punctuation = '.' if i == n else ','
1040 1004
                 if (
1041
-                        current_length
1042
-                        + len(space)
1043
-                        + len(item)
1044
-                        + len(punctuation)
1005
+                    current_length + len(space) + len(item) + len(punctuation)
1045 1006
                     <= VERSION_OUTPUT_WRAPPING_WIDTH
1046 1007
                 ):
1047
-                        current_length += (
1048
-                            len(space) + len(item) + len(punctuation)
1049
-                        )
1008
+                    current_length += len(space) + len(item) + len(punctuation)
1050 1009
                     piece = f'{space}{item}{punctuation}'
1051 1010
                 else:
1052 1011
                     space = '    '
1053
-                        current_length = (
1054
-                            len(space) + len(item) + len(punctuation)
1055
-                        )
1012
+                    current_length = len(space) + len(item) + len(punctuation)
1056 1013
                     piece = f'\n{space}{item}{punctuation}'
1057 1014
                 formatted_item_list_pieces.append(piece)
1058 1015
             click.echo(
... ...
@@ -1065,10 +1022,118 @@ def version_option_callback(
1065 1022
                 ]),
1066 1023
                 color=ctx.color,
1067 1024
             )
1025
+
1026
+
1027
+def derivepassphrase_version_option_callback(
1028
+    ctx: click.Context,
1029
+    param: click.Parameter,
1030
+    value: bool,  # noqa: FBT001
1031
+) -> None:
1032
+    if value and not ctx.resilient_parsing:
1033
+        common_version_output(ctx, param, value)
1034
+        derivation_schemes = {'vault': True}
1035
+        supported_subcommands = {'export', 'vault'}
1036
+        click.echo()
1037
+        version_info_types = {
1038
+            _msg.Label.SUPPORTED_DERIVATION_SCHEMES: [
1039
+                k for k, v in derivation_schemes.items() if v
1040
+            ],
1041
+            _msg.Label.KNOWN_DERIVATION_SCHEMES: [
1042
+                k for k, v in derivation_schemes.items() if not v
1043
+            ],
1044
+            _msg.Label.SUPPORTED_SUBCOMMANDS: sorted(supported_subcommands),
1045
+        }
1046
+        print_version_info_types(version_info_types, ctx=ctx)
1068 1047
         ctx.exit()
1069 1048
 
1070 1049
 
1071
-def version_option(f: Callable[P, R]) -> Callable[P, R]:
1050
+def export_version_option_callback(
1051
+    ctx: click.Context,
1052
+    param: click.Parameter,
1053
+    value: bool,  # noqa: FBT001
1054
+) -> None:
1055
+    if value and not ctx.resilient_parsing:
1056
+        common_version_output(ctx, param, value)
1057
+        supported_subcommands = {'vault'}
1058
+        foreign_configuration_formats = {
1059
+            'vault storeroom': False,
1060
+            'vault v0.2': False,
1061
+            'vault v0.3': False,
1062
+        }
1063
+        click.echo()
1064
+        version_info_types = {
1065
+            _msg.Label.KNOWN_FOREIGN_CONFIGURATION_FORMATS: [
1066
+                k for k, v in foreign_configuration_formats.items() if not v
1067
+            ],
1068
+            _msg.Label.SUPPORTED_SUBCOMMANDS: sorted(supported_subcommands),
1069
+        }
1070
+        print_version_info_types(version_info_types, ctx=ctx)
1071
+        ctx.exit()
1072
+
1073
+
1074
+def export_vault_version_option_callback(
1075
+    ctx: click.Context,
1076
+    param: click.Parameter,
1077
+    value: bool,  # noqa: FBT001
1078
+) -> None:
1079
+    if value and not ctx.resilient_parsing:
1080
+        common_version_output(ctx, param, value)
1081
+        foreign_configuration_formats = {
1082
+            'vault storeroom': False,
1083
+            'vault v0.2': False,
1084
+            'vault v0.3': False,
1085
+        }
1086
+        known_extras = {
1087
+            'export': False,
1088
+        }
1089
+        try:
1090
+            from derivepassphrase.exporter import storeroom, vault_native  # noqa: I001,PLC0415
1091
+
1092
+            foreign_configuration_formats[
1093
+                'vault storeroom'
1094
+            ] = not storeroom.STUBBED
1095
+            foreign_configuration_formats[
1096
+                'vault v0.2'
1097
+            ] = not vault_native.STUBBED
1098
+            foreign_configuration_formats[
1099
+                'vault v0.3'
1100
+            ] = not vault_native.STUBBED
1101
+            known_extras['export'] = (
1102
+                not storeroom.STUBBED and not vault_native.STUBBED
1103
+            )
1104
+        except ModuleNotFoundError:  # pragma: no cover
1105
+            pass
1106
+        click.echo()
1107
+        version_info_types = {
1108
+            _msg.Label.SUPPORTED_FOREIGN_CONFIGURATION_FORMATS: [
1109
+                k for k, v in foreign_configuration_formats.items() if v
1110
+            ],
1111
+            _msg.Label.KNOWN_FOREIGN_CONFIGURATION_FORMATS: [
1112
+                k for k, v in foreign_configuration_formats.items() if not v
1113
+            ],
1114
+            _msg.Label.ENABLED_PEP508_EXTRAS: [
1115
+                k for k, v in known_extras.items() if v
1116
+            ],
1117
+        }
1118
+        print_version_info_types(version_info_types, ctx=ctx)
1119
+        ctx.exit()
1120
+
1121
+
1122
+def vault_version_option_callback(
1123
+    ctx: click.Context,
1124
+    param: click.Parameter,
1125
+    value: bool,  # noqa: FBT001
1126
+) -> None:
1127
+    if value and not ctx.resilient_parsing:
1128
+        common_version_output(ctx, param, value)
1129
+        ctx.exit()
1130
+
1131
+
1132
+def version_option(
1133
+    version_option_callback: Callable[
1134
+        [click.Context, click.Parameter, Any], Any
1135
+    ],
1136
+) -> Callable[[Callable[P, R]], Callable[P, R]]:
1072 1137
     return click.option(
1073 1138
         '--version',
1074 1139
         is_flag=True,
... ...
@@ -1077,7 +1142,7 @@ def version_option(f: Callable[P, R]) -> Callable[P, R]:
1077 1142
         callback=version_option_callback,
1078 1143
         cls=StandardOption,
1079 1144
         help=_msg.TranslatedString(_msg.Label.VERSION_OPTION_HELP_TEXT),
1080
-    )(f)
1145
+    )
1081 1146
 
1082 1147
 
1083 1148
 color_forcing_pseudo_option = click.option(
... ...
@@ -1327,6 +1327,15 @@ class Label(enum.Enum):
1327 1327
         'Supported foreign configuration formats:',
1328 1328
     )
1329 1329
     """"""
1330
+    SUPPORTED_SUBCOMMANDS = commented(
1331
+        'This is part of the version output, emitting lists of supported '
1332
+        'subcommands.  A comma-separated English list '
1333
+        'of items follows, with standard English punctuation.',
1334
+    )(
1335
+        'Label :: Info Message:: Table row header',
1336
+        'Supported subcommands:',
1337
+    )
1338
+    """"""
1330 1339
     CONFIRM_THIS_CHOICE_PROMPT_TEXT = commented(
1331 1340
         'There is no support for "yes" or "no" in other languages '
1332 1341
         'than English, so it is advised that your translation makes it '
... ...
@@ -58,7 +58,9 @@ VERSION = _internals.VERSION
58 58
         _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_03),
59 59
     ),
60 60
 )
61
-@cli_machinery.version_option
61
+@cli_machinery.version_option(
62
+    cli_machinery.derivepassphrase_version_option_callback
63
+)
62 64
 @cli_machinery.color_forcing_pseudo_option
63 65
 @cli_machinery.standard_logging_options
64 66
 @click.pass_context
... ...
@@ -113,7 +115,7 @@ def derivepassphrase(ctx: click.Context, /) -> None:
113 115
         _msg.TranslatedString(_msg.Label.DERIVEPASSPHRASE_EXPORT_03),
114 116
     ),
115 117
 )
116
-@cli_machinery.version_option
118
+@cli_machinery.version_option(cli_machinery.export_version_option_callback)
117 119
 @cli_machinery.color_forcing_pseudo_option
118 120
 @cli_machinery.standard_logging_options
119 121
 @click.pass_context
... ...
@@ -204,7 +206,9 @@ def derivepassphrase_export(ctx: click.Context, /) -> None:
204 206
     ),
205 207
     cls=cli_machinery.StandardOption,
206 208
 )
207
-@cli_machinery.version_option
209
+@cli_machinery.version_option(
210
+    cli_machinery.export_vault_version_option_callback
211
+)
208 212
 @cli_machinery.color_forcing_pseudo_option
209 213
 @cli_machinery.standard_logging_options
210 214
 @click.argument(
... ...
@@ -610,7 +614,7 @@ def derivepassphrase_export_vault(
610 614
     ),
611 615
     cls=cli_machinery.CompatibilityOption,
612 616
 )
613
-@cli_machinery.version_option
617
+@cli_machinery.version_option(cli_machinery.vault_version_option_callback)
614 618
 @cli_machinery.color_forcing_pseudo_option
615 619
 @cli_machinery.standard_logging_options
616 620
 @click.argument(
... ...
@@ -85,6 +85,7 @@ class VersionOutputData(NamedTuple):
85 85
     derivation_schemes: dict[str, bool]
86 86
     foreign_configuration_formats: dict[str, bool]
87 87
     extras: frozenset[str]
88
+    subcommands: frozenset[str]
88 89
 
89 90
 
90 91
 PASSPHRASE_GENERATION_OPTIONS: list[tuple[str, ...]] = [
... ...
@@ -349,15 +350,15 @@ def parse_version_output(  # noqa: C901
349 350
     The version output contains two paragraphs.  The first paragraph
350 351
     details the version number, and the version number of any major
351 352
     libraries in use.  The second paragraph details known and supported
352
-    passphrase derivation schemes, foreign configuration formats, and
353
-    PEP 508 package extras.  For the schemes and formats, there is
354
-    a "supported" line for supported items, and a "known" line for known
355
-    but currently unsupported items (usually because of missing
356
-    dependencies), either of which may be empty and thus omitted.  For
357
-    extras, only active items are shown, and there is a separate message
358
-    for the "no extras active" case.  Item lists may be spilled across
359
-    multiple lines, but only at item boundaries, and the continuation
360
-    lines are then indented.
353
+    passphrase derivation schemes, foreign configuration formats,
354
+    subcommands and PEP 508 package extras.  For the schemes and
355
+    formats, there is a "supported" line for supported items, and
356
+    a "known" line for known but currently unsupported items (usually
357
+    because of missing dependencies), either of which may be empty and
358
+    thus omitted.  For extras, only active items are shown, and there is
359
+    a separate message for the "no extras active" case.  Item lists may
360
+    be spilled across multiple lines, but only at item boundaries, and
361
+    the continuation lines are then indented.
361 362
 
362 363
     Args:
363 364
         text:
... ...
@@ -389,8 +390,8 @@ def parse_version_output(  # noqa: C901
389 390
     if paragraph:  # pragma: no branch
390 391
         paragraphs.append(paragraph.copy())
391 392
         paragraph.clear()
392
-    assert len(paragraphs) == 2, (
393
-        f'expected exactly two lines of version output: {paragraphs!r}'
393
+    assert paragraphs, (
394
+        f'expected at least one paragraph of version output: {paragraphs!r}'
394 395
     )
395 396
     assert prog_name is None or prog_name in paragraphs[0][0], (
396 397
         f'first version output line should mention '
... ...
@@ -400,9 +401,17 @@ def parse_version_output(  # noqa: C901
400 401
         f'first version output line should mention the version number '
401 402
         f'{version}: {paragraphs[0][0]!r}'
402 403
     )
403
-    schemas: dict[str, bool] = {}
404
+    schemes: dict[str, bool] = {}
404 405
     formats: dict[str, bool] = {}
406
+    subcommands: set[str] = set()
405 407
     extras: set[str] = set()
408
+    if len(paragraphs) < 2:
409
+        return VersionOutputData(
410
+            derivation_schemes=schemes,
411
+            foreign_configuration_formats=formats,
412
+            subcommands=frozenset(subcommands),
413
+            extras=frozenset(extras),
414
+        )
406 415
     for line in paragraphs[1]:
407 416
         line_type, _, value = line.partition(':')
408 417
         if line_type == line:
... ...
@@ -416,16 +425,23 @@ def parse_version_output(  # noqa: C901
416 425
             elif line_type == 'Known foreign configuration formats':
417 426
                 formats[item] = False
418 427
             elif line_type == 'Supported derivation schemes':
419
-                schemas[item] = True
428
+                schemes[item] = True
420 429
             elif line_type == 'Known derivation schemes':
421
-                schemas[item] = False
430
+                schemes[item] = False
431
+            elif line_type == 'Supported subcommands':
432
+                subcommands.add(item)
422 433
             elif line_type == 'PEP 508 extras':
423 434
                 extras.add(item)
424 435
             else:
425 436
                 raise AssertionError(  # noqa: TRY003
426 437
                     f'Unknown version info line type: {line_type!r}'  # noqa: EM102
427 438
                 )
428
-    return VersionOutputData(schemas, formats, frozenset(extras))
439
+    return VersionOutputData(
440
+        derivation_schemes=schemes,
441
+        foreign_configuration_formats=formats,
442
+        subcommands=frozenset(subcommands),
443
+        extras=frozenset(extras),
444
+    )
429 445
 
430 446
 
431 447
 def bash_format(item: click.shell_completion.CompletionItem) -> str:
... ...
@@ -1451,7 +1467,6 @@ class Parametrize(types.SimpleNamespace):
1451 1467
 derivepassphrase 0.4.0
1452 1468
 Using cryptography 44.0.0
1453 1469
 
1454
-Supported derivation schemes: vault.
1455 1470
 Supported foreign configuration formats: vault storeroom, vault v0.2,
1456 1471
     vault v0.3.
1457 1472
 PEP 508 extras: export.
... ...
@@ -1459,12 +1474,13 @@ PEP 508 extras: export.
1459 1474
                 'derivepassphrase',
1460 1475
                 '0.4.0',
1461 1476
                 VersionOutputData(
1462
-                    derivation_schemes={'vault': True},
1477
+                    derivation_schemes={},
1463 1478
                     foreign_configuration_formats={
1464 1479
                         'vault storeroom': True,
1465 1480
                         'vault v0.2': True,
1466 1481
                         'vault v0.3': True,
1467 1482
                     },
1483
+                    subcommands=frozenset(),
1468 1484
                     extras=frozenset({'export'}),
1469 1485
                 ),
1470 1486
                 id='derivepassphrase-0.4.0-export',
... ...
@@ -1475,6 +1491,7 @@ derivepassphrase 0.5
1475 1491
 
1476 1492
 Supported derivation schemes: vault.
1477 1493
 Known foreign configuration formats: vault storeroom, vault v0.2, vault v0.3.
1494
+Supported subcommands: export, vault.
1478 1495
 No PEP 508 extras are active.
1479 1496
 """,
1480 1497
                 'derivepassphrase',
... ...
@@ -1486,6 +1503,7 @@ No PEP 508 extras are active.
1486 1503
                         'vault v0.2': False,
1487 1504
                         'vault v0.3': False,
1488 1505
                     },
1506
+                    subcommands=frozenset({'export', 'vault'}),
1489 1507
                     extras=frozenset({}),
1490 1508
                 ),
1491 1509
                 id='derivepassphrase-0.5-plain',
... ...
@@ -1506,6 +1524,7 @@ Known derivation schemes: divination, /dev/random,
1506 1524
 Supported foreign configuration formats: derivepassphrase, nonsense.
1507 1525
 Known foreign configuration formats: divination v3.141592,
1508 1526
     /dev/random.
1527
+Supported subcommands: delete-all-files, dump-core.
1509 1528
 PEP 508 extras: annoying-popups, delete-all-files,
1510 1529
     dump-core-depending-on-the-phase-of-the-moon.
1511 1530
 
... ...
@@ -1528,6 +1547,7 @@ PEP 508 extras: annoying-popups, delete-all-files,
1528 1547
                         'divination v3.141592': False,
1529 1548
                         '/dev/random': False,
1530 1549
                     },
1550
+                    subcommands=frozenset({'delete-all-files', 'dump-core'}),
1531 1551
                     extras=frozenset({
1532 1552
                         'annoying-popups',
1533 1553
                         'delete-all-files',
... ...
@@ -1778,20 +1798,19 @@ class TestAllCLI:
1778 1798
             'Expected no color, but found an ANSI control sequence'
1779 1799
         )
1780 1800
 
1781
-    @Parametrize.COMMAND_NON_EAGER_ARGUMENTS
1782
-    def test_202_version_option_output(
1801
+    def test_202a_derivepassphrase_version_option_output(
1783 1802
         self,
1784
-        command: list[str],
1785
-        non_eager_arguments: list[str],
1786 1803
     ) -> None:
1787 1804
         """The version output states supported features.
1788 1805
 
1789 1806
         The version output is parsed using [`parse_version_output`][].
1790 1807
         Format examples can be found in
1791
-        [`Parametrize.VERSION_OUTPUT_DATA`][].
1808
+        [`Parametrize.VERSION_OUTPUT_DATA`][].  Specifically, for the
1809
+        top-level `derivepassphrase` command, the output should contain
1810
+        the known and supported derivation schemes, and a list of
1811
+        subcommands.
1792 1812
 
1793 1813
         """
1794
-        del non_eager_arguments
1795 1814
         runner = click.testing.CliRunner(mix_stderr=False)
1796 1815
         # TODO(the-13th-letter): Rewrite using parenthesized
1797 1816
         # with-statements.
... ...
@@ -1806,15 +1825,103 @@ class TestAllCLI:
1806 1825
             )
1807 1826
             result_ = runner.invoke(
1808 1827
                 cli.derivepassphrase,
1809
-                [*command, '--version'],
1828
+                ['--version'],
1810 1829
                 catch_exceptions=False,
1811 1830
             )
1812 1831
             result = tests.ReadableResult.parse(result_)
1813 1832
         assert result.clean_exit(empty_stderr=True), 'expected clean exit'
1814 1833
         assert result.output.strip(), 'expected version output'
1815 1834
         version_data = parse_version_output(result.output)
1816
-        actually_known_formats: dict[str, bool] = {}
1817 1835
         actually_known_schemes = {'vault': True}
1836
+        subcommands = {'export', 'vault'}
1837
+        assert version_data.derivation_schemes == actually_known_schemes
1838
+        assert not version_data.foreign_configuration_formats
1839
+        assert version_data.subcommands == subcommands
1840
+        assert not version_data.extras
1841
+
1842
+    def test_202b_export_version_option_output(
1843
+        self,
1844
+    ) -> None:
1845
+        """The version output states supported features.
1846
+
1847
+        The version output is parsed using [`parse_version_output`][].
1848
+        Format examples can be found in
1849
+        [`Parametrize.VERSION_OUTPUT_DATA`][].  Specifically, for the
1850
+        `export` command, the output should contain the known foreign
1851
+        configuration formats (but not marked as supported), and a list
1852
+        of subcommands.
1853
+
1854
+        """
1855
+        runner = click.testing.CliRunner(mix_stderr=False)
1856
+        # TODO(the-13th-letter): Rewrite using parenthesized
1857
+        # with-statements.
1858
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1859
+        with contextlib.ExitStack() as stack:
1860
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1861
+            stack.enter_context(
1862
+                tests.isolated_config(
1863
+                    monkeypatch=monkeypatch,
1864
+                    runner=runner,
1865
+                )
1866
+            )
1867
+            result_ = runner.invoke(
1868
+                cli.derivepassphrase,
1869
+                ['export', '--version'],
1870
+                catch_exceptions=False,
1871
+            )
1872
+            result = tests.ReadableResult.parse(result_)
1873
+        assert result.clean_exit(empty_stderr=True), 'expected clean exit'
1874
+        assert result.output.strip(), 'expected version output'
1875
+        version_data = parse_version_output(result.output)
1876
+        actually_known_formats: dict[str, bool] = {
1877
+            'vault storeroom': False,
1878
+            'vault v0.2': False,
1879
+            'vault v0.3': False,
1880
+        }
1881
+        subcommands = {'vault'}
1882
+        assert not version_data.derivation_schemes
1883
+        assert (
1884
+            version_data.foreign_configuration_formats
1885
+            == actually_known_formats
1886
+        )
1887
+        assert version_data.subcommands == subcommands
1888
+        assert not version_data.extras
1889
+
1890
+    def test_202c_export_vault_version_option_output(
1891
+        self,
1892
+    ) -> None:
1893
+        """The version output states supported features.
1894
+
1895
+        The version output is parsed using [`parse_version_output`][].
1896
+        Format examples can be found in
1897
+        [`Parametrize.VERSION_OUTPUT_DATA`][].  Specifically, for the
1898
+        `export vault` subcommand, the output should contain the
1899
+        vault-specific subset of the known or supported foreign
1900
+        configuration formats, and a list of available PEP 508 extras.
1901
+
1902
+        """
1903
+        runner = click.testing.CliRunner(mix_stderr=False)
1904
+        # TODO(the-13th-letter): Rewrite using parenthesized
1905
+        # with-statements.
1906
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1907
+        with contextlib.ExitStack() as stack:
1908
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1909
+            stack.enter_context(
1910
+                tests.isolated_config(
1911
+                    monkeypatch=monkeypatch,
1912
+                    runner=runner,
1913
+                )
1914
+            )
1915
+            result_ = runner.invoke(
1916
+                cli.derivepassphrase,
1917
+                ['export', 'vault', '--version'],
1918
+                catch_exceptions=False,
1919
+            )
1920
+            result = tests.ReadableResult.parse(result_)
1921
+        assert result.clean_exit(empty_stderr=True), 'expected clean exit'
1922
+        assert result.output.strip(), 'expected version output'
1923
+        version_data = parse_version_output(result.output)
1924
+        actually_known_formats: dict[str, bool] = {}
1818 1925
         actually_enabled_extras: set[str] = set()
1819 1926
         with contextlib.suppress(ModuleNotFoundError):
1820 1927
             from derivepassphrase.exporter import storeroom, vault_native  # noqa: I001,PLC0415
... ...
@@ -1826,13 +1933,52 @@ class TestAllCLI:
1826 1933
             })
1827 1934
             if not storeroom.STUBBED and not vault_native.STUBBED:
1828 1935
                 actually_enabled_extras.add('export')
1936
+        assert not version_data.derivation_schemes
1829 1937
         assert (
1830 1938
             version_data.foreign_configuration_formats
1831 1939
             == actually_known_formats
1832 1940
         )
1833
-        assert version_data.derivation_schemes == actually_known_schemes
1941
+        assert not version_data.subcommands
1834 1942
         assert version_data.extras == actually_enabled_extras
1835 1943
 
1944
+    def test_202d_vault_version_option_output(
1945
+        self,
1946
+    ) -> None:
1947
+        """The version output states supported features.
1948
+
1949
+        The version output is parsed using [`parse_version_output`][].
1950
+        Format examples can be found in
1951
+        [`Parametrize.VERSION_OUTPUT_DATA`][].  Specifically, for the
1952
+        vault command, the output should not contain anything beyond the
1953
+        first paragraph.
1954
+
1955
+        """
1956
+        runner = click.testing.CliRunner(mix_stderr=False)
1957
+        # TODO(the-13th-letter): Rewrite using parenthesized
1958
+        # with-statements.
1959
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1960
+        with contextlib.ExitStack() as stack:
1961
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1962
+            stack.enter_context(
1963
+                tests.isolated_config(
1964
+                    monkeypatch=monkeypatch,
1965
+                    runner=runner,
1966
+                )
1967
+            )
1968
+            result_ = runner.invoke(
1969
+                cli.derivepassphrase,
1970
+                ['vault', '--version'],
1971
+                catch_exceptions=False,
1972
+            )
1973
+            result = tests.ReadableResult.parse(result_)
1974
+        assert result.clean_exit(empty_stderr=True), 'expected clean exit'
1975
+        assert result.output.strip(), 'expected version output'
1976
+        version_data = parse_version_output(result.output)
1977
+        assert not version_data.derivation_schemes
1978
+        assert not version_data.foreign_configuration_formats
1979
+        assert not version_data.subcommands
1980
+        assert not version_data.extras
1981
+
1836 1982
 
1837 1983
 class TestCLI:
1838 1984
     """Tests for the `derivepassphrase vault` command-line interface."""
1839 1985