Marco Ricci commited on 2025-01-07 12:10:27
              Zeige 6 geänderte Dateien mit 916 Einfügungen und 52 Löschungen.
            
We move the shell completion code in `derivepassphrase/cli.py` to a new section and retire the specific handling of `VAULT_PATH` for the two path completion functions. Having actually tried this out interactively in Bash, it does not work that well if we complete filenames, partial filenames, and fixed strings in the same completion function: you would have to set conflicting completion options for this function. It *also* does not work at all with `click`'s stock Bash completion script, which would then discard either the fixed strings and partial filenames or the complete filenames, depending on the order they are emitted in by our shell completion code. So abandon the completion handling of `VAULT_PATH` in favor of standard filename completion, for paths. Every supported shell has further limitations on which inputs it can properly deserialize: Bash strips NUL characters from command substitutions, and Fish (v3) breaks in most situations involving newlines. Furthermore, the stock shell completion scripts add their own additional limitations: all shells use newline-terminated messages, so embedded newlines in the completion item (type, value, help text) cause (generally silent) parsing failures; and the Zsh completion functions in particular parse completions as "name:description" pairs, and thus need colons in the name to be escaped. We fix the colon handling of the Zsh completion script by providing a fixed serialization handler for Zsh. We avoid all of the other aforementioned issues by not returning any service names containing ASCII control characters as completion items. We also warn the user upon importing or configuring such a service that its service name will not be available for shell completion. Finally, we document this new warning in the manpage and the completion behavior in the changelog, and add tests for (the Python side of) the completion machinery, dependent on the current serialization format. References: [fish-shell#10874](https://github.com/fish-shell/fish-shell/issues/10874), [fish-shell#9693](https://github.com/fish-shell/fish-shell/issues/9693), [fish-shell#751](https://github.com/fish-shell/fish-shell/issues/751), [fish-shell#10651](https://github.com/fish-shell/fish-shell/issues/10651), [fish-shell#9847](https://github.com/fish-shell/fish-shell/issues/9847), [click#2703](https://github.com/pallets/click/issues/2703).
| ... | ... | 
                      @@ -1,5 +1,11 @@  | 
                  
| 1 | 1 | 
                        ### Added  | 
                    
| 2 | 2 | 
                         | 
                    
| 3 | 3 | 
                        - `derivepassphrase` now explicitly supports shell completion, in  | 
                    
| 4 | 
                        - particular including filename and service name completion in the `export  | 
                    |
| 5 | 
                        - vault` and `vault` subcommands.  | 
                    |
| 4 | 
                        + particular filename and service name completion in the `export vault`  | 
                    |
| 5 | 
                        + and `vault` subcommands.  | 
                    |
| 6 | 
                        +  | 
                    |
| 7 | 
                        + However, because of restrictions regarding the exchange of data between  | 
                    |
| 8 | 
                        + `derivepassphrase` and the shell, `derivepassphrase` will not offer any  | 
                    |
| 9 | 
                        + service names containing ASCII control characters for completion, and  | 
                    |
| 10 | 
                        + a warning will be issued when importing or configuring such a service.  | 
                    |
| 11 | 
                        + They may still otherwise be used normally.  | 
                    
| ... | ... | 
                      @@ -1,4 +1,4 @@  | 
                  
| 1 | 
                        -.Dd 2024-12-25  | 
                    |
| 1 | 
                        +.Dd 2025-01-07  | 
                    |
| 2 | 2 | 
                        .Dt DERIVEPASSPHRASE-VAULT 1  | 
                    
| 3 | 3 | 
                        .Os derivepassphrase 0.4.0  | 
                    
| 4 | 4 | 
                        .  | 
                    
| ... | ... | 
                      @@ -802,6 +802,15 @@ When importing a configuration, the indicated ineffective setting has been  | 
                  
| 802 | 802 | 
                        removed.  | 
                    
| 803 | 803 | 
                        .Pq The Do interpretation Dc of the configuration doesn't change .  | 
                    
| 804 | 804 | 
                        .  | 
                    
| 805 | 
                        +.It "The service name %s" "contains an ASCII control character," "which is not supported" "by our shell completion code."  | 
                    |
| 806 | 
                        +Because of limitations in the shell completion code, this specific service name  | 
                    |
| 807 | 
                        +will not be available as a suggestion in tab completion.  | 
                    |
| 808 | 
                        +.Po  | 
                    |
| 809 | 
                        +This  | 
                    |
| 810 | 
                        +.Em only  | 
                    |
| 811 | 
                        +affects tab completion, not other functionality.  | 
                    |
| 812 | 
                        +.Pc  | 
                    |
| 813 | 
                        +.  | 
                    |
| 805 | 814 | 
                        .It Setting a %s passphrase is ineffective because a key is also set.  | 
                    
| 806 | 815 | 
                        The configuration (global or key-specific) contains both a stored master  | 
                    
| 807 | 816 | 
                        passphrase and an SSH key.  | 
                    
| ... | ... | 
                      @@ -390,6 +390,11 @@ The <b>derivepassphrase vault</b> utility exits 0 on success, and >0 if an error  | 
                  
| 390 | 390 | 
                        When importing a configuration, the indicated ineffective setting has been removed.  | 
                    
| 391 | 391 | 
                        (The "interpretation" of the configuration doesn’t change).  | 
                    
| 392 | 392 | 
                         | 
                    
| 393 | 
                        +??? warning "`The service name %s contains an ASCII control character, which is not supported by our shell completion code.`"  | 
                    |
| 394 | 
                        +  | 
                    |
| 395 | 
                        + Because of limitations in the shell completion code, this specific service name will not be available as a suggestion in tab completion.  | 
                    |
| 396 | 
                        + (This *only* affects tab completion, not other functionality.)  | 
                    |
| 397 | 
                        +  | 
                    |
| 393 | 398 | 
                        ??? warning "`Setting a %s passphrase is ineffective because a key is also set.`"  | 
                    
| 394 | 399 | 
                         | 
                    
| 395 | 400 | 
                        The configuration (global or key-specific) contains both a stored master passphrase and an SSH key.  | 
                    
| ... | ... | 
                      @@ -816,6 +816,19 @@ class WarnMsgTemplate(enum.Enum):  | 
                  
| 816 | 816 | 
                        context='warning message',  | 
                    
| 817 | 817 | 
                        flags='python-brace-format',  | 
                    
| 818 | 818 | 
                        )  | 
                    
| 819 | 
                        + SERVICE_NAME_INCOMPLETABLE = _prepare_translatable(  | 
                    |
| 820 | 
                        + msg="""  | 
                    |
| 821 | 
                        +        The service name {service!r} contains an ASCII control
                       | 
                    |
| 822 | 
                        + character, which is not supported by our shell completion code.  | 
                    |
| 823 | 
                        + This service name will therefore not be available for completion  | 
                    |
| 824 | 
                        + on the command-line. You may of course still type it in  | 
                    |
| 825 | 
                        + manually in whatever format your shell accepts, but we highly  | 
                    |
| 826 | 
                        + recommend choosing a different service name instead.  | 
                    |
| 827 | 
                        + """,  | 
                    |
| 828 | 
                        + comments='',  | 
                    |
| 829 | 
                        + context='warning message',  | 
                    |
| 830 | 
                        + flags='python-brace-format',  | 
                    |
| 831 | 
                        + )  | 
                    |
| 819 | 832 | 
                        SERVICE_PASSPHRASE_INEFFECTIVE = _prepare_translatable(  | 
                    
| 820 | 833 | 
                        comments=r"""  | 
                    
| 821 | 834 | 
                        TRANSLATORS: The key that is set need not necessarily be set at  | 
                    
| ... | ... | 
                      @@ -33,11 +33,13 @@ from typing import (  | 
                  
| 33 | 33 | 
                        )  | 
                    
| 34 | 34 | 
                         | 
                    
| 35 | 35 | 
                        import click  | 
                    
| 36 | 
                        +import click.shell_completion  | 
                    |
| 36 | 37 | 
                        from typing_extensions import (  | 
                    
| 37 | 38 | 
                        Any,  | 
                    
| 38 | 39 | 
                        ParamSpec,  | 
                    
| 39 | 40 | 
                        Self,  | 
                    
| 40 | 41 | 
                        assert_never,  | 
                    
| 42 | 
                        + override,  | 
                    |
| 41 | 43 | 
                        )  | 
                    
| 42 | 44 | 
                         | 
                    
| 43 | 45 | 
                        import derivepassphrase as dpp  | 
                    
| ... | ... | 
                      @@ -1102,6 +1104,135 @@ def standard_logging_options(f: Callable[P, R]) -> Callable[P, R]:  | 
                  
| 1102 | 1104 | 
                        """  | 
                    
| 1103 | 1105 | 
                        return debug_option(verbose_option(quiet_option(f)))  | 
                    
| 1104 | 1106 | 
                         | 
                    
| 1107 | 
                        +# Shell completion  | 
                    |
| 1108 | 
                        +# ================  | 
                    |
| 1109 | 
                        +  | 
                    |
| 1110 | 
                        +# Use naive filename completion for the `path` argument of  | 
                    |
| 1111 | 
                        +# `derivepassphrase vault`'s `--import` and `--export` options, as well  | 
                    |
| 1112 | 
                        +# as the `path` argument of `derivepassphrase export vault`. The latter  | 
                    |
| 1113 | 
                        +# treats the pseudo-filename `VAULT_PATH` specially, but this is awkward  | 
                    |
| 1114 | 
                        +# to combine with standard filename completion, particularly in bash, so  | 
                    |
| 1115 | 
                        +# we would probably have to implement *all* completion (`VAULT_PATH` and  | 
                    |
| 1116 | 
                        +# filename completion) ourselves, lacking some niceties of bash's  | 
                    |
| 1117 | 
                        +# built-in completion (e.g., adding spaces or slashes depending on  | 
                    |
| 1118 | 
                        +# whether the completion is a directory or a complete filename).  | 
                    |
| 1119 | 
                        +  | 
                    |
| 1120 | 
                        +  | 
                    |
| 1121 | 
                        +def _shell_complete_path(  | 
                    |
| 1122 | 
                        + ctx: click.Context,  | 
                    |
| 1123 | 
                        + parameter: click.Parameter,  | 
                    |
| 1124 | 
                        + value: str,  | 
                    |
| 1125 | 
                        +) -> list[str | click.shell_completion.CompletionItem]:  | 
                    |
| 1126 | 
                        + """Request standard path completion for the `path` argument."""  | 
                    |
| 1127 | 
                        + del ctx, parameter, value  | 
                    |
| 1128 | 
                        +    return [click.shell_completion.CompletionItem('', type='file')]  # noqa: DOC201
                       | 
                    |
| 1129 | 
                        +  | 
                    |
| 1130 | 
                        +  | 
                    |
| 1131 | 
                        +# The standard `click` shell completion scripts serialize the completion  | 
                    |
| 1132 | 
                        +# items as newline-separated one-line entries, which get silently  | 
                    |
| 1133 | 
                        +# corrupted if the value contains newlines. Each shell imposes  | 
                    |
| 1134 | 
                        +# additional restrictions: Fish uses newlines in all internal completion  | 
                    |
| 1135 | 
                        +# helper scripts, so it is difficult, if not impossible, to register  | 
                    |
| 1136 | 
                        +# completion entries containing newlines if completion comes from within  | 
                    |
| 1137 | 
                        +# a Fish completion function (instead of a Fish builtin). Zsh's  | 
                    |
| 1138 | 
                        +# completion system supports descriptions for each completion item, and  | 
                    |
| 1139 | 
                        +# the completion helper functions parse every entry as a colon-separated  | 
                    |
| 1140 | 
                        +# 2-tuple of item and description, meaning any colon in the item value  | 
                    |
| 1141 | 
                        +# must be escaped. Finally, Bash requires the result array to be  | 
                    |
| 1142 | 
                        +# populated at the completion function's top-level scope, but for/while  | 
                    |
| 1143 | 
                        +# loops within pipelines do not run at top-level scope, and Bash *also*  | 
                    |
| 1144 | 
                        +# strips NUL characters from command substitution output, making it  | 
                    |
| 1145 | 
                        +# difficult to read in external data into an array in a cross-platform  | 
                    |
| 1146 | 
                        +# manner from entirely within Bash.  | 
                    |
| 1147 | 
                        +#  | 
                    |
| 1148 | 
                        +# We capitulate in front of these problems---most egregiously because of  | 
                    |
| 1149 | 
                        +# Fish---and ensure that completion items (in this case: service names)  | 
                    |
| 1150 | 
                        +# never contain ASCII control characters by refusing to offer such  | 
                    |
| 1151 | 
                        +# items as valid completions. On the other side, `derivepassphrase`  | 
                    |
| 1152 | 
                        +# will warn the user when configuring or importing a service with such  | 
                    |
| 1153 | 
                        +# a name that it will not be available for shell completion.  | 
                    |
| 1154 | 
                        +  | 
                    |
| 1155 | 
                        +  | 
                    |
| 1156 | 
                        +def _is_completable_item(obj: object) -> bool:  | 
                    |
| 1157 | 
                        + """Return whether the item is completable on the command-line.  | 
                    |
| 1158 | 
                        +  | 
                    |
| 1159 | 
                        + The item is completable if and only if it contains no ASCII control  | 
                    |
| 1160 | 
                        + characters (U+0000 through U+001F, and U+007F).  | 
                    |
| 1161 | 
                        +  | 
                    |
| 1162 | 
                        + """  | 
                    |
| 1163 | 
                        + obj = str(obj)  | 
                    |
| 1164 | 
                        +    forbidden = frozenset(chr(i) for i in range(32)) | {'\x7F'}
                       | 
                    |
| 1165 | 
                        + return not any(f in obj for f in forbidden)  | 
                    |
| 1166 | 
                        +  | 
                    |
| 1167 | 
                        +  | 
                    |
| 1168 | 
                        +def _shell_complete_service(  | 
                    |
| 1169 | 
                        + ctx: click.Context,  | 
                    |
| 1170 | 
                        + parameter: click.Parameter,  | 
                    |
| 1171 | 
                        + value: str,  | 
                    |
| 1172 | 
                        +) -> list[str | click.shell_completion.CompletionItem]:  | 
                    |
| 1173 | 
                        + """Return known vault service names as completion items.  | 
                    |
| 1174 | 
                        +  | 
                    |
| 1175 | 
                        + Service names are looked up in the vault configuration file. All  | 
                    |
| 1176 | 
                        + errors will be suppressed. Additionally, any service names deemed  | 
                    |
| 1177 | 
                        + not completable as per [`_is_completable_item`][] will be silently  | 
                    |
| 1178 | 
                        + skipped.  | 
                    |
| 1179 | 
                        +  | 
                    |
| 1180 | 
                        + """  | 
                    |
| 1181 | 
                        + del ctx, parameter  | 
                    |
| 1182 | 
                        + try:  | 
                    |
| 1183 | 
                        + config = _load_config()  | 
                    |
| 1184 | 
                        + return sorted(  | 
                    |
| 1185 | 
                        + sv  | 
                    |
| 1186 | 
                        + for sv in config['services']  | 
                    |
| 1187 | 
                        + if sv.startswith(value) and _is_completable_item(sv)  | 
                    |
| 1188 | 
                        + )  | 
                    |
| 1189 | 
                        + except FileNotFoundError:  | 
                    |
| 1190 | 
                        + try:  | 
                    |
| 1191 | 
                        + config, _exc = _migrate_and_load_old_config()  | 
                    |
| 1192 | 
                        + return sorted(  | 
                    |
| 1193 | 
                        + sv  | 
                    |
| 1194 | 
                        + for sv in config['services']  | 
                    |
| 1195 | 
                        + if sv.startswith(value) and _is_completable_item(sv)  | 
                    |
| 1196 | 
                        + )  | 
                    |
| 1197 | 
                        + except FileNotFoundError:  | 
                    |
| 1198 | 
                        + return []  | 
                    |
| 1199 | 
                        + except Exception: # noqa: BLE001  | 
                    |
| 1200 | 
                        + return []  | 
                    |
| 1201 | 
                        +  | 
                    |
| 1202 | 
                        +  | 
                    |
| 1203 | 
                        +class ZshComplete(click.shell_completion.ZshComplete):  | 
                    |
| 1204 | 
                        + """Zsh completion class that supports colons.  | 
                    |
| 1205 | 
                        +  | 
                    |
| 1206 | 
                        + `click`'s Zsh completion class (at least v8.1.7 and v8.1.8) uses  | 
                    |
| 1207 | 
                        + completion helper functions (provided by Zsh) that parse each  | 
                    |
| 1208 | 
                        + completion item into value-description pairs, separated by a colon.  | 
                    |
| 1209 | 
                        + Correspondingly, any internal colons in the completion item's value  | 
                    |
| 1210 | 
                        + need to be escaped. `click` doesn't do this. So, this subclass  | 
                    |
| 1211 | 
                        + overrides those parts, and adds the missing escaping.  | 
                    |
| 1212 | 
                        +  | 
                    |
| 1213 | 
                        + """  | 
                    |
| 1214 | 
                        +  | 
                    |
| 1215 | 
                        + @override  | 
                    |
| 1216 | 
                        + def format_completion(  | 
                    |
| 1217 | 
                        + self,  | 
                    |
| 1218 | 
                        + item: click.shell_completion.CompletionItem,  | 
                    |
| 1219 | 
                        + ) -> str:  | 
                    |
| 1220 | 
                        + """Return a suitable serialization of the CompletionItem.  | 
                    |
| 1221 | 
                        +  | 
                    |
| 1222 | 
                        + This serialization ensures colons in the item value are properly  | 
                    |
| 1223 | 
                        + escaped.  | 
                    |
| 1224 | 
                        +  | 
                    |
| 1225 | 
                        + """  | 
                    |
| 1226 | 
                        + type, value, help = ( # noqa: A001  | 
                    |
| 1227 | 
                        + item.type,  | 
                    |
| 1228 | 
                        +            item.value.replace(':', '\\:'),
                       | 
                    |
| 1229 | 
                        + item.help or '_',  | 
                    |
| 1230 | 
                        + )  | 
                    |
| 1231 | 
                        +        return f'{type}\n{value}\n{help}'
                       | 
                    |
| 1232 | 
                        +  | 
                    |
| 1233 | 
                        +  | 
                    |
| 1234 | 
                        +click.shell_completion.add_completion_class(ZshComplete)  | 
                    |
| 1235 | 
                        +  | 
                    |
| 1105 | 1236 | 
                         | 
                    
| 1106 | 1237 | 
                        # Top-level  | 
                    
| 1107 | 1238 | 
                        # =========  | 
                    
| ... | ... | 
                      @@ -1380,23 +1511,6 @@ def _load_data(  | 
                  
| 1380 | 1511 | 
                        assert_never(fmt)  | 
                    
| 1381 | 1512 | 
                         | 
                    
| 1382 | 1513 | 
                         | 
                    
| 1383 | 
                        -def _shell_complete_vault_path( # pragma: no cover  | 
                    |
| 1384 | 
                        - ctx: click.Context,  | 
                    |
| 1385 | 
                        - param: click.Parameter,  | 
                    |
| 1386 | 
                        - incomplete: str,  | 
                    |
| 1387 | 
                        -) -> list[str | click.shell_completion.CompletionItem]:  | 
                    |
| 1388 | 
                        - del ctx, param  | 
                    |
| 1389 | 
                        - if incomplete and 'VAULT_PATH'.startswith(incomplete):  | 
                    |
| 1390 | 
                        -        ret: set[str | click.shell_completion.CompletionItem] = {'VAULT_PATH'}
                       | 
                    |
| 1391 | 
                        - for f in os.listdir():  | 
                    |
| 1392 | 
                        - if f.startswith(incomplete):  | 
                    |
| 1393 | 
                        - ret.add(f + os.path.sep if os.path.isdir(f) else f)  | 
                    |
| 1394 | 
                        - return sorted(ret)  | 
                    |
| 1395 | 
                        - return [  | 
                    |
| 1396 | 
                        -        click.shell_completion.CompletionItem('', type='file'),
                       | 
                    |
| 1397 | 
                        - ]  | 
                    |
| 1398 | 
                        -  | 
                    |
| 1399 | 
                        -  | 
                    |
| 1400 | 1514 | 
                        @derivepassphrase_export.command(  | 
                    
| 1401 | 1515 | 
                        'vault',  | 
                    
| 1402 | 1516 | 
                             context_settings={'help_option_names': ['-h', '--help']},
                       | 
                    
| ... | ... | 
                      @@ -1456,7 +1570,7 @@ def _shell_complete_vault_path( # pragma: no cover  | 
                  
| 1456 | 1570 | 
                        'path',  | 
                    
| 1457 | 1571 | 
                        metavar=_msg.TranslatedString(_msg.Label.EXPORT_VAULT_METAVAR_PATH),  | 
                    
| 1458 | 1572 | 
                        required=True,  | 
                    
| 1459 | 
                        - shell_complete=_shell_complete_vault_path,  | 
                    |
| 1573 | 
                        + shell_complete=_shell_complete_path,  | 
                    |
| 1460 | 1574 | 
                        )  | 
                    
| 1461 | 1575 | 
                        @click.pass_context  | 
                    
| 1462 | 1576 | 
                        def derivepassphrase_export_vault(  | 
                    
| ... | ... | 
                      @@ -2258,36 +2372,6 @@ def _validate_length(  | 
                  
| 2258 | 2372 | 
                        return int_value  | 
                    
| 2259 | 2373 | 
                         | 
                    
| 2260 | 2374 | 
                         | 
                    
| 2261 | 
                        -def _shell_complete_path( # pragma: no cover  | 
                    |
| 2262 | 
                        - ctx: click.Context,  | 
                    |
| 2263 | 
                        - parameter: click.Parameter,  | 
                    |
| 2264 | 
                        - incomplete: str,  | 
                    |
| 2265 | 
                        -) -> list[str | click.shell_completion.CompletionItem]:  | 
                    |
| 2266 | 
                        - del ctx, parameter, incomplete  | 
                    |
| 2267 | 
                        -    return [click.shell_completion.CompletionItem('', type='file')]
                       | 
                    |
| 2268 | 
                        -  | 
                    |
| 2269 | 
                        -  | 
                    |
| 2270 | 
                        -def _shell_complete_service( # pragma: no cover  | 
                    |
| 2271 | 
                        - ctx: click.Context,  | 
                    |
| 2272 | 
                        - parameter: click.Parameter,  | 
                    |
| 2273 | 
                        - incomplete: str,  | 
                    |
| 2274 | 
                        -) -> list[str | click.shell_completion.CompletionItem]:  | 
                    |
| 2275 | 
                        - del ctx, parameter  | 
                    |
| 2276 | 
                        - try:  | 
                    |
| 2277 | 
                        - config = _load_config()  | 
                    |
| 2278 | 
                        - return [sv for sv in config['services'] if sv.startswith(incomplete)]  | 
                    |
| 2279 | 
                        - except FileNotFoundError:  | 
                    |
| 2280 | 
                        - try:  | 
                    |
| 2281 | 
                        - config, _exc = _migrate_and_load_old_config()  | 
                    |
| 2282 | 
                        - return [  | 
                    |
| 2283 | 
                        - sv for sv in config['services'] if sv.startswith(incomplete)  | 
                    |
| 2284 | 
                        - ]  | 
                    |
| 2285 | 
                        - except FileNotFoundError:  | 
                    |
| 2286 | 
                        - return []  | 
                    |
| 2287 | 
                        - except Exception: # noqa: BLE001  | 
                    |
| 2288 | 
                        - return []  | 
                    |
| 2289 | 
                        -  | 
                    |
| 2290 | 
                        -  | 
                    |
| 2291 | 2375 | 
                        DEFAULT_NOTES_TEMPLATE = """\  | 
                    
| 2292 | 2376 | 
                        # Enter notes below the line with the cut mark (ASCII scissors and  | 
                    
| 2293 | 2377 | 
                        # dashes). Lines above the cut mark (such as this one) will be ignored.  | 
                    
| ... | ... | 
                      @@ -3070,6 +3154,15 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915  | 
                  
| 3070 | 3154 | 
                        ),  | 
                    
| 3071 | 3155 | 
                                         extra={'color': ctx.color},
                       | 
                    
| 3072 | 3156 | 
                        )  | 
                    
| 3157 | 
                        + for service_name in sorted(maybe_config['services'].keys()):  | 
                    |
| 3158 | 
                        + if not _is_completable_item(service_name):  | 
                    |
| 3159 | 
                        + logger.warning(  | 
                    |
| 3160 | 
                        + _msg.TranslatedString(  | 
                    |
| 3161 | 
                        + _msg.WarnMsgTemplate.SERVICE_NAME_INCOMPLETABLE,  | 
                    |
| 3162 | 
                        + service=service_name,  | 
                    |
| 3163 | 
                        + ),  | 
                    |
| 3164 | 
                        +                    extra={'color': ctx.color},
                       | 
                    |
| 3165 | 
                        + )  | 
                    |
| 3073 | 3166 | 
                        try:  | 
                    
| 3074 | 3167 | 
                        _check_for_misleading_passphrase(  | 
                    
| 3075 | 3168 | 
                                         ('global',),
                       | 
                    
| ... | ... | 
                      @@ -3336,6 +3429,14 @@ def derivepassphrase_vault( # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915  | 
                  
| 3336 | 3429 | 
                        setting=setting,  | 
                    
| 3337 | 3430 | 
                        )  | 
                    
| 3338 | 3431 | 
                        raise click.UsageError(str(err_msg))  | 
                    
| 3432 | 
                        + if not _is_completable_item(service):  | 
                    |
| 3433 | 
                        + logger.warning(  | 
                    |
| 3434 | 
                        + _msg.TranslatedString(  | 
                    |
| 3435 | 
                        + _msg.WarnMsgTemplate.SERVICE_NAME_INCOMPLETABLE,  | 
                    |
| 3436 | 
                        + service=service,  | 
                    |
| 3437 | 
                        + ),  | 
                    |
| 3438 | 
                        +                    extra={'color': ctx.color},
                       | 
                    |
| 3439 | 
                        + )  | 
                    |
| 3339 | 3440 | 
                        subtree: dict[str, Any] = (  | 
                    
| 3340 | 3441 | 
                                         configuration['services'].setdefault(service, {})  # type: ignore[assignment]
                       | 
                    
| 3341 | 3442 | 
                        if service  | 
                    
| ... | ... | 
                      @@ -29,9 +29,12 @@ import tests  | 
                  
| 29 | 29 | 
                        from derivepassphrase import _types, cli, ssh_agent, vault  | 
                    
| 30 | 30 | 
                         | 
                    
| 31 | 31 | 
                        if TYPE_CHECKING:  | 
                    
| 32 | 
                        - from collections.abc import Callable, Iterable, Iterator  | 
                    |
| 32 | 
                        + from collections.abc import Callable, Iterable, Iterator, Sequence  | 
                    |
| 33 | 
                        + from collections.abc import Set as AbstractSet  | 
                    |
| 33 | 34 | 
                        from typing import NoReturn  | 
                    
| 34 | 35 | 
                         | 
                    
| 36 | 
                        + from typing_extensions import Literal  | 
                    |
| 37 | 
                        +  | 
                    |
| 35 | 38 | 
                        DUMMY_SERVICE = tests.DUMMY_SERVICE  | 
                    
| 36 | 39 | 
                        DUMMY_PASSPHRASE = tests.DUMMY_PASSPHRASE  | 
                    
| 37 | 40 | 
                        DUMMY_CONFIG_SETTINGS = tests.DUMMY_CONFIG_SETTINGS  | 
                    
| ... | ... | 
                      @@ -3142,6 +3145,28 @@ class TestCLITransition:  | 
                  
| 3142 | 3145 | 
                        'Failed to migrate to ', caplog.record_tuples  | 
                    
| 3143 | 3146 | 
                        ), 'expected known warning message in stderr'  | 
                    
| 3144 | 3147 | 
                         | 
                    
| 3148 | 
                        + def test_400_completion_service_name_old_config_file(  | 
                    |
| 3149 | 
                        + self,  | 
                    |
| 3150 | 
                        + monkeypatch: pytest.MonkeyPatch,  | 
                    |
| 3151 | 
                        + ) -> None:  | 
                    |
| 3152 | 
                        + runner = click.testing.CliRunner(mix_stderr=False)  | 
                    |
| 3153 | 
                        +        config = {'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()}}
                       | 
                    |
| 3154 | 
                        + with tests.isolated_vault_config(  | 
                    |
| 3155 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 3156 | 
                        + runner=runner,  | 
                    |
| 3157 | 
                        + vault_config=config,  | 
                    |
| 3158 | 
                        + ):  | 
                    |
| 3159 | 
                        + old_name = cli._config_filename(subsystem='old settings.json')  | 
                    |
| 3160 | 
                        + new_name = cli._config_filename(subsystem='vault')  | 
                    |
| 3161 | 
                        + with contextlib.suppress(FileNotFoundError):  | 
                    |
| 3162 | 
                        + os.remove(old_name)  | 
                    |
| 3163 | 
                        + os.rename(new_name, old_name)  | 
                    |
| 3164 | 
                        + assert cli._shell_complete_service(  | 
                    |
| 3165 | 
                        + click.Context(cli.derivepassphrase),  | 
                    |
| 3166 | 
                        + click.Argument(['some_parameter']),  | 
                    |
| 3167 | 
                        + '',  | 
                    |
| 3168 | 
                        + ) == [DUMMY_SERVICE]  | 
                    |
| 3169 | 
                        +  | 
                    |
| 3145 | 3170 | 
                         | 
                    
| 3146 | 3171 | 
                        _known_services = (DUMMY_SERVICE, 'email', 'bank', 'work')  | 
                    
| 3147 | 3172 | 
                        _valid_properties = (  | 
                    
| ... | ... | 
                      @@ -3490,3 +3515,708 @@ class ConfigManagementStateMachine(stateful.RuleBasedStateMachine):  | 
                  
| 3490 | 3515 | 
                         | 
                    
| 3491 | 3516 | 
                         | 
                    
| 3492 | 3517 | 
                        TestConfigManagement = ConfigManagementStateMachine.TestCase  | 
                    
| 3518 | 
                        +  | 
                    |
| 3519 | 
                        +  | 
                    |
| 3520 | 
                        +def bash_format(item: click.shell_completion.CompletionItem) -> str:  | 
                    |
| 3521 | 
                        + type, value = ( # noqa: A001  | 
                    |
| 3522 | 
                        + item.type,  | 
                    |
| 3523 | 
                        + item.value,  | 
                    |
| 3524 | 
                        + )  | 
                    |
| 3525 | 
                        +    return f'{type},{value}'
                       | 
                    |
| 3526 | 
                        +  | 
                    |
| 3527 | 
                        +  | 
                    |
| 3528 | 
                        +def fish_format(item: click.shell_completion.CompletionItem) -> str:  | 
                    |
| 3529 | 
                        + type, value, help = ( # noqa: A001  | 
                    |
| 3530 | 
                        + item.type,  | 
                    |
| 3531 | 
                        + item.value,  | 
                    |
| 3532 | 
                        + item.help,  | 
                    |
| 3533 | 
                        + )  | 
                    |
| 3534 | 
                        +    return f'{type},{value}\t{help}' if help else f'{type},{value}'
                       | 
                    |
| 3535 | 
                        +  | 
                    |
| 3536 | 
                        +  | 
                    |
| 3537 | 
                        +def zsh_format(item: click.shell_completion.CompletionItem) -> str:  | 
                    |
| 3538 | 
                        + type, value, help = ( # noqa: A001  | 
                    |
| 3539 | 
                        + item.type,  | 
                    |
| 3540 | 
                        +        item.value.replace(':', r'\:'),
                       | 
                    |
| 3541 | 
                        + item.help or '_',  | 
                    |
| 3542 | 
                        + )  | 
                    |
| 3543 | 
                        +    return f'{type}\n{value}\n{help}'
                       | 
                    |
| 3544 | 
                        +  | 
                    |
| 3545 | 
                        +  | 
                    |
| 3546 | 
                        +def completion_item(  | 
                    |
| 3547 | 
                        + item: str | click.shell_completion.CompletionItem  | 
                    |
| 3548 | 
                        +) -> click.shell_completion.CompletionItem:  | 
                    |
| 3549 | 
                        + return (  | 
                    |
| 3550 | 
                        + click.shell_completion.CompletionItem(item, type='plain')  | 
                    |
| 3551 | 
                        + if isinstance(item, str)  | 
                    |
| 3552 | 
                        + else item  | 
                    |
| 3553 | 
                        + )  | 
                    |
| 3554 | 
                        +  | 
                    |
| 3555 | 
                        +  | 
                    |
| 3556 | 
                        +def assertable_item(  | 
                    |
| 3557 | 
                        + item: str | click.shell_completion.CompletionItem  | 
                    |
| 3558 | 
                        +) -> tuple[str, Any, str | None]:  | 
                    |
| 3559 | 
                        + item = completion_item(item)  | 
                    |
| 3560 | 
                        + return (item.type, item.value, item.help)  | 
                    |
| 3561 | 
                        +  | 
                    |
| 3562 | 
                        +  | 
                    |
| 3563 | 
                        +class TestShellCompletion:  | 
                    |
| 3564 | 
                        +  | 
                    |
| 3565 | 
                        + class Completions:  | 
                    |
| 3566 | 
                        + def __init__(  | 
                    |
| 3567 | 
                        + self,  | 
                    |
| 3568 | 
                        + args: Sequence[str],  | 
                    |
| 3569 | 
                        + incomplete: str,  | 
                    |
| 3570 | 
                        + ) -> None:  | 
                    |
| 3571 | 
                        + self.args = tuple(args)  | 
                    |
| 3572 | 
                        + self.incomplete = incomplete  | 
                    |
| 3573 | 
                        +  | 
                    |
| 3574 | 
                        + def __call__(self) -> Sequence[click.shell_completion.CompletionItem]:  | 
                    |
| 3575 | 
                        + args = list(self.args)  | 
                    |
| 3576 | 
                        + completion = click.shell_completion.ShellComplete(  | 
                    |
| 3577 | 
                        + cli=cli.derivepassphrase,  | 
                    |
| 3578 | 
                        +                ctx_args={},
                       | 
                    |
| 3579 | 
                        + prog_name='derivepassphrase',  | 
                    |
| 3580 | 
                        + complete_var='_DERIVEPASSPHRASE_COMPLETE',  | 
                    |
| 3581 | 
                        + )  | 
                    |
| 3582 | 
                        + return completion.get_completions(args, self.incomplete)  | 
                    |
| 3583 | 
                        +  | 
                    |
| 3584 | 
                        + def get_words(self) -> Sequence[str]:  | 
                    |
| 3585 | 
                        + return tuple(c.value for c in self())  | 
                    |
| 3586 | 
                        +  | 
                    |
| 3587 | 
                        + @pytest.mark.parametrize(  | 
                    |
| 3588 | 
                        + ['partial', 'is_completable'],  | 
                    |
| 3589 | 
                        + [  | 
                    |
| 3590 | 
                        +            ('', True),
                       | 
                    |
| 3591 | 
                        + (DUMMY_SERVICE, True),  | 
                    |
| 3592 | 
                        +            ('a\bn', False),
                       | 
                    |
| 3593 | 
                        +            ('\b', False),
                       | 
                    |
| 3594 | 
                        +            ('\x00', False),
                       | 
                    |
| 3595 | 
                        +            ('\x20', True),
                       | 
                    |
| 3596 | 
                        +            ('\x7f', False),
                       | 
                    |
| 3597 | 
                        +            ('service with spaces', True),
                       | 
                    |
| 3598 | 
                        +            ('service\nwith\nnewlines', False),
                       | 
                    |
| 3599 | 
                        + ]  | 
                    |
| 3600 | 
                        + )  | 
                    |
| 3601 | 
                        + def test_100_is_completable_item(  | 
                    |
| 3602 | 
                        + self,  | 
                    |
| 3603 | 
                        + partial: str,  | 
                    |
| 3604 | 
                        + is_completable: bool,  | 
                    |
| 3605 | 
                        + ) -> None:  | 
                    |
| 3606 | 
                        + assert cli._is_completable_item(partial) == is_completable  | 
                    |
| 3607 | 
                        +  | 
                    |
| 3608 | 
                        + @pytest.mark.parametrize(  | 
                    |
| 3609 | 
                        + ['command_prefix', 'incomplete', 'completions'],  | 
                    |
| 3610 | 
                        + [  | 
                    |
| 3611 | 
                        + pytest.param(  | 
                    |
| 3612 | 
                        + (),  | 
                    |
| 3613 | 
                        + '-',  | 
                    |
| 3614 | 
                        +                frozenset({
                       | 
                    |
| 3615 | 
                        + '--help',  | 
                    |
| 3616 | 
                        + '-h',  | 
                    |
| 3617 | 
                        + '--version',  | 
                    |
| 3618 | 
                        + '--debug',  | 
                    |
| 3619 | 
                        + '--verbose',  | 
                    |
| 3620 | 
                        + '-v',  | 
                    |
| 3621 | 
                        + '--quiet',  | 
                    |
| 3622 | 
                        + '-q',  | 
                    |
| 3623 | 
                        + }),  | 
                    |
| 3624 | 
                        + id='derivepassphrase',  | 
                    |
| 3625 | 
                        + ),  | 
                    |
| 3626 | 
                        + pytest.param(  | 
                    |
| 3627 | 
                        +                ('export',),
                       | 
                    |
| 3628 | 
                        + '-',  | 
                    |
| 3629 | 
                        +                frozenset({
                       | 
                    |
| 3630 | 
                        + '--help',  | 
                    |
| 3631 | 
                        + '-h',  | 
                    |
| 3632 | 
                        + '--version',  | 
                    |
| 3633 | 
                        + '--debug',  | 
                    |
| 3634 | 
                        + '--verbose',  | 
                    |
| 3635 | 
                        + '-v',  | 
                    |
| 3636 | 
                        + '--quiet',  | 
                    |
| 3637 | 
                        + '-q',  | 
                    |
| 3638 | 
                        + }),  | 
                    |
| 3639 | 
                        + id='derivepassphrase-export',  | 
                    |
| 3640 | 
                        + ),  | 
                    |
| 3641 | 
                        + pytest.param(  | 
                    |
| 3642 | 
                        +                ('export', 'vault'),
                       | 
                    |
| 3643 | 
                        + '-',  | 
                    |
| 3644 | 
                        +                frozenset({
                       | 
                    |
| 3645 | 
                        + '--help',  | 
                    |
| 3646 | 
                        + '-h',  | 
                    |
| 3647 | 
                        + '--version',  | 
                    |
| 3648 | 
                        + '--debug',  | 
                    |
| 3649 | 
                        + '--verbose',  | 
                    |
| 3650 | 
                        + '-v',  | 
                    |
| 3651 | 
                        + '--quiet',  | 
                    |
| 3652 | 
                        + '-q',  | 
                    |
| 3653 | 
                        + '--format',  | 
                    |
| 3654 | 
                        + '-f',  | 
                    |
| 3655 | 
                        + '--key',  | 
                    |
| 3656 | 
                        + '-k',  | 
                    |
| 3657 | 
                        + }),  | 
                    |
| 3658 | 
                        + id='derivepassphrase-export-vault',  | 
                    |
| 3659 | 
                        + ),  | 
                    |
| 3660 | 
                        + pytest.param(  | 
                    |
| 3661 | 
                        +                ('vault',),
                       | 
                    |
| 3662 | 
                        + '-',  | 
                    |
| 3663 | 
                        +                frozenset({
                       | 
                    |
| 3664 | 
                        + '--help',  | 
                    |
| 3665 | 
                        + '-h',  | 
                    |
| 3666 | 
                        + '--version',  | 
                    |
| 3667 | 
                        + '--debug',  | 
                    |
| 3668 | 
                        + '--verbose',  | 
                    |
| 3669 | 
                        + '-v',  | 
                    |
| 3670 | 
                        + '--quiet',  | 
                    |
| 3671 | 
                        + '-q',  | 
                    |
| 3672 | 
                        + '--phrase',  | 
                    |
| 3673 | 
                        + '-p',  | 
                    |
| 3674 | 
                        + '--key',  | 
                    |
| 3675 | 
                        + '-k',  | 
                    |
| 3676 | 
                        + '--length',  | 
                    |
| 3677 | 
                        + '-l',  | 
                    |
| 3678 | 
                        + '--repeat',  | 
                    |
| 3679 | 
                        + '-r',  | 
                    |
| 3680 | 
                        + '--upper',  | 
                    |
| 3681 | 
                        + '--lower',  | 
                    |
| 3682 | 
                        + '--number',  | 
                    |
| 3683 | 
                        + '--space',  | 
                    |
| 3684 | 
                        + '--dash',  | 
                    |
| 3685 | 
                        + '--symbol',  | 
                    |
| 3686 | 
                        + '--config',  | 
                    |
| 3687 | 
                        + '-c',  | 
                    |
| 3688 | 
                        + '--notes',  | 
                    |
| 3689 | 
                        + '-n',  | 
                    |
| 3690 | 
                        + '--delete',  | 
                    |
| 3691 | 
                        + '-x',  | 
                    |
| 3692 | 
                        + '--delete-globals',  | 
                    |
| 3693 | 
                        + '--clear',  | 
                    |
| 3694 | 
                        + '-X',  | 
                    |
| 3695 | 
                        + '--export',  | 
                    |
| 3696 | 
                        + '-e',  | 
                    |
| 3697 | 
                        + '--import',  | 
                    |
| 3698 | 
                        + '-i',  | 
                    |
| 3699 | 
                        + '--overwrite-existing',  | 
                    |
| 3700 | 
                        + '--merge-existing',  | 
                    |
| 3701 | 
                        + '--unset',  | 
                    |
| 3702 | 
                        + '--export-as',  | 
                    |
| 3703 | 
                        + }),  | 
                    |
| 3704 | 
                        + id='derivepassphrase-vault',  | 
                    |
| 3705 | 
                        + ),  | 
                    |
| 3706 | 
                        + ],  | 
                    |
| 3707 | 
                        + )  | 
                    |
| 3708 | 
                        + def test_200_options(  | 
                    |
| 3709 | 
                        + self,  | 
                    |
| 3710 | 
                        + command_prefix: Sequence[str],  | 
                    |
| 3711 | 
                        + incomplete: str,  | 
                    |
| 3712 | 
                        + completions: AbstractSet[str],  | 
                    |
| 3713 | 
                        + ) -> None:  | 
                    |
| 3714 | 
                        + comp = self.Completions(command_prefix, incomplete)  | 
                    |
| 3715 | 
                        + assert frozenset(comp.get_words()) == completions  | 
                    |
| 3716 | 
                        +  | 
                    |
| 3717 | 
                        + @pytest.mark.parametrize(  | 
                    |
| 3718 | 
                        + ['command_prefix', 'incomplete', 'completions'],  | 
                    |
| 3719 | 
                        + [  | 
                    |
| 3720 | 
                        + pytest.param(  | 
                    |
| 3721 | 
                        + (),  | 
                    |
| 3722 | 
                        + '',  | 
                    |
| 3723 | 
                        +                frozenset({'export', 'vault'}),
                       | 
                    |
| 3724 | 
                        + id='derivepassphrase',  | 
                    |
| 3725 | 
                        + ),  | 
                    |
| 3726 | 
                        + pytest.param(  | 
                    |
| 3727 | 
                        +                ('export',),
                       | 
                    |
| 3728 | 
                        + '',  | 
                    |
| 3729 | 
                        +                frozenset({'vault'}),
                       | 
                    |
| 3730 | 
                        + id='derivepassphrase-export',  | 
                    |
| 3731 | 
                        + ),  | 
                    |
| 3732 | 
                        + ],  | 
                    |
| 3733 | 
                        + )  | 
                    |
| 3734 | 
                        + def test_201_subcommands(  | 
                    |
| 3735 | 
                        + self,  | 
                    |
| 3736 | 
                        + command_prefix: Sequence[str],  | 
                    |
| 3737 | 
                        + incomplete: str,  | 
                    |
| 3738 | 
                        + completions: AbstractSet[str],  | 
                    |
| 3739 | 
                        + ) -> None:  | 
                    |
| 3740 | 
                        + comp = self.Completions(command_prefix, incomplete)  | 
                    |
| 3741 | 
                        + assert frozenset(comp.get_words()) == completions  | 
                    |
| 3742 | 
                        +  | 
                    |
| 3743 | 
                        + @pytest.mark.parametrize(  | 
                    |
| 3744 | 
                        + 'command_prefix',  | 
                    |
| 3745 | 
                        + [  | 
                    |
| 3746 | 
                        + pytest.param(  | 
                    |
| 3747 | 
                        +                ('export', 'vault'),
                       | 
                    |
| 3748 | 
                        + id='derivepassphrase-export-vault',  | 
                    |
| 3749 | 
                        + ),  | 
                    |
| 3750 | 
                        + pytest.param(  | 
                    |
| 3751 | 
                        +                ('vault', '--export'),
                       | 
                    |
| 3752 | 
                        + id='derivepassphrase-vault--export',  | 
                    |
| 3753 | 
                        + ),  | 
                    |
| 3754 | 
                        + pytest.param(  | 
                    |
| 3755 | 
                        +                ('vault', '--import'),
                       | 
                    |
| 3756 | 
                        + id='derivepassphrase-vault--import',  | 
                    |
| 3757 | 
                        + ),  | 
                    |
| 3758 | 
                        + ],  | 
                    |
| 3759 | 
                        + )  | 
                    |
| 3760 | 
                        +    @pytest.mark.parametrize('incomplete', ['', 'partial'])
                       | 
                    |
| 3761 | 
                        + def test_202_paths(  | 
                    |
| 3762 | 
                        + self,  | 
                    |
| 3763 | 
                        + command_prefix: Sequence[str],  | 
                    |
| 3764 | 
                        + incomplete: str,  | 
                    |
| 3765 | 
                        + ) -> None:  | 
                    |
| 3766 | 
                        +        file = click.shell_completion.CompletionItem('', type='file')
                       | 
                    |
| 3767 | 
                        +        completions = frozenset({(file.type, file.value, file.help)})
                       | 
                    |
| 3768 | 
                        + comp = self.Completions(command_prefix, incomplete)  | 
                    |
| 3769 | 
                        + assert frozenset(  | 
                    |
| 3770 | 
                        + (x.type, x.value, x.help) for x in comp()  | 
                    |
| 3771 | 
                        + ) == completions  | 
                    |
| 3772 | 
                        +  | 
                    |
| 3773 | 
                        + @pytest.mark.parametrize(  | 
                    |
| 3774 | 
                        + ['config', 'incomplete', 'completions'],  | 
                    |
| 3775 | 
                        + [  | 
                    |
| 3776 | 
                        + pytest.param(  | 
                    |
| 3777 | 
                        +                {"services": {}},
                       | 
                    |
| 3778 | 
                        + '',  | 
                    |
| 3779 | 
                        + frozenset(),  | 
                    |
| 3780 | 
                        + id='no_services',  | 
                    |
| 3781 | 
                        + ),  | 
                    |
| 3782 | 
                        + pytest.param(  | 
                    |
| 3783 | 
                        +                {"services": {}},
                       | 
                    |
| 3784 | 
                        + 'partial',  | 
                    |
| 3785 | 
                        + frozenset(),  | 
                    |
| 3786 | 
                        + id='no_services_partial',  | 
                    |
| 3787 | 
                        + ),  | 
                    |
| 3788 | 
                        + pytest.param(  | 
                    |
| 3789 | 
                        +                {"services": {DUMMY_SERVICE: {"length": 10}}},
                       | 
                    |
| 3790 | 
                        + '',  | 
                    |
| 3791 | 
                        +                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 3792 | 
                        + id='one_service',  | 
                    |
| 3793 | 
                        + ),  | 
                    |
| 3794 | 
                        + pytest.param(  | 
                    |
| 3795 | 
                        +                {"services": {DUMMY_SERVICE: {"length": 10}}},
                       | 
                    |
| 3796 | 
                        + DUMMY_SERVICE[:4],  | 
                    |
| 3797 | 
                        +                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 3798 | 
                        + id='one_service_partial',  | 
                    |
| 3799 | 
                        + ),  | 
                    |
| 3800 | 
                        + pytest.param(  | 
                    |
| 3801 | 
                        +                {"services": {DUMMY_SERVICE: {"length": 10}}},
                       | 
                    |
| 3802 | 
                        + DUMMY_SERVICE[-4:],  | 
                    |
| 3803 | 
                        + frozenset(),  | 
                    |
| 3804 | 
                        + id='one_service_partial_miss',  | 
                    |
| 3805 | 
                        + ),  | 
                    |
| 3806 | 
                        + ],  | 
                    |
| 3807 | 
                        + )  | 
                    |
| 3808 | 
                        + def test_203_service_names(  | 
                    |
| 3809 | 
                        + self,  | 
                    |
| 3810 | 
                        + monkeypatch: pytest.MonkeyPatch,  | 
                    |
| 3811 | 
                        + config: _types.VaultConfig,  | 
                    |
| 3812 | 
                        + incomplete: str,  | 
                    |
| 3813 | 
                        + completions: AbstractSet[str],  | 
                    |
| 3814 | 
                        + ) -> None:  | 
                    |
| 3815 | 
                        + runner = click.testing.CliRunner(mix_stderr=False)  | 
                    |
| 3816 | 
                        + with tests.isolated_vault_config(  | 
                    |
| 3817 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 3818 | 
                        + runner=runner,  | 
                    |
| 3819 | 
                        + vault_config=config,  | 
                    |
| 3820 | 
                        + ):  | 
                    |
| 3821 | 
                        + comp = self.Completions(['vault'], incomplete)  | 
                    |
| 3822 | 
                        + assert frozenset(comp.get_words()) == completions  | 
                    |
| 3823 | 
                        +  | 
                    |
| 3824 | 
                        + @pytest.mark.parametrize(  | 
                    |
| 3825 | 
                        + ['shell', 'format_func'],  | 
                    |
| 3826 | 
                        + [  | 
                    |
| 3827 | 
                        +            pytest.param('bash', bash_format, id='bash'),
                       | 
                    |
| 3828 | 
                        +            pytest.param('fish', fish_format, id='fish'),
                       | 
                    |
| 3829 | 
                        +            pytest.param('zsh', zsh_format, id='zsh'),
                       | 
                    |
| 3830 | 
                        + ],  | 
                    |
| 3831 | 
                        + )  | 
                    |
| 3832 | 
                        + @pytest.mark.parametrize(  | 
                    |
| 3833 | 
                        + ['config', 'comp_func', 'args', 'incomplete', 'results'],  | 
                    |
| 3834 | 
                        + [  | 
                    |
| 3835 | 
                        + pytest.param(  | 
                    |
| 3836 | 
                        +                {"services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()}},
                       | 
                    |
| 3837 | 
                        + cli._shell_complete_service,  | 
                    |
| 3838 | 
                        + ['vault'],  | 
                    |
| 3839 | 
                        + '',  | 
                    |
| 3840 | 
                        + [DUMMY_SERVICE],  | 
                    |
| 3841 | 
                        + id='base_config-service',  | 
                    |
| 3842 | 
                        + ),  | 
                    |
| 3843 | 
                        + pytest.param(  | 
                    |
| 3844 | 
                        +                {"services": {}},
                       | 
                    |
| 3845 | 
                        + cli._shell_complete_service,  | 
                    |
| 3846 | 
                        + ['vault'],  | 
                    |
| 3847 | 
                        + '',  | 
                    |
| 3848 | 
                        + [],  | 
                    |
| 3849 | 
                        + id='empty_config-service',  | 
                    |
| 3850 | 
                        + ),  | 
                    |
| 3851 | 
                        + pytest.param(  | 
                    |
| 3852 | 
                        +                {
                       | 
                    |
| 3853 | 
                        +                    "services": {
                       | 
                    |
| 3854 | 
                        + DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 3855 | 
                        + "newline\nin\nname": DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 3856 | 
                        + }  | 
                    |
| 3857 | 
                        + },  | 
                    |
| 3858 | 
                        + cli._shell_complete_service,  | 
                    |
| 3859 | 
                        + ['vault'],  | 
                    |
| 3860 | 
                        + '',  | 
                    |
| 3861 | 
                        + [DUMMY_SERVICE],  | 
                    |
| 3862 | 
                        + id='incompletable_newline_config-service',  | 
                    |
| 3863 | 
                        + ),  | 
                    |
| 3864 | 
                        + pytest.param(  | 
                    |
| 3865 | 
                        +                {
                       | 
                    |
| 3866 | 
                        +                    "services": {
                       | 
                    |
| 3867 | 
                        + DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 3868 | 
                        + "backspace\bin\bname": DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 3869 | 
                        + }  | 
                    |
| 3870 | 
                        + },  | 
                    |
| 3871 | 
                        + cli._shell_complete_service,  | 
                    |
| 3872 | 
                        + ['vault'],  | 
                    |
| 3873 | 
                        + '',  | 
                    |
| 3874 | 
                        + [DUMMY_SERVICE],  | 
                    |
| 3875 | 
                        + id='incompletable_backspace_config-service',  | 
                    |
| 3876 | 
                        + ),  | 
                    |
| 3877 | 
                        + pytest.param(  | 
                    |
| 3878 | 
                        +                {
                       | 
                    |
| 3879 | 
                        +                    "services": {
                       | 
                    |
| 3880 | 
                        + DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 3881 | 
                        + "colon:in:name": DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 3882 | 
                        + }  | 
                    |
| 3883 | 
                        + },  | 
                    |
| 3884 | 
                        + cli._shell_complete_service,  | 
                    |
| 3885 | 
                        + ['vault'],  | 
                    |
| 3886 | 
                        + '',  | 
                    |
| 3887 | 
                        + sorted([DUMMY_SERVICE, 'colon:in:name']),  | 
                    |
| 3888 | 
                        + id='brittle_colon_config-service',  | 
                    |
| 3889 | 
                        + ),  | 
                    |
| 3890 | 
                        + pytest.param(  | 
                    |
| 3891 | 
                        +                {
                       | 
                    |
| 3892 | 
                        +                    "services": {
                       | 
                    |
| 3893 | 
                        + DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 3894 | 
                        + "colon:in:name": DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 3895 | 
                        + "newline\nin\nname": DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 3896 | 
                        + "backspace\bin\bname": DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 3897 | 
                        + "nul\x00in\x00name": DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 3898 | 
                        + "del\x7fin\x7fname": DUMMY_CONFIG_SETTINGS.copy(),  | 
                    |
| 3899 | 
                        + }  | 
                    |
| 3900 | 
                        + },  | 
                    |
| 3901 | 
                        + cli._shell_complete_service,  | 
                    |
| 3902 | 
                        + ['vault'],  | 
                    |
| 3903 | 
                        + '',  | 
                    |
| 3904 | 
                        + sorted([DUMMY_SERVICE, 'colon:in:name']),  | 
                    |
| 3905 | 
                        + id='brittle_incompletable_multi_config-service',  | 
                    |
| 3906 | 
                        + ),  | 
                    |
| 3907 | 
                        + pytest.param(  | 
                    |
| 3908 | 
                        +                {"services": {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()}},
                       | 
                    |
| 3909 | 
                        + cli._shell_complete_path,  | 
                    |
| 3910 | 
                        + ['vault', '--import'],  | 
                    |
| 3911 | 
                        + '',  | 
                    |
| 3912 | 
                        +                [click.shell_completion.CompletionItem('', type='file')],
                       | 
                    |
| 3913 | 
                        + id='base_config-path',  | 
                    |
| 3914 | 
                        + ),  | 
                    |
| 3915 | 
                        + pytest.param(  | 
                    |
| 3916 | 
                        +                {"services": {}},
                       | 
                    |
| 3917 | 
                        + cli._shell_complete_path,  | 
                    |
| 3918 | 
                        + ['vault', '--import'],  | 
                    |
| 3919 | 
                        + '',  | 
                    |
| 3920 | 
                        +                [click.shell_completion.CompletionItem('', type='file')],
                       | 
                    |
| 3921 | 
                        + id='empty_config-path',  | 
                    |
| 3922 | 
                        + ),  | 
                    |
| 3923 | 
                        + ],  | 
                    |
| 3924 | 
                        + )  | 
                    |
| 3925 | 
                        + def test_300_shell_completion_formatting(  | 
                    |
| 3926 | 
                        + self,  | 
                    |
| 3927 | 
                        + monkeypatch: pytest.MonkeyPatch,  | 
                    |
| 3928 | 
                        + shell: str,  | 
                    |
| 3929 | 
                        + format_func: Callable[[click.shell_completion.CompletionItem], str],  | 
                    |
| 3930 | 
                        + config: _types.VaultConfig,  | 
                    |
| 3931 | 
                        + comp_func: Callable[  | 
                    |
| 3932 | 
                        + [click.Context, click.Parameter, str],  | 
                    |
| 3933 | 
                        + list[str | click.shell_completion.CompletionItem]  | 
                    |
| 3934 | 
                        + ],  | 
                    |
| 3935 | 
                        + args: list[str],  | 
                    |
| 3936 | 
                        + incomplete: str,  | 
                    |
| 3937 | 
                        + results: list[str | click.shell_completion.CompletionItem],  | 
                    |
| 3938 | 
                        + ) -> None:  | 
                    |
| 3939 | 
                        + runner = click.testing.CliRunner(mix_stderr=False)  | 
                    |
| 3940 | 
                        + with tests.isolated_vault_config(  | 
                    |
| 3941 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 3942 | 
                        + runner=runner,  | 
                    |
| 3943 | 
                        + vault_config=config,  | 
                    |
| 3944 | 
                        + ):  | 
                    |
| 3945 | 
                        + expected_items = [  | 
                    |
| 3946 | 
                        + assertable_item(item)  | 
                    |
| 3947 | 
                        + for item in results  | 
                    |
| 3948 | 
                        + ]  | 
                    |
| 3949 | 
                        + expected_string = '\n'.join(  | 
                    |
| 3950 | 
                        + format_func(completion_item(item))  | 
                    |
| 3951 | 
                        + for item in results  | 
                    |
| 3952 | 
                        + )  | 
                    |
| 3953 | 
                        + manual_raw_items = comp_func(  | 
                    |
| 3954 | 
                        + click.Context(cli.derivepassphrase),  | 
                    |
| 3955 | 
                        + click.Argument(['sample_parameter']),  | 
                    |
| 3956 | 
                        + incomplete,  | 
                    |
| 3957 | 
                        + )  | 
                    |
| 3958 | 
                        + manual_items = [  | 
                    |
| 3959 | 
                        + assertable_item(item)  | 
                    |
| 3960 | 
                        + for item in manual_raw_items  | 
                    |
| 3961 | 
                        + ]  | 
                    |
| 3962 | 
                        + manual_string = '\n'.join(  | 
                    |
| 3963 | 
                        + format_func(completion_item(item))  | 
                    |
| 3964 | 
                        + for item in manual_raw_items  | 
                    |
| 3965 | 
                        + )  | 
                    |
| 3966 | 
                        + assert manual_items == expected_items  | 
                    |
| 3967 | 
                        + assert manual_string == expected_string  | 
                    |
| 3968 | 
                        + comp_class = click.shell_completion.get_completion_class(shell)  | 
                    |
| 3969 | 
                        + assert comp_class is not None  | 
                    |
| 3970 | 
                        + comp = comp_class(  | 
                    |
| 3971 | 
                        + cli.derivepassphrase,  | 
                    |
| 3972 | 
                        +                {},
                       | 
                    |
| 3973 | 
                        + 'derivepassphrase',  | 
                    |
| 3974 | 
                        + '_DERIVEPASSPHRASE_COMPLETE',  | 
                    |
| 3975 | 
                        + )  | 
                    |
| 3976 | 
                        + monkeypatch.setattr(  | 
                    |
| 3977 | 
                        + comp,  | 
                    |
| 3978 | 
                        + 'get_completion_args',  | 
                    |
| 3979 | 
                        + lambda *_a, **_kw: (args, incomplete),  | 
                    |
| 3980 | 
                        + )  | 
                    |
| 3981 | 
                        + actual_raw_items = comp.get_completions(  | 
                    |
| 3982 | 
                        + *comp.get_completion_args()  | 
                    |
| 3983 | 
                        + )  | 
                    |
| 3984 | 
                        + actual_items = [  | 
                    |
| 3985 | 
                        + assertable_item(item)  | 
                    |
| 3986 | 
                        + for item in actual_raw_items  | 
                    |
| 3987 | 
                        + ]  | 
                    |
| 3988 | 
                        + actual_string = comp.complete()  | 
                    |
| 3989 | 
                        + assert actual_items == expected_items  | 
                    |
| 3990 | 
                        + assert actual_string == expected_string  | 
                    |
| 3991 | 
                        +  | 
                    |
| 3992 | 
                        +    @pytest.mark.parametrize('mode', ['config', 'import'])
                       | 
                    |
| 3993 | 
                        + @pytest.mark.parametrize(  | 
                    |
| 3994 | 
                        + ['config', 'key', 'incomplete', 'completions'],  | 
                    |
| 3995 | 
                        + [  | 
                    |
| 3996 | 
                        + pytest.param(  | 
                    |
| 3997 | 
                        +                {
                       | 
                    |
| 3998 | 
                        +                    "services": {
                       | 
                    |
| 3999 | 
                        +                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 4000 | 
                        +                        "newline\nin\nname": {"length": 10},
                       | 
                    |
| 4001 | 
                        + },  | 
                    |
| 4002 | 
                        + },  | 
                    |
| 4003 | 
                        + 'newline\nin\nname',  | 
                    |
| 4004 | 
                        + '',  | 
                    |
| 4005 | 
                        +                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 4006 | 
                        + id='newline',  | 
                    |
| 4007 | 
                        + ),  | 
                    |
| 4008 | 
                        + pytest.param(  | 
                    |
| 4009 | 
                        +                {
                       | 
                    |
| 4010 | 
                        +                    "services": {
                       | 
                    |
| 4011 | 
                        +                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 4012 | 
                        +                        "newline\nin\nname": {"length": 10},
                       | 
                    |
| 4013 | 
                        + },  | 
                    |
| 4014 | 
                        + },  | 
                    |
| 4015 | 
                        + 'newline\nin\nname',  | 
                    |
| 4016 | 
                        + 'serv',  | 
                    |
| 4017 | 
                        +                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 4018 | 
                        + id='newline_partial_other',  | 
                    |
| 4019 | 
                        + ),  | 
                    |
| 4020 | 
                        + pytest.param(  | 
                    |
| 4021 | 
                        +                {
                       | 
                    |
| 4022 | 
                        +                    "services": {
                       | 
                    |
| 4023 | 
                        +                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 4024 | 
                        +                        "newline\nin\nname": {"length": 10},
                       | 
                    |
| 4025 | 
                        + },  | 
                    |
| 4026 | 
                        + },  | 
                    |
| 4027 | 
                        + 'newline\nin\nname',  | 
                    |
| 4028 | 
                        + 'newline',  | 
                    |
| 4029 | 
                        +                frozenset({}),
                       | 
                    |
| 4030 | 
                        + id='newline_partial_specific',  | 
                    |
| 4031 | 
                        + ),  | 
                    |
| 4032 | 
                        + pytest.param(  | 
                    |
| 4033 | 
                        +                {
                       | 
                    |
| 4034 | 
                        +                    "services": {
                       | 
                    |
| 4035 | 
                        +                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 4036 | 
                        +                        "nul\x00in\x00name": {"length": 10},
                       | 
                    |
| 4037 | 
                        + },  | 
                    |
| 4038 | 
                        + },  | 
                    |
| 4039 | 
                        + 'nul\x00in\x00name',  | 
                    |
| 4040 | 
                        + '',  | 
                    |
| 4041 | 
                        +                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 4042 | 
                        + id='nul',  | 
                    |
| 4043 | 
                        + ),  | 
                    |
| 4044 | 
                        + pytest.param(  | 
                    |
| 4045 | 
                        +                {
                       | 
                    |
| 4046 | 
                        +                    "services": {
                       | 
                    |
| 4047 | 
                        +                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 4048 | 
                        +                        "nul\x00in\x00name": {"length": 10},
                       | 
                    |
| 4049 | 
                        + },  | 
                    |
| 4050 | 
                        + },  | 
                    |
| 4051 | 
                        + 'nul\x00in\x00name',  | 
                    |
| 4052 | 
                        + 'serv',  | 
                    |
| 4053 | 
                        +                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 4054 | 
                        + id='nul_partial_other',  | 
                    |
| 4055 | 
                        + ),  | 
                    |
| 4056 | 
                        + pytest.param(  | 
                    |
| 4057 | 
                        +                {
                       | 
                    |
| 4058 | 
                        +                    "services": {
                       | 
                    |
| 4059 | 
                        +                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 4060 | 
                        +                        "nul\x00in\x00name": {"length": 10},
                       | 
                    |
| 4061 | 
                        + },  | 
                    |
| 4062 | 
                        + },  | 
                    |
| 4063 | 
                        + 'nul\x00in\x00name',  | 
                    |
| 4064 | 
                        + 'nul',  | 
                    |
| 4065 | 
                        +                frozenset({}),
                       | 
                    |
| 4066 | 
                        + id='nul_partial_specific',  | 
                    |
| 4067 | 
                        + ),  | 
                    |
| 4068 | 
                        + pytest.param(  | 
                    |
| 4069 | 
                        +                {
                       | 
                    |
| 4070 | 
                        +                    "services": {
                       | 
                    |
| 4071 | 
                        +                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 4072 | 
                        +                        "backspace\bin\bname": {"length": 10},
                       | 
                    |
| 4073 | 
                        + },  | 
                    |
| 4074 | 
                        + },  | 
                    |
| 4075 | 
                        + 'backspace\bin\bname',  | 
                    |
| 4076 | 
                        + '',  | 
                    |
| 4077 | 
                        +                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 4078 | 
                        + id='backspace',  | 
                    |
| 4079 | 
                        + ),  | 
                    |
| 4080 | 
                        + pytest.param(  | 
                    |
| 4081 | 
                        +                {
                       | 
                    |
| 4082 | 
                        +                    "services": {
                       | 
                    |
| 4083 | 
                        +                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 4084 | 
                        +                        "backspace\bin\bname": {"length": 10},
                       | 
                    |
| 4085 | 
                        + },  | 
                    |
| 4086 | 
                        + },  | 
                    |
| 4087 | 
                        + 'backspace\bin\bname',  | 
                    |
| 4088 | 
                        + 'serv',  | 
                    |
| 4089 | 
                        +                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 4090 | 
                        + id='backspace_partial_other',  | 
                    |
| 4091 | 
                        + ),  | 
                    |
| 4092 | 
                        + pytest.param(  | 
                    |
| 4093 | 
                        +                {
                       | 
                    |
| 4094 | 
                        +                    "services": {
                       | 
                    |
| 4095 | 
                        +                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 4096 | 
                        +                        "backspace\bin\bname": {"length": 10},
                       | 
                    |
| 4097 | 
                        + },  | 
                    |
| 4098 | 
                        + },  | 
                    |
| 4099 | 
                        + 'backspace\bin\bname',  | 
                    |
| 4100 | 
                        + 'back',  | 
                    |
| 4101 | 
                        +                frozenset({}),
                       | 
                    |
| 4102 | 
                        + id='backspace_partial_specific',  | 
                    |
| 4103 | 
                        + ),  | 
                    |
| 4104 | 
                        + pytest.param(  | 
                    |
| 4105 | 
                        +                {
                       | 
                    |
| 4106 | 
                        +                    "services": {
                       | 
                    |
| 4107 | 
                        +                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 4108 | 
                        +                        "del\x7fin\x7fname": {"length": 10},
                       | 
                    |
| 4109 | 
                        + },  | 
                    |
| 4110 | 
                        + },  | 
                    |
| 4111 | 
                        + 'del\x7fin\x7fname',  | 
                    |
| 4112 | 
                        + '',  | 
                    |
| 4113 | 
                        +                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 4114 | 
                        + id='del',  | 
                    |
| 4115 | 
                        + ),  | 
                    |
| 4116 | 
                        + pytest.param(  | 
                    |
| 4117 | 
                        +                {
                       | 
                    |
| 4118 | 
                        +                    "services": {
                       | 
                    |
| 4119 | 
                        +                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 4120 | 
                        +                        "del\x7fin\x7fname": {"length": 10},
                       | 
                    |
| 4121 | 
                        + },  | 
                    |
| 4122 | 
                        + },  | 
                    |
| 4123 | 
                        + 'del\x7fin\x7fname',  | 
                    |
| 4124 | 
                        + 'serv',  | 
                    |
| 4125 | 
                        +                frozenset({DUMMY_SERVICE}),
                       | 
                    |
| 4126 | 
                        + id='del_partial_other',  | 
                    |
| 4127 | 
                        + ),  | 
                    |
| 4128 | 
                        + pytest.param(  | 
                    |
| 4129 | 
                        +                {
                       | 
                    |
| 4130 | 
                        +                    "services": {
                       | 
                    |
| 4131 | 
                        +                        DUMMY_SERVICE: {"length": 10},
                       | 
                    |
| 4132 | 
                        +                        "del\x7fin\x7fname": {"length": 10},
                       | 
                    |
| 4133 | 
                        + },  | 
                    |
| 4134 | 
                        + },  | 
                    |
| 4135 | 
                        + 'del\x7fin\x7fname',  | 
                    |
| 4136 | 
                        + 'del',  | 
                    |
| 4137 | 
                        +                frozenset({}),
                       | 
                    |
| 4138 | 
                        + id='del_partial_specific',  | 
                    |
| 4139 | 
                        + ),  | 
                    |
| 4140 | 
                        + ],  | 
                    |
| 4141 | 
                        + )  | 
                    |
| 4142 | 
                        + def test_400_incompletable_service_names(  | 
                    |
| 4143 | 
                        + self,  | 
                    |
| 4144 | 
                        + monkeypatch: pytest.MonkeyPatch,  | 
                    |
| 4145 | 
                        + caplog: pytest.LogCaptureFixture,  | 
                    |
| 4146 | 
                        + mode: Literal['config', 'import'],  | 
                    |
| 4147 | 
                        + config: _types.VaultConfig,  | 
                    |
| 4148 | 
                        + key: str,  | 
                    |
| 4149 | 
                        + incomplete: str,  | 
                    |
| 4150 | 
                        + completions: AbstractSet[str],  | 
                    |
| 4151 | 
                        + ) -> None:  | 
                    |
| 4152 | 
                        + runner = click.testing.CliRunner(mix_stderr=False)  | 
                    |
| 4153 | 
                        +        vault_config = config if mode == 'config' else {'services': {}}
                       | 
                    |
| 4154 | 
                        + with tests.isolated_vault_config(  | 
                    |
| 4155 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 4156 | 
                        + runner=runner,  | 
                    |
| 4157 | 
                        + vault_config=vault_config,  | 
                    |
| 4158 | 
                        + ):  | 
                    |
| 4159 | 
                        + if mode == 'config':  | 
                    |
| 4160 | 
                        + _result = runner.invoke(  | 
                    |
| 4161 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 4162 | 
                        + ['--config', '--length=10', '--', key],  | 
                    |
| 4163 | 
                        + catch_exceptions=False,  | 
                    |
| 4164 | 
                        + )  | 
                    |
| 4165 | 
                        + else:  | 
                    |
| 4166 | 
                        + _result = runner.invoke(  | 
                    |
| 4167 | 
                        + cli.derivepassphrase_vault,  | 
                    |
| 4168 | 
                        + ['--import', '-'],  | 
                    |
| 4169 | 
                        + catch_exceptions=False,  | 
                    |
| 4170 | 
                        + input=json.dumps(config),  | 
                    |
| 4171 | 
                        + )  | 
                    |
| 4172 | 
                        + result = tests.ReadableResult.parse(_result)  | 
                    |
| 4173 | 
                        + assert result.clean_exit(), 'expected clean exit'  | 
                    |
| 4174 | 
                        + assert tests.warning_emitted(  | 
                    |
| 4175 | 
                        + 'contains an ASCII control character', caplog.record_tuples  | 
                    |
| 4176 | 
                        + ), 'expected known warning message in stderr'  | 
                    |
| 4177 | 
                        + assert tests.warning_emitted(  | 
                    |
| 4178 | 
                        + 'not be available for completion', caplog.record_tuples  | 
                    |
| 4179 | 
                        + ), 'expected known warning message in stderr'  | 
                    |
| 4180 | 
                        + assert cli._load_config() == config  | 
                    |
| 4181 | 
                        + comp = self.Completions(['vault'], incomplete)  | 
                    |
| 4182 | 
                        + assert frozenset(comp.get_words()) == completions  | 
                    |
| 4183 | 
                        +  | 
                    |
| 4184 | 
                        + def test_410a_service_name_exceptions_not_found(  | 
                    |
| 4185 | 
                        + self,  | 
                    |
| 4186 | 
                        + monkeypatch: pytest.MonkeyPatch,  | 
                    |
| 4187 | 
                        + ) -> None:  | 
                    |
| 4188 | 
                        + runner = click.testing.CliRunner(mix_stderr=False)  | 
                    |
| 4189 | 
                        + with tests.isolated_vault_config(  | 
                    |
| 4190 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 4191 | 
                        + runner=runner,  | 
                    |
| 4192 | 
                        +            vault_config={'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}},
                       | 
                    |
| 4193 | 
                        + ):  | 
                    |
| 4194 | 
                        + os.remove(cli._config_filename(subsystem='vault'))  | 
                    |
| 4195 | 
                        + assert not cli._shell_complete_service(  | 
                    |
| 4196 | 
                        + click.Context(cli.derivepassphrase),  | 
                    |
| 4197 | 
                        + click.Argument(['some_parameter']),  | 
                    |
| 4198 | 
                        + '',  | 
                    |
| 4199 | 
                        + )  | 
                    |
| 4200 | 
                        +  | 
                    |
| 4201 | 
                        +    @pytest.mark.parametrize('exc_type', [RuntimeError, KeyError, ValueError])
                       | 
                    |
| 4202 | 
                        + def test_410b_service_name_exceptions_custom_error(  | 
                    |
| 4203 | 
                        + self,  | 
                    |
| 4204 | 
                        + monkeypatch: pytest.MonkeyPatch,  | 
                    |
| 4205 | 
                        + exc_type: type[Exception],  | 
                    |
| 4206 | 
                        + ) -> None:  | 
                    |
| 4207 | 
                        + runner = click.testing.CliRunner(mix_stderr=False)  | 
                    |
| 4208 | 
                        + with tests.isolated_vault_config(  | 
                    |
| 4209 | 
                        + monkeypatch=monkeypatch,  | 
                    |
| 4210 | 
                        + runner=runner,  | 
                    |
| 4211 | 
                        +            vault_config={'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}},
                       | 
                    |
| 4212 | 
                        + ):  | 
                    |
| 4213 | 
                        +  | 
                    |
| 4214 | 
                        + def raiser(*_a: Any, **_kw: Any) -> NoReturn:  | 
                    |
| 4215 | 
                        +                raise exc_type('just being difficult')  # noqa: EM101,TRY003
                       | 
                    |
| 4216 | 
                        +  | 
                    |
| 4217 | 
                        + monkeypatch.setattr(cli, '_load_config', raiser)  | 
                    |
| 4218 | 
                        + assert not cli._shell_complete_service(  | 
                    |
| 4219 | 
                        + click.Context(cli.derivepassphrase),  | 
                    |
| 4220 | 
                        + click.Argument(['some_parameter']),  | 
                    |
| 4221 | 
                        + '',  | 
                    |
| 4222 | 
                        + )  | 
                    |
| 3493 | 4223 |