Reformat everything with ruff
Marco Ricci

Marco Ricci commited on 2024-07-21 10:09:10
Zeige 13 geänderte Dateien mit 1583 Einfügungen und 913 Löschungen.

... ...
@@ -122,6 +122,7 @@ extend = "ruff_defaults_v0.5.0.toml"
122 122
 
123 123
 [tool.ruff.format]
124 124
 quote-style = 'single'
125
+docstring-code-line-length = "dynamic"
125 126
 preview = true
126 127
 
127 128
 [tool.ruff.lint]
... ...
@@ -2,9 +2,7 @@
2 2
 #
3 3
 # SPDX-License-Identifier: MIT
4 4
 
5
-"""Work-alike of vault(1) – a deterministic, stateless password manager
6
-
7
-"""  # noqa: RUF002
5
+"""Work-alike of vault(1) – a deterministic, stateless password manager"""  # noqa: RUF002
8 6
 
9 7
 from __future__ import annotations
10 8
 
... ...
@@ -19,8 +17,9 @@ from typing_extensions import assert_type
19 17
 import sequin
20 18
 import ssh_agent_client
21 19
 
22
-__author__ = "Marco Ricci <m@the13thletter.info>"
23
-__version__ = "0.1.1"
20
+__author__ = 'Marco Ricci <m@the13thletter.info>'
21
+__version__ = '0.1.1'
22
+
24 23
 
25 24
 class AmbiguousByteRepresentationError(ValueError):
26 25
     """The object has an ambiguous byte representation."""
... ...
@@ -40,8 +40,10 @@ _CHARSETS = collections.OrderedDict([
40 40
 ])
41 41
 _CHARSETS['alpha'] = _CHARSETS['lower'] + _CHARSETS['upper']
42 42
 _CHARSETS['alphanum'] = _CHARSETS['alpha'] + _CHARSETS['number']
43
-_CHARSETS['all'] = (_CHARSETS['alphanum'] + _CHARSETS['space']
44
-                    + _CHARSETS['symbol'])
43
+_CHARSETS['all'] = (
44
+    _CHARSETS['alphanum'] + _CHARSETS['space'] + _CHARSETS['symbol']
45
+)
46
+
45 47
 
46 48
 class Vault:
47 49
     """A work-alike of James Coglan's vault.
... ...
@@ -75,10 +78,16 @@ class Vault:
75 78
     """
76 79
 
77 80
     def __init__(
78
-        self, *, phrase: bytes | bytearray | str = b'',
79
-        length: int = 20, repeat: int = 0, lower: int | None = None,
80
-        upper: int | None = None, number: int | None = None,
81
-        space: int | None = None, dash: int | None = None,
81
+        self,
82
+        *,
83
+        phrase: bytes | bytearray | str = b'',
84
+        length: int = 20,
85
+        repeat: int = 0,
86
+        lower: int | None = None,
87
+        upper: int | None = None,
88
+        number: int | None = None,
89
+        space: int | None = None,
90
+        dash: int | None = None,
82 91
         symbol: int | None = None,
83 92
     ) -> None:
84 93
         """Initialize the Vault object.
... ...
@@ -177,7 +188,8 @@ class Vault:
177 188
         return math.fsum(math.log2(f) for f in factors)
178 189
 
179 190
     def _estimate_sufficient_hash_length(
180
-        self, safety_factor: float = 2.0,
191
+        self,
192
+        safety_factor: float = 2.0,
181 193
     ) -> int:
182 194
         """Estimate the sufficient hash length, given the current settings.
183 195
 
... ...
@@ -241,8 +253,11 @@ class Vault:
241 253
 
242 254
     @classmethod
243 255
     def create_hash(
244
-        cls, phrase: bytes | bytearray | str,
245
-        service: bytes | bytearray, *, length: int = 32,
256
+        cls,
257
+        phrase: bytes | bytearray | str,
258
+        service: bytes | bytearray,
259
+        *,
260
+        length: int = 32,
246 261
     ) -> bytes:
247 262
         r"""Create a pseudorandom byte stream from phrase and service.
248 263
 
... ...
@@ -302,11 +317,19 @@ class Vault:
302 317
         phrase = cls._get_binary_string(phrase)
303 318
         assert not isinstance(phrase, str)
304 319
         salt = bytes(service) + cls._UUID
305
-        return hashlib.pbkdf2_hmac(hash_name='sha1', password=phrase,
306
-                                   salt=salt, iterations=8, dklen=length)
320
+        return hashlib.pbkdf2_hmac(
321
+            hash_name='sha1',
322
+            password=phrase,
323
+            salt=salt,
324
+            iterations=8,
325
+            dklen=length,
326
+        )
307 327
 
308 328
     def generate(
309
-        self, service_name: str | bytes | bytearray, /, *,
329
+        self,
330
+        service_name: str | bytes | bytearray,
331
+        /,
332
+        *,
310 333
         phrase: bytes | bytearray | str = b'',
311 334
     ) -> bytes:
312 335
         r"""Generate a service passphrase.
... ...
@@ -358,8 +381,11 @@ class Vault:
358 381
         while True:
359 382
             try:
360 383
                 required = self._required[:]
361
-                seq = sequin.Sequin(self.create_hash(
362
-                    phrase=phrase, service=service_name, length=hash_length))
384
+                seq = sequin.Sequin(
385
+                    self.create_hash(
386
+                        phrase=phrase, service=service_name, length=hash_length
387
+                    )
388
+                )
363 389
                 result = bytearray()
364 390
                 while len(result) < self._length:
365 391
                     pos = seq.generate(len(required))
... ...
@@ -374,8 +400,9 @@ class Vault:
374 400
                     if self._repeat and result:
375 401
                         bad_suffix = bytes(result[-1:]) * (self._repeat - 1)
376 402
                         if result.endswith(bad_suffix):
377
-                            charset = self._subtract(bytes(result[-1:]),
378
-                                                     charset)
403
+                            charset = self._subtract(
404
+                                bytes(result[-1:]), charset
405
+                            )
379 406
                     pos = seq.generate(len(charset))
380 407
                     result.extend(charset[pos : pos + 1])
381 408
             except sequin.SequinExhaustedError:
... ...
@@ -399,19 +426,16 @@ class Vault:
399 426
 
400 427
         """
401 428
         deterministic_signature_types = {
402
-            'ssh-ed25519':
403
-                lambda k: k.startswith(b'\x00\x00\x00\x0bssh-ed25519'),
404
-            'ssh-ed448':
405
-                lambda k: k.startswith(b'\x00\x00\x00\x09ssh-ed448'),
406
-            'ssh-rsa':
407
-                lambda k: k.startswith(b'\x00\x00\x00\x07ssh-rsa'),
429
+            'ssh-ed25519': lambda k: k.startswith(
430
+                b'\x00\x00\x00\x0bssh-ed25519'
431
+            ),
432
+            'ssh-ed448': lambda k: k.startswith(b'\x00\x00\x00\x09ssh-ed448'),
433
+            'ssh-rsa': lambda k: k.startswith(b'\x00\x00\x00\x07ssh-rsa'),
408 434
         }
409 435
         return any(v(key) for v in deterministic_signature_types.values())
410 436
 
411 437
     @classmethod
412
-    def phrase_from_key(
413
-        cls, key: bytes | bytearray, /
414
-    ) -> bytes:
438
+    def phrase_from_key(cls, key: bytes | bytearray, /) -> bytes:
415 439
         """Obtain the master passphrase from a configured SSH key.
416 440
 
417 441
         vault allows the usage of certain SSH keys to derive a master
... ...
@@ -456,8 +480,10 @@ class Vault:
456 480
 
457 481
         """
458 482
         if not cls._is_suitable_ssh_key(key):
459
-            msg = ('unsuitable SSH key: bad key, or '
460
-                   'signature not deterministic')
483
+            msg = (
484
+                'unsuitable SSH key: bad key, or '
485
+                'signature not deterministic'
486
+            )
461 487
             raise ValueError(msg)
462 488
         with ssh_agent_client.SSHAgentClient() as client:
463 489
             raw_sig = client.sign(key, cls._UUID)
... ...
@@ -467,7 +493,8 @@ class Vault:
467 493
 
468 494
     @staticmethod
469 495
     def _subtract(
470
-        charset: bytes | bytearray, allowed: bytes | bytearray,
496
+        charset: bytes | bytearray,
497
+        allowed: bytes | bytearray,
471 498
     ) -> bytearray:
472 499
         """Remove the characters in charset from allowed.
473 500
 
... ...
@@ -489,8 +516,9 @@ class Vault:
489 516
                 `allowed` or `charset` contained duplicate characters.
490 517
 
491 518
         """
492
-        allowed = (allowed if isinstance(allowed, bytearray)
493
-                   else bytearray(allowed))
519
+        allowed = (
520
+            allowed if isinstance(allowed, bytearray) else bytearray(allowed)
521
+        )
494 522
         assert_type(allowed, bytearray)
495 523
         msg_dup_characters = 'duplicate characters in set'
496 524
         if len(frozenset(allowed)) != len(allowed):
... ...
@@ -5,7 +5,7 @@
5 5
 
6 6
 import sys
7 7
 
8
-if __name__ == "__main__":
8
+if __name__ == '__main__':
9 9
     from derivepassphrase.cli import derivepassphrase
10 10
 
11 11
     sys.exit(derivepassphrase())
... ...
@@ -2,9 +2,7 @@
2 2
 #
3 3
 # SPDX-License-Identifier: MIT
4 4
 
5
-"""Command-line interface for derivepassphrase.
6
-
7
-"""
5
+"""Command-line interface for derivepassphrase."""
8 6
 
9 7
 from __future__ import annotations
10 8
 
... ...
@@ -64,8 +62,9 @@ def _config_filename() -> str | bytes | pathlib.Path:
64 62
 
65 63
     """
66 64
     path: str | bytes | pathlib.Path
67
-    path = (os.getenv(PROG_NAME.upper() + '_PATH')
68
-            or click.get_app_dir(PROG_NAME, force_posix=True))
65
+    path = os.getenv(PROG_NAME.upper() + '_PATH') or click.get_app_dir(
66
+        PROG_NAME, force_posix=True
67
+    )
69 68
     return os.path.join(path, 'settings.json')
70 69
 
71 70
 
... ...
@@ -122,8 +121,7 @@ def _save_config(config: dpp_types.VaultConfig, /) -> None:
122 121
 
123 122
 
124 123
 def _get_suitable_ssh_keys(
125
-    conn: ssh_agent_client.SSHAgentClient | socket.socket | None = None,
126
-    /
124
+    conn: ssh_agent_client.SSHAgentClient | socket.socket | None = None, /
127 125
 ) -> Iterator[ssh_agent_client.types.KeyCommentPair]:
128 126
     """Yield all SSH keys suitable for passphrase derivation.
129 127
 
... ...
@@ -188,7 +186,8 @@ def _get_suitable_ssh_keys(
188 186
 
189 187
 
190 188
 def _prompt_for_selection(
191
-    items: Sequence[str | bytes], heading: str = 'Possible choices:',
189
+    items: Sequence[str | bytes],
190
+    heading: str = 'Possible choices:',
192 191
     single_choice_prompt: str = 'Confirm this choice?',
193 192
 ) -> int:
194 193
     """Prompt user for a choice among the given items.
... ...
@@ -229,26 +228,34 @@ def _prompt_for_selection(
229 228
         choices = click.Choice([''] + [str(i) for i in range(1, n + 1)])
230 229
         choice = click.prompt(
231 230
             f'Your selection? (1-{n}, leave empty to abort)',
232
-            err=True, type=choices, show_choices=False,
233
-            show_default=False, default='')
231
+            err=True,
232
+            type=choices,
233
+            show_choices=False,
234
+            show_default=False,
235
+            default='',
236
+        )
234 237
         if not choice:
235 238
             raise IndexError(_EMPTY_SELECTION)
236 239
         return int(choice) - 1
237
-    prompt_suffix = (' '
238
-                     if single_choice_prompt.endswith(tuple('?.!'))
239
-                     else ': ')
240
+    prompt_suffix = (
241
+        ' ' if single_choice_prompt.endswith(tuple('?.!')) else ': '
242
+    )
240 243
     try:
241
-        click.confirm(single_choice_prompt,
242
-                      prompt_suffix=prompt_suffix, err=True,
243
-                      abort=True, default=False, show_default=False)
244
+        click.confirm(
245
+            single_choice_prompt,
246
+            prompt_suffix=prompt_suffix,
247
+            err=True,
248
+            abort=True,
249
+            default=False,
250
+            show_default=False,
251
+        )
244 252
     except click.Abort:
245 253
         raise IndexError(_EMPTY_SELECTION) from None
246 254
     return 0
247 255
 
248 256
 
249 257
 def _select_ssh_key(
250
-    conn: ssh_agent_client.SSHAgentClient | socket.socket | None = None,
251
-    /
258
+    conn: ssh_agent_client.SSHAgentClient | socket.socket | None = None, /
252 259
 ) -> bytes | bytearray:
253 260
     """Interactively select an SSH key for passphrase derivation.
254 261
 
... ...
@@ -292,14 +299,18 @@ def _select_ssh_key(
292 299
     for key, comment in suitable_keys:
293 300
         keytype = unstring_prefix(key)[0].decode('ASCII')
294 301
         key_str = base64.standard_b64encode(key).decode('ASCII')
295
-        key_prefix = (key_str
302
+        key_prefix = (
303
+            key_str
296 304
             if len(key_str) < KEY_DISPLAY_LENGTH + len('...')
297
-                      else key_str[:KEY_DISPLAY_LENGTH] + '...')
305
+            else key_str[:KEY_DISPLAY_LENGTH] + '...'
306
+        )
298 307
         comment_str = comment.decode('UTF-8', errors='replace')
299 308
         key_listing.append(f'{keytype} {key_prefix} {comment_str}')
300 309
     choice = _prompt_for_selection(
301
-        key_listing, heading='Suitable SSH keys:',
302
-        single_choice_prompt='Use this key?')
310
+        key_listing,
311
+        heading='Suitable SSH keys:',
312
+        single_choice_prompt='Use this key?',
313
+    )
303 314
     return suitable_keys[choice].key
304 315
 
305 316
 
... ...
@@ -313,8 +324,9 @@ def _prompt_for_passphrase() -> str:
313 324
         The user input.
314 325
 
315 326
     """
316
-    return click.prompt('Passphrase', default='', hide_input=True,
317
-                        show_default=False, err=True)
327
+    return click.prompt(
328
+        'Passphrase', default='', hide_input=True, show_default=False, err=True
329
+    )
318 330
 
319 331
 
320 332
 class OptionGroupOption(click.Option):
... ...
@@ -353,7 +366,9 @@ class CommandWithHelpGroups(click.Command):
353 366
     """
354 367
 
355 368
     def format_options(
356
-        self, ctx: click.Context, formatter: click.HelpFormatter,
369
+        self,
370
+        ctx: click.Context,
371
+        formatter: click.HelpFormatter,
357 372
     ) -> None:
358 373
         r"""Format options on the help listing, grouped into sections.
359 374
 
... ...
@@ -398,8 +413,9 @@ class CommandWithHelpGroups(click.Command):
398 413
                     group_name = ''
399 414
                 help_records.setdefault(group_name, []).append(rec)
400 415
         default_group = help_records.pop('')
401
-        default_group_name = ('Other Options' if len(default_group) > 1
402
-                              else 'Options')
416
+        default_group_name = (
417
+            'Other Options' if len(default_group) > 1 else 'Options'
418
+        )
403 419
         help_records[default_group_name] = default_group
404 420
         for group_name, records in help_records.items():
405 421
             with formatter.section(group_name):
... ...
@@ -414,31 +430,37 @@ class CommandWithHelpGroups(click.Command):
414 430
 # Concrete option groups used by this command-line interface.
415 431
 class PasswordGenerationOption(OptionGroupOption):
416 432
     """Password generation options for the CLI."""
433
+
417 434
     option_group_name = 'Password generation'
418
-    epilog = '''
435
+    epilog = """
419 436
         Use NUMBER=0, e.g. "--symbol 0", to exclude a character type
420 437
         from the output.
421
-    '''
438
+    """
422 439
 
423 440
 
424 441
 class ConfigurationOption(OptionGroupOption):
425 442
     """Configuration options for the CLI."""
443
+
426 444
     option_group_name = 'Configuration'
427
-    epilog = '''
445
+    epilog = """
428 446
         Use $VISUAL or $EDITOR to configure the spawned editor.
429
-    '''
447
+    """
430 448
 
431 449
 
432 450
 class StorageManagementOption(OptionGroupOption):
433 451
     """Storage management options for the CLI."""
452
+
434 453
     option_group_name = 'Storage management'
435
-    epilog = '''
454
+    epilog = """
436 455
         Using "-" as PATH for standard input/standard output is
437 456
         supported.
438
-    '''
457
+    """
458
+
439 459
 
440 460
 def _validate_occurrence_constraint(
441
-    ctx: click.Context, param: click.Parameter, value: Any,
461
+    ctx: click.Context,
462
+    param: click.Parameter,
463
+    value: Any,
442 464
 ) -> int | None:
443 465
     """Check that the occurrence constraint is valid (int, 0 or larger)."""
444 466
     del ctx  # Unused.
... ...
@@ -460,7 +482,9 @@ def _validate_occurrence_constraint(
460 482
 
461 483
 
462 484
 def _validate_length(
463
-    ctx: click.Context, param: click.Parameter, value: Any,
485
+    ctx: click.Context,
486
+    param: click.Parameter,
487
+    value: Any,
464 488
 ) -> int | None:
465 489
     """Check that the length is valid (int, 1 or larger)."""
466 490
     del ctx  # Unused.
... ...
@@ -480,7 +504,8 @@ def _validate_length(
480 504
         raise click.BadParameter(msg)
481 505
     return int_value
482 506
 
483
-DEFAULT_NOTES_TEMPLATE = '''\
507
+
508
+DEFAULT_NOTES_TEMPLATE = """\
484 509
 # Enter notes below the line with the cut mark (ASCII scissors and
485 510
 # dashes).  Lines above the cut mark (such as this one) will be ignored.
486 511
 #
... ...
@@ -490,14 +515,14 @@ DEFAULT_NOTES_TEMPLATE = '''\
490 515
 # retained.
491 516
 #
492 517
 # - - - - - >8 - - - - - >8 - - - - - >8 - - - - - >8 - - - - -
493
-'''
518
+"""
494 519
 DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -'
495 520
 
496 521
 
497 522
 @click.command(
498
-    context_settings={"help_option_names": ["-h", "--help"]},
523
+    context_settings={'help_option_names': ['-h', '--help']},
499 524
     cls=CommandWithHelpGroups,
500
-    epilog=r'''
525
+    epilog=r"""
501 526
         WARNING: There is NO WAY to retrieve the generated passphrases
502 527
         if the master passphrase, the SSH key, or the exact passphrase
503 528
         settings are lost, short of trying out all possible
... ...
@@ -510,74 +535,145 @@ DEFAULT_NOTES_MARKER = '# - - - - - >8 - - - - -'
510 535
         `C:\Users\<user>\AppData\Roaming\Derivepassphrase` on Windows.
511 536
         The configuration is NOT encrypted, and you are STRONGLY
512 537
         discouraged from using a stored passphrase.
513
-    ''',
538
+    """,
514 539
 )
515
-@click.option('-p', '--phrase', 'use_phrase', is_flag=True,
540
+@click.option(
541
+    '-p',
542
+    '--phrase',
543
+    'use_phrase',
544
+    is_flag=True,
516 545
     help='prompts you for your passphrase',
517
-              cls=PasswordGenerationOption)
518
-@click.option('-k', '--key', 'use_key', is_flag=True,
546
+    cls=PasswordGenerationOption,
547
+)
548
+@click.option(
549
+    '-k',
550
+    '--key',
551
+    'use_key',
552
+    is_flag=True,
519 553
     help='uses your SSH private key to generate passwords',
520
-              cls=PasswordGenerationOption)
521
-@click.option('-l', '--length', metavar='NUMBER',
554
+    cls=PasswordGenerationOption,
555
+)
556
+@click.option(
557
+    '-l',
558
+    '--length',
559
+    metavar='NUMBER',
522 560
     callback=_validate_length,
523 561
     help='emits password of length NUMBER',
524
-              cls=PasswordGenerationOption)
525
-@click.option('-r', '--repeat', metavar='NUMBER',
562
+    cls=PasswordGenerationOption,
563
+)
564
+@click.option(
565
+    '-r',
566
+    '--repeat',
567
+    metavar='NUMBER',
526 568
     callback=_validate_occurrence_constraint,
527 569
     help='allows maximum of NUMBER repeated adjacent chars',
528
-              cls=PasswordGenerationOption)
529
-@click.option('--lower', metavar='NUMBER',
570
+    cls=PasswordGenerationOption,
571
+)
572
+@click.option(
573
+    '--lower',
574
+    metavar='NUMBER',
530 575
     callback=_validate_occurrence_constraint,
531 576
     help='includes at least NUMBER lowercase letters',
532
-              cls=PasswordGenerationOption)
533
-@click.option('--upper', metavar='NUMBER',
577
+    cls=PasswordGenerationOption,
578
+)
579
+@click.option(
580
+    '--upper',
581
+    metavar='NUMBER',
534 582
     callback=_validate_occurrence_constraint,
535 583
     help='includes at least NUMBER uppercase letters',
536
-              cls=PasswordGenerationOption)
537
-@click.option('--number', metavar='NUMBER',
584
+    cls=PasswordGenerationOption,
585
+)
586
+@click.option(
587
+    '--number',
588
+    metavar='NUMBER',
538 589
     callback=_validate_occurrence_constraint,
539 590
     help='includes at least NUMBER digits',
540
-              cls=PasswordGenerationOption)
541
-@click.option('--space', metavar='NUMBER',
591
+    cls=PasswordGenerationOption,
592
+)
593
+@click.option(
594
+    '--space',
595
+    metavar='NUMBER',
542 596
     callback=_validate_occurrence_constraint,
543 597
     help='includes at least NUMBER spaces',
544
-              cls=PasswordGenerationOption)
545
-@click.option('--dash', metavar='NUMBER',
598
+    cls=PasswordGenerationOption,
599
+)
600
+@click.option(
601
+    '--dash',
602
+    metavar='NUMBER',
546 603
     callback=_validate_occurrence_constraint,
547 604
     help='includes at least NUMBER "-" or "_"',
548
-              cls=PasswordGenerationOption)
549
-@click.option('--symbol', metavar='NUMBER',
605
+    cls=PasswordGenerationOption,
606
+)
607
+@click.option(
608
+    '--symbol',
609
+    metavar='NUMBER',
550 610
     callback=_validate_occurrence_constraint,
551 611
     help='includes at least NUMBER symbol chars',
552
-              cls=PasswordGenerationOption)
553
-@click.option('-n', '--notes', 'edit_notes', is_flag=True,
612
+    cls=PasswordGenerationOption,
613
+)
614
+@click.option(
615
+    '-n',
616
+    '--notes',
617
+    'edit_notes',
618
+    is_flag=True,
554 619
     help='spawn an editor to edit notes for SERVICE',
555
-              cls=ConfigurationOption)
556
-@click.option('-c', '--config', 'store_config_only', is_flag=True,
620
+    cls=ConfigurationOption,
621
+)
622
+@click.option(
623
+    '-c',
624
+    '--config',
625
+    'store_config_only',
626
+    is_flag=True,
557 627
     help='saves the given settings for SERVICE or global',
558
-              cls=ConfigurationOption)
559
-@click.option('-x', '--delete', 'delete_service_settings', is_flag=True,
628
+    cls=ConfigurationOption,
629
+)
630
+@click.option(
631
+    '-x',
632
+    '--delete',
633
+    'delete_service_settings',
634
+    is_flag=True,
560 635
     help='deletes settings for SERVICE',
561
-              cls=ConfigurationOption)
562
-@click.option('--delete-globals', is_flag=True,
636
+    cls=ConfigurationOption,
637
+)
638
+@click.option(
639
+    '--delete-globals',
640
+    is_flag=True,
563 641
     help='deletes the global shared settings',
564
-              cls=ConfigurationOption)
565
-@click.option('-X', '--clear', 'clear_all_settings', is_flag=True,
642
+    cls=ConfigurationOption,
643
+)
644
+@click.option(
645
+    '-X',
646
+    '--clear',
647
+    'clear_all_settings',
648
+    is_flag=True,
566 649
     help='deletes all settings',
567
-              cls=ConfigurationOption)
568
-@click.option('-e', '--export', 'export_settings', metavar='PATH',
650
+    cls=ConfigurationOption,
651
+)
652
+@click.option(
653
+    '-e',
654
+    '--export',
655
+    'export_settings',
656
+    metavar='PATH',
569 657
     type=click.Path(file_okay=True, allow_dash=True, exists=False),
570 658
     help='export all saved settings into file PATH',
571
-              cls=StorageManagementOption)
572
-@click.option('-i', '--import', 'import_settings', metavar='PATH',
659
+    cls=StorageManagementOption,
660
+)
661
+@click.option(
662
+    '-i',
663
+    '--import',
664
+    'import_settings',
665
+    metavar='PATH',
573 666
     type=click.Path(file_okay=True, allow_dash=True, exists=False),
574 667
     help='import saved settings from file PATH',
575
-              cls=StorageManagementOption)
668
+    cls=StorageManagementOption,
669
+)
576 670
 @click.version_option(version=dpp.__version__, prog_name=PROG_NAME)
577 671
 @click.argument('service', required=False)
578 672
 @click.pass_context
579 673
 def derivepassphrase(
580
-    ctx: click.Context, /, *,
674
+    ctx: click.Context,
675
+    /,
676
+    *,
581 677
     service: str | None = None,
582 678
     use_phrase: bool = False,
583 679
     use_key: bool = False,
... ...
@@ -704,7 +800,8 @@ def derivepassphrase(
704 800
                     group = StorageManagementOption
705 801
                 case OptionGroupOption():
706 802
                     raise AssertionError(  # noqa: TRY003
707
-                        f'Unknown option group for {param!r}')  # noqa: EM102
803
+                        f'Unknown option group for {param!r}'  # noqa: EM102
804
+                    )
708 805
                 case _:
709 806
                     group = click.Option
710 807
             options_in_group.setdefault(group, []).append(param)
... ...
@@ -716,7 +813,8 @@ def derivepassphrase(
716 813
         return bool(ctx.params.get(param.human_readable_name))
717 814
 
718 815
     def check_incompatible_options(
719
-        param: click.Parameter | str, *incompatible: click.Parameter | str,
816
+        param: click.Parameter | str,
817
+        *incompatible: click.Parameter | str,
720 818
     ) -> None:
721 819
         if isinstance(param, str):
722 820
             param = params_by_str[param]
... ...
@@ -731,7 +829,8 @@ def derivepassphrase(
731 829
                 opt_str = param.opts[0]
732 830
                 other_str = other.opts[0]
733 831
                 raise click.BadOptionUsage(
734
-                    opt_str, f'mutually exclusive with {other_str}', ctx=ctx)
832
+                    opt_str, f'mutually exclusive with {other_str}', ctx=ctx
833
+                )
735 834
 
736 835
     def get_config() -> dpp_types.VaultConfig:
737 836
         try:
... ...
@@ -748,15 +847,20 @@ def derivepassphrase(
748 847
         for opt in options_in_group[group]:
749 848
             if opt != params_by_str['--config']:
750 849
                 check_incompatible_options(
751
-                    opt, *options_in_group[PasswordGenerationOption])
850
+                    opt, *options_in_group[PasswordGenerationOption]
851
+                )
752 852
 
753 853
     for group in (ConfigurationOption, StorageManagementOption):
754 854
         for opt in options_in_group[group]:
755 855
             check_incompatible_options(
756
-                opt, *options_in_group[ConfigurationOption],
757
-                *options_in_group[StorageManagementOption])
758
-    sv_options = (options_in_group[PasswordGenerationOption] +
759
-                  [params_by_str['--notes'], params_by_str['--delete']])
856
+                opt,
857
+                *options_in_group[ConfigurationOption],
858
+                *options_in_group[StorageManagementOption],
859
+            )
860
+    sv_options = options_in_group[PasswordGenerationOption] + [
861
+        params_by_str['--notes'],
862
+        params_by_str['--delete'],
863
+    ]
760 864
     sv_options.remove(params_by_str['--key'])
761 865
     sv_options.remove(params_by_str['--phrase'])
762 866
     for param in sv_options:
... ...
@@ -765,16 +869,17 @@ def derivepassphrase(
765 869
             msg = f'{opt_str} requires a SERVICE'
766 870
             raise click.UsageError(msg)
767 871
     for param in [params_by_str['--key'], params_by_str['--phrase']]:
768
-        if (
769
-            is_param_set(param)
770
-            and not (service or is_param_set(params_by_str['--config']))
872
+        if is_param_set(param) and not (
873
+            service or is_param_set(params_by_str['--config'])
771 874
         ):
772 875
             opt_str = param.opts[0]
773 876
             msg = f'{opt_str} requires a SERVICE or --config'
774 877
             raise click.UsageError(msg)
775
-    no_sv_options = [params_by_str['--delete-globals'],
878
+    no_sv_options = [
879
+        params_by_str['--delete-globals'],
776 880
         params_by_str['--clear'],
777
-                     *options_in_group[StorageManagementOption]]
881
+        *options_in_group[StorageManagementOption],
882
+    ]
778 883
     for param in no_sv_options:
779 884
         if is_param_set(param) and service:
780 885
             opt_str = param.opts[0]
... ...
@@ -784,10 +889,9 @@ def derivepassphrase(
784 889
     if edit_notes:
785 890
         assert service is not None
786 891
         configuration = get_config()
787
-        text = (DEFAULT_NOTES_TEMPLATE +
788
-                configuration['services']
789
-                .get(service, cast(dpp_types.VaultConfigServicesSettings, {}))
790
-                .get('notes', ''))
892
+        text = DEFAULT_NOTES_TEMPLATE + configuration['services'].get(
893
+            service, cast(dpp_types.VaultConfigServicesSettings, {})
894
+        ).get('notes', '')
791 895
         notes_value = click.edit(text=text)
792 896
         if notes_value is not None:
793 897
             notes_lines = collections.deque(notes_value.splitlines(True))
... ...
@@ -800,7 +904,8 @@ def derivepassphrase(
800 904
                 if not notes_value.strip():
801 905
                     ctx.fail('not saving new notes: user aborted request')
802 906
             configuration['services'].setdefault(service, {})['notes'] = (
803
-                notes_value.strip('\n'))
907
+                notes_value.strip('\n')
908
+            )
804 909
             _save_config(configuration)
805 910
     elif delete_service_settings:
806 911
         assert service is not None
... ...
@@ -818,9 +923,11 @@ def derivepassphrase(
818 923
     elif import_settings:
819 924
         try:
820 925
             # TODO: keep track of auto-close; try os.dup if feasible
821
-            infile = (cast(TextIO, import_settings)
926
+            infile = (
927
+                cast(TextIO, import_settings)
822 928
                 if hasattr(import_settings, 'close')
823
-                      else click.open_file(os.fspath(import_settings), 'rt'))
929
+                else click.open_file(os.fspath(import_settings), 'rt')
930
+            )
824 931
             with infile:
825 932
                 maybe_config = json.load(infile)
826 933
         except json.JSONDecodeError as e:
... ...
@@ -835,9 +942,11 @@ def derivepassphrase(
835 942
         configuration = get_config()
836 943
         try:
837 944
             # TODO: keep track of auto-close; try os.dup if feasible
838
-            outfile = (cast(TextIO, export_settings)
945
+            outfile = (
946
+                cast(TextIO, export_settings)
839 947
                 if hasattr(export_settings, 'close')
840
-                       else click.open_file(os.fspath(export_settings), 'wt'))
948
+                else click.open_file(os.fspath(export_settings), 'wt')
949
+            )
841 950
             with outfile:
842 951
                 json.dump(configuration, outfile)
843 952
         except OSError as e:
... ...
@@ -849,20 +958,36 @@ def derivepassphrase(
849 958
         # have a type guarding function anyway, assert that we didn't
850 959
         # make any mistakes at the end instead.
851 960
         global_keys = {'key', 'phrase'}
852
-        service_keys = {'key', 'phrase', 'length', 'repeat', 'lower',
853
-                        'upper', 'number', 'space', 'dash', 'symbol'}
961
+        service_keys = {
962
+            'key',
963
+            'phrase',
964
+            'length',
965
+            'repeat',
966
+            'lower',
967
+            'upper',
968
+            'number',
969
+            'space',
970
+            'dash',
971
+            'symbol',
972
+        }
854 973
         settings: collections.ChainMap[str, Any] = collections.ChainMap(
855
-            {k: v for k, v in locals().items()
856
-             if k in service_keys and v is not None},
857
-            cast(dict[str, Any],
858
-                 configuration['services'].get(service or '', {})),
974
+            {
975
+                k: v
976
+                for k, v in locals().items()
977
+                if k in service_keys and v is not None
978
+            },
979
+            cast(
980
+                dict[str, Any],
981
+                configuration['services'].get(service or '', {}),
982
+            ),
859 983
             {},
860
-            cast(dict[str, Any], configuration.get('global', {}))
984
+            cast(dict[str, Any], configuration.get('global', {})),
861 985
         )
862 986
         if use_key:
863 987
             try:
864
-                key = base64.standard_b64encode(
865
-                    _select_ssh_key()).decode('ASCII')
988
+                key = base64.standard_b64encode(_select_ssh_key()).decode(
989
+                    'ASCII'
990
+                )
866 991
             except IndexError:
867 992
                 ctx.fail('no valid SSH key selected')
868 993
             except (LookupError, RuntimeError) as e:
... ...
@@ -875,8 +1000,11 @@ def derivepassphrase(
875 1000
                 phrase = maybe_phrase
876 1001
         if store_config_only:
877 1002
             view: collections.ChainMap[str, Any]
878
-            view = (collections.ChainMap(*settings.maps[:2]) if service
879
-                    else settings.parents.parents)
1003
+            view = (
1004
+                collections.ChainMap(*settings.maps[:2])
1005
+                if service
1006
+                else settings.parents.parents
1007
+            )
880 1008
             if use_key:
881 1009
                 view['key'] = key
882 1010
                 for m in view.maps:
... ...
@@ -887,25 +1015,29 @@ def derivepassphrase(
887 1015
                     m.pop('key', '')
888 1016
             if not view.maps[0]:
889 1017
                 settings_type = 'service' if service else 'global'
890
-                msg = (f'cannot update {settings_type} settings without '
891
-                       f'actual settings')
1018
+                msg = (
1019
+                    f'cannot update {settings_type} settings without '
1020
+                    f'actual settings'
1021
+                )
892 1022
                 raise click.UsageError(msg)
893 1023
             if service:
894
-                configuration['services'].setdefault(
895
-                    service, {}).update(view)  # type: ignore[typeddict-item]
1024
+                configuration['services'].setdefault(service, {}).update(view)  # type: ignore[typeddict-item]
896 1025
             else:
897
-                configuration.setdefault(
898
-                    'global', {}).update(view)  # type: ignore[typeddict-item]
899
-            assert dpp_types.is_vault_config(configuration), (
900
-                f'invalid vault configuration: {configuration!r}'
901
-            )
1026
+                configuration.setdefault('global', {}).update(view)  # type: ignore[typeddict-item]
1027
+            assert dpp_types.is_vault_config(
1028
+                configuration
1029
+            ), f'invalid vault configuration: {configuration!r}'
902 1030
             _save_config(configuration)
903 1031
         else:
904 1032
             if not service:
905 1033
                 msg = 'SERVICE is required'
906 1034
                 raise click.UsageError(msg)
907
-            kwargs: dict[str, Any] = {k: v for k, v in settings.items()
908
-                                      if k in service_keys and v is not None}
1035
+            kwargs: dict[str, Any] = {
1036
+                k: v
1037
+                for k, v in settings.items()
1038
+                if k in service_keys and v is not None
1039
+            }
1040
+
909 1041
             # If either --key or --phrase are given, use that setting.
910 1042
             # Otherwise, if both key and phrase are set in the config,
911 1043
             # one must be global (ignore it) and one must be
... ...
@@ -915,10 +1047,12 @@ def derivepassphrase(
915 1047
             # derivepassphrase.Vault.phrase_from_key if a key is
916 1048
             # given. Finally, if nothing is set, error out.
917 1049
             def key_to_phrase(
918
-                key: str | bytes | bytearray
1050
+                key: str | bytes | bytearray,
919 1051
             ) -> bytes | bytearray:
920 1052
                 return dpp.Vault.phrase_from_key(
921
-                    base64.standard_b64decode(key))
1053
+                    base64.standard_b64decode(key)
1054
+                )
1055
+
922 1056
             if use_key or use_phrase:
923 1057
                 if use_key:
924 1058
                     kwargs['phrase'] = key_to_phrase(key)
... ...
@@ -935,8 +1069,10 @@ def derivepassphrase(
935 1069
             elif kwargs.get('phrase'):
936 1070
                 pass
937 1071
             else:
938
-                msg = ('no passphrase or key given on command-line '
939
-                       'or in configuration')
1072
+                msg = (
1073
+                    'no passphrase or key given on command-line '
1074
+                    'or in configuration'
1075
+                )
940 1076
                 raise click.UsageError(msg)
941 1077
             vault = dpp.Vault(**kwargs)
942 1078
             result = vault.generate(service)
... ...
@@ -2,9 +2,7 @@
2 2
 #
3 3
 # SPDX-License-Identifier: MIT
4 4
 
5
-"""Common typing declarations for the parent module.
6
-
7
-"""
5
+"""Common typing declarations for the parent module."""
8 6
 
9 7
 from __future__ import annotations
10 8
 
... ...
@@ -79,9 +80,13 @@ class VaultConfigServicesSettings(VaultConfigGlobalSettings, total=False):
79 80
     symbol: NotRequired[int]
80 81
 
81 82
 
82
-_VaultConfig = TypedDict('_VaultConfig',
83
+_VaultConfig = TypedDict(
84
+    '_VaultConfig',
83 85
     {'global': NotRequired[VaultConfigGlobalSettings]},
84
-                         total=False)
86
+    total=False,
87
+)
88
+
89
+
85 90
 class VaultConfig(TypedDict, _VaultConfig, total=False):
86 91
     r"""Configuration for vault.
87 92
 
... ...
@@ -32,8 +32,9 @@ if TYPE_CHECKING:
32 32
     from collections.abc import Iterator, Sequence
33 33
 
34 34
 __all__ = ('Sequin', 'SequinExhaustedError')
35
-__author__ = "Marco Ricci <m@the13thletter.info>"
36
-__version__ = "0.1.1"
35
+__author__ = 'Marco Ricci <m@the13thletter.info>'
36
+__version__ = '0.1.1'
37
+
37 38
 
38 39
 class Sequin:
39 40
     """Generate pseudorandom non-negative numbers in different ranges.
... ...
@@ -60,7 +62,9 @@ class Sequin:
60 62
     def __init__(
61 63
         self,
62 64
         sequence: str | bytes | bytearray | Sequence[int],
63
-        /, *, is_bitstring: bool = False
65
+        /,
66
+        *,
67
+        is_bitstring: bool = False,
64 68
     ):
65 69
         """Initialize the Sequin.
66 70
 
... ...
@@ -126,8 +134,7 @@ class Sequin:
126 134
             sequences.
127 135
 
128 136
         Examples:
129
-            >>> seq = Sequin([1, 0, 1, 0, 0, 1, 0, 0, 0, 1],
130
-            ...              is_bitstring=True)
137
+            >>> seq = Sequin([1, 0, 1, 0, 0, 1, 0, 0, 0, 1], is_bitstring=True)
131 138
             >>> seq.bases
132 139
             {2: deque([1, 0, 1, 0, 0, 1, 0, 0, 0, 1])}
133 140
             >>> seq._all_or_nothing_shift(3)
... ...
@@ -163,9 +170,7 @@ class Sequin:
163 170
         return tuple(stash)
164 171
 
165 172
     @staticmethod
166
-    def _big_endian_number(
167
-        digits: Sequence[int], /, *, base: int = 2
168
-    ) -> int:
173
+    def _big_endian_number(digits: Sequence[int], /, *, base: int = 2) -> int:
169 174
         """Evaluate the given integer sequence as a big endian number.
170 175
 
171 176
         Args:
... ...
@@ -231,10 +236,14 @@ class Sequin:
231 236
                 The sequin is exhausted.
232 237
 
233 238
         Examples:
234
-            >>> seq = Sequin([1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1],
235
-            ...              is_bitstring=True)
236
-            >>> seq2 = Sequin([1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1],
237
-            ...               is_bitstring=True)
239
+            >>> seq = Sequin(
240
+            ...     [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1],
241
+            ...     is_bitstring=True,
242
+            ... )
243
+            >>> seq2 = Sequin(
244
+            ...     [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1],
245
+            ...     is_bitstring=True,
246
+            ... )
238 247
             >>> seq.generate(5)
239 248
             3
240 249
             >>> seq.generate(5)
... ...
@@ -266,9 +275,7 @@ class Sequin:
266 275
             raise SequinExhaustedError
267 276
         return value
268 277
 
269
-    def _generate_inner(
270
-        self, n: int, /, *, base: int = 2
271
-    ) -> int:
278
+    def _generate_inner(self, n: int, /, *, base: int = 2) -> int:
272 279
         """Recursive call to generate a base `n` non-negative integer.
273 280
 
274 281
         We first determine the correct exponent `k` to generate base `n`
... ...
@@ -298,10 +305,14 @@ class Sequin:
298 305
                 The range is empty.
299 306
 
300 307
         Examples:
301
-            >>> seq = Sequin([1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1],
302
-            ...              is_bitstring=True)
303
-            >>> seq2 = Sequin([1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1],
304
-            ...               is_bitstring=True)
308
+            >>> seq = Sequin(
309
+            ...     [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1],
310
+            ...     is_bitstring=True,
311
+            ... )
312
+            >>> seq2 = Sequin(
313
+            ...     [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1],
314
+            ...     is_bitstring=True,
315
+            ... )
305 316
             >>> seq._generate_inner(5)
306 317
             3
307 318
             >>> seq._generate_inner(5)
... ...
@@ -21,8 +21,8 @@ if TYPE_CHECKING:
21 21
     from collections.abc import Sequence
22 22
 
23 23
 __all__ = ('SSHAgentClient',)
24
-__author__ = "Marco Ricci <m@the13thletter.info>"
25
-__version__ = "0.1.1"
24
+__author__ = 'Marco Ricci <m@the13thletter.info>'
25
+__version__ = '0.1.1'
26 26
 
27 27
 # In SSH bytestrings, the "length" of the byte string is stored as
28 28
 # a 4-byte/32-bit unsigned integer at the beginning.
... ...
@@ -105,8 +110,7 @@ class SSHAgentClient:
105 110
     ) -> bool:
106 111
         """Close socket connection upon context manager completion."""
107 112
         return bool(
108
-            self._connection.__exit__(
109
-                exc_type, exc_val, exc_tb)  # type: ignore[func-returns-value]
113
+            self._connection.__exit__(exc_type, exc_val, exc_tb)  # type: ignore[func-returns-value]
110 114
         )
111 115
 
112 116
     @staticmethod
... ...
@@ -172,17 +176,16 @@ class SSHAgentClient:
172 176
         Examples:
173 177
             >>> bytes(SSHAgentClient.unstring(b'\x00\x00\x00\x07ssh-rsa'))
174 178
             b'ssh-rsa'
175
-            >>> bytes(SSHAgentClient.unstring(
176
-            ...     SSHAgentClient.string(b'ssh-ed25519')))
179
+            >>> bytes(
180
+            ...     SSHAgentClient.unstring(SSHAgentClient.string(b'ssh-ed25519'))
181
+            ... )
177 182
             b'ssh-ed25519'
178 183
 
179
-        """
184
+        """  # noqa: E501
180 185
         n = len(bytestring)
181 186
         msg = 'malformed SSH byte string'
182
-        if (
183
-            n < HEAD_LEN
184
-            or n != HEAD_LEN + int.from_bytes(bytestring[:HEAD_LEN], 'big',
185
-                                              signed=False)
187
+        if n < HEAD_LEN or n != HEAD_LEN + int.from_bytes(
188
+            bytestring[:HEAD_LEN], 'big', signed=False
186 189
         ):
187 190
             raise ValueError(msg)
188 191
         return bytestring[HEAD_LEN:]
... ...
@@ -209,11 +212,13 @@ class SSHAgentClient:
209 212
 
210 213
         Examples:
211 214
             >>> a, b = SSHAgentClient.unstring_prefix(
212
-            ...     b'\x00\x00\x00\x07ssh-rsa____trailing data')
215
+            ...     b'\x00\x00\x00\x07ssh-rsa____trailing data'
216
+            ... )
213 217
             >>> (bytes(a), bytes(b))
214 218
             (b'ssh-rsa', b'____trailing data')
215 219
             >>> a, b = SSHAgentClient.unstring_prefix(
216
-            ...     SSHAgentClient.string(b'ssh-ed25519'))
220
+            ...     SSHAgentClient.string(b'ssh-ed25519')
221
+            ... )
217 222
             >>> (bytes(a), bytes(b))
218 223
             (b'ssh-ed25519', b'')
219 224
 
... ...
@@ -225,7 +230,10 @@ class SSHAgentClient:
225 230
         m = int.from_bytes(bytestring[:HEAD_LEN], 'big', signed=False)
226 231
         if m + HEAD_LEN > n:
227 232
             raise ValueError(msg)
228
-        return (bytestring[HEAD_LEN:m + HEAD_LEN], bytestring[m + HEAD_LEN:])
233
+        return (
234
+            bytestring[HEAD_LEN : m + HEAD_LEN],
235
+            bytestring[m + HEAD_LEN :],
236
+        )
229 237
 
230 238
     def request(
231 239
         self, code: int, payload: bytes | bytearray, /
... ...
@@ -282,12 +290,16 @@ class SSHAgentClient:
282 290
 
283 291
         """
284 292
         response_code, response = self.request(
285
-            ssh_types.SSH_AGENTC.REQUEST_IDENTITIES.value, b'')
293
+            ssh_types.SSH_AGENTC.REQUEST_IDENTITIES.value, b''
294
+        )
286 295
         if response_code != ssh_types.SSH_AGENT.IDENTITIES_ANSWER.value:
287
-            msg = (f'error return from SSH agent: '
288
-                   f'{response_code = }, {response = }')
296
+            msg = (
297
+                f'error return from SSH agent: '
298
+                f'{response_code = }, {response = }'
299
+            )
289 300
             raise RuntimeError(msg)
290 301
         response_stream = collections.deque(response)
302
+
291 303
         def shift(num: int) -> bytes:
292 304
             buf = collections.deque(b'')
293 305
             for _ in range(num):
... ...
@@ -314,8 +327,13 @@ class SSHAgentClient:
314 327
         return keys
315 328
 
316 329
     def sign(
317
-        self, /, key: bytes | bytearray, payload: bytes | bytearray,
318
-        *, flags: int = 0, check_if_key_loaded: bool = False,
330
+        self,
331
+        /,
332
+        key: bytes | bytearray,
333
+        payload: bytes | bytearray,
334
+        *,
335
+        flags: int = 0,
336
+        check_if_key_loaded: bool = False,
319 337
     ) -> bytes | bytearray:
320 338
         """Request the SSH agent sign the payload with the key.
321 339
 
... ...
@@ -361,7 +379,8 @@ class SSHAgentClient:
361 379
         request_data.extend(self.string(payload))
362 380
         request_data.extend(self.uint32(flags))
363 381
         response_code, response = self.request(
364
-            ssh_types.SSH_AGENTC.SIGN_REQUEST.value, request_data)
382
+            ssh_types.SSH_AGENTC.SIGN_REQUEST.value, request_data
383
+        )
365 384
         if response_code != ssh_types.SSH_AGENT.SIGN_RESPONSE.value:
366 385
             msg = f'signing data failed: {response_code = }, {response = }'
367 386
             raise RuntimeError(msg)
... ...
@@ -33,38 +33,39 @@ if TYPE_CHECKING:
33 33
         expected_signature: bytes | None
34 34
         derived_passphrase: bytes | str | None
35 35
 
36
+
36 37
 SUPPORTED_KEYS: Mapping[str, SSHTestKey] = {
37 38
     'ed25519': {
38
-        'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY-----
39
+        'private_key': rb"""-----BEGIN OPENSSH PRIVATE KEY-----
39 40
 b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
40 41
 QyNTUxOQAAACCBeIFoJtYCSF8P/zJIb+TBMIncHGpFBgnpCQ/7whJpdgAAAKDweO7H8Hju
41 42
 xwAAAAtzc2gtZWQyNTUxOQAAACCBeIFoJtYCSF8P/zJIb+TBMIncHGpFBgnpCQ/7whJpdg
42 43
 AAAEAbM/A869nkWZbe2tp3Dm/L6gitvmpH/aRZt8sBII3ExYF4gWgm1gJIXw//Mkhv5MEw
43 44
 idwcakUGCekJD/vCEml2AAAAG3Rlc3Qga2V5IHdpdGhvdXQgcGFzc3BocmFzZQEC
44 45
 -----END OPENSSH PRIVATE KEY-----
45
-''',
46
-        'public_key': rb'''ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIF4gWgm1gJIXw//Mkhv5MEwidwcakUGCekJD/vCEml2 test key without passphrase
47
-''',  # noqa: E501
48
-        'public_key_data': bytes.fromhex('''
46
+""",
47
+        'public_key': rb"""ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIF4gWgm1gJIXw//Mkhv5MEwidwcakUGCekJD/vCEml2 test key without passphrase
48
+""",  # noqa: E501
49
+        'public_key_data': bytes.fromhex("""
49 50
             00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39
50 51
             00 00 00 20
51 52
             81 78 81 68 26 d6 02 48 5f 0f ff 32 48 6f e4 c1
52 53
             30 89 dc 1c 6a 45 06 09 e9 09 0f fb c2 12 69 76
53
-'''),
54
-        'expected_signature': bytes.fromhex('''
54
+"""),
55
+        'expected_signature': bytes.fromhex("""
55 56
             00 00 00 0b 73 73 68 2d 65 64 32 35 35 31 39
56 57
             00 00 00 40
57 58
             f0 98 19 80 6c 1a 97 d5 26 03 6e cc e3 65 8f 86
58 59
             66 07 13 19 13 09 21 33 33 f9 e4 36 53 1d af fd
59 60
             0d 08 1f ec f8 73 9b 8c 5f 55 39 16 7c 53 54 2c
60 61
             1e 52 bb 30 ed 7f 89 e2 2f 69 51 55 d8 9e a6 02
61
-        '''),
62
+        """),
62 63
         'derived_passphrase': rb'8JgZgGwal9UmA27M42WPhmYHExkTCSEzM/nkNlMdr/0NCB/s+HObjF9VORZ8U1QsHlK7MO1/ieIvaVFV2J6mAg==',  # noqa: E501
63 64
     },
64 65
     # Currently only supported by PuTTY (which is deficient in other
65 66
     # niceties of the SSH agent and the agent's client).
66 67
     'ed448': {
67
-        'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY-----
68
+        'private_key': rb"""-----BEGIN OPENSSH PRIVATE KEY-----
68 69
 b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAASgAAAAlz
69 70
 c2gtZWQ0NDgAAAA54vZy009Wu8wExjvEb3hqtLz1GO/+d5vmGUbErWQ4AUO9mYLT
70 71
 zHJHc2m4s+yWzP29Cc3EcxizLG8AAAAA8BdhfCcXYXwnAAAACXNzaC1lZDQ0OAAA
... ...
@@ -74,19 +75,18 @@ ADni9nLTT1a7zATGO8RveGq0vPUY7/53m+YZRsStZDgBQ72ZgtPMckdzabiz7JbM
74 75
 GUbErWQ4AUO9mYLTzHJHc2m4s+yWzP29Cc3EcxizLG8AAAAAG3Rlc3Qga2V5IHdp
75 76
 dGhvdXQgcGFzc3BocmFzZQECAwQFBgcICQ==
76 77
 -----END OPENSSH PRIVATE KEY-----
77
-''',
78
-        'public_key': rb'''ssh-ed448 AAAACXNzaC1lZDQ0OAAAADni9nLTT1a7zATGO8RveGq0vPUY7/53m+YZRsStZDgBQ72ZgtPMckdzabiz7JbM/b0JzcRzGLMsbwA= test key without passphrase
79
-''',  # noqa: E501
80
-        'public_key_data': bytes.fromhex('''
78
+""",
79
+        'public_key': rb"""ssh-ed448 AAAACXNzaC1lZDQ0OAAAADni9nLTT1a7zATGO8RveGq0vPUY7/53m+YZRsStZDgBQ72ZgtPMckdzabiz7JbM/b0JzcRzGLMsbwA= test key without passphrase
80
+""",  # noqa: E501
81
+        'public_key_data': bytes.fromhex("""
81 82
             00 00 00 09 73 73 68 2d 65 64 34 34 38
82 83
             00 00 00 39
83 84
             e2 f6 72 d3 4f 56 bb cc 04 c6 3b c4 6f 78 6a b4
84 85
             bc f5 18 ef fe 77 9b e6 19 46 c4 ad 64 38 01 43
85 86
             bd 99 82 d3 cc 72 47 73 69 b8 b3 ec 96 cc fd bd
86 87
             09 cd c4 73 18 b3 2c 6f 00
87
-        '''),
88
-
89
-        'expected_signature': bytes.fromhex('''
88
+        """),
89
+        'expected_signature': bytes.fromhex("""
90 90
             00 00 00 09 73 73 68 2d 65 64 34 34 38
91 91
             00 00 00 72 06 86
92 92
             f4 64 a4 a6 ba d9 c3 22 c4 93 49 99 fc 11 de 67
... ...
@@ -96,11 +96,11 @@ dGhvdXQgcGFzc3BocmFzZQECAwQFBgcICQ==
96 96
             db e5 89 f3 35 be 24 58 90 c6 ca 04 f0 db 88 80
97 97
             db bd 77 7c 80 20 7f 3a 48 61 f6 1f ae a9 5e 53
98 98
             7b e0 9d 93 1e ea dc eb b5 cd 56 4c ea 8f 08 00
99
-        '''),
99
+        """),
100 100
         'derived_passphrase': rb'Bob0ZKSmutnDIsSTSZn8Ed5nlwjy2Lc8LBPnxRwekqYO2C9tgQOCAONy5DJtctJtMoQ/zKkeVywAmrOZ3kXazi7R2+WJ8zW+JFiQxsoE8NuIgNu9d3yAIH86SGH2H66pXlN74J2THurc67XNVkzqjwgA',  # noqa: E501
101 101
     },
102 102
     'rsa': {
103
-        'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY-----
103
+        'private_key': rb"""-----BEGIN OPENSSH PRIVATE KEY-----
104 104
 b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn
105 105
 NhAAAAAwEAAQAAAYEAsaHu6Xs4cVsuDSNJlMCqoPVgmDgEviI8TfXmHKqX3JkIqI3LsvV7
106 106
 Ijf8WCdTveEq7CkuZhImtsR52AOEVAoU8mDXDNr+nJ5wUPzf1UIaRjDe0lcXW4SlF01hQs
... ...
@@ -138,10 +138,10 @@ vC2EAD91xexdorIA5BgXU+qltBqzzBVzVtF7+jOZOjfzOlaTX9I5I5veyeTaTxZj1XXUzi
138 138
 btBNdMEJJp7ifucYmoYAAwE7K+VlWagDEK2y8Mte9y9E+N0uO2j+h85sQt/UIb2iE/vhcg
139 139
 Bgp6142WnSCQAAABt0ZXN0IGtleSB3aXRob3V0IHBhc3NwaHJhc2UB
140 140
 -----END OPENSSH PRIVATE KEY-----
141
-''',
142
-        'public_key': rb'''ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCxoe7pezhxWy4NI0mUwKqg9WCYOAS+IjxN9eYcqpfcmQiojcuy9XsiN/xYJ1O94SrsKS5mEia2xHnYA4RUChTyYNcM2v6cnnBQ/N/VQhpGMN7SVxdbhKUXTWFCwbjBgO6rGyHB6WtoH8vd7TOEPt+NgcXwhsWyoaUUdYTA62V+GF9vEmxMaC4ubgDz+B0QkPnauSoNxmkhcIe0lsLNb1pClZyz88PDnKXCX/d0HuN/HJ+sbPg7dCvOyqFYSyKn3uY6bCXqoIdurxXzH3O7z0P8f5sbmKOrGGKNuNxVRbeVl/D/3uDL0nqsbfUc1qvkfwbJwtMXC4IV6kOZMSk2BAsqh7x48gQ+rhYeEVSi8F3CWs4HJQoqrGt7K9a3mCSlMBHP70u3w6ME7eumoryxlUofewTd17ZEkzdX08l2ZlKzZvwQUrc+xQZ2Uw8z2mfW6Ti4gi0pYGaig7Ke4PwuXpo/C5YAWfeXycsvJZ2uaYRjMdZeJGNAnHLUGLkBscw5aI8= test key without passphrase
143
-''',  # noqa: E501
144
-        'public_key_data': bytes.fromhex('''
141
+""",
142
+        'public_key': rb"""ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCxoe7pezhxWy4NI0mUwKqg9WCYOAS+IjxN9eYcqpfcmQiojcuy9XsiN/xYJ1O94SrsKS5mEia2xHnYA4RUChTyYNcM2v6cnnBQ/N/VQhpGMN7SVxdbhKUXTWFCwbjBgO6rGyHB6WtoH8vd7TOEPt+NgcXwhsWyoaUUdYTA62V+GF9vEmxMaC4ubgDz+B0QkPnauSoNxmkhcIe0lsLNb1pClZyz88PDnKXCX/d0HuN/HJ+sbPg7dCvOyqFYSyKn3uY6bCXqoIdurxXzH3O7z0P8f5sbmKOrGGKNuNxVRbeVl/D/3uDL0nqsbfUc1qvkfwbJwtMXC4IV6kOZMSk2BAsqh7x48gQ+rhYeEVSi8F3CWs4HJQoqrGt7K9a3mCSlMBHP70u3w6ME7eumoryxlUofewTd17ZEkzdX08l2ZlKzZvwQUrc+xQZ2Uw8z2mfW6Ti4gi0pYGaig7Ke4PwuXpo/C5YAWfeXycsvJZ2uaYRjMdZeJGNAnHLUGLkBscw5aI8= test key without passphrase
143
+""",  # noqa: E501
144
+        'public_key_data': bytes.fromhex("""
145 145
             00 00 00 07 73 73 68 2d 72 73 61
146 146
             00 00 00 03 01 00 01
147 147
             00 00 01 81 00
... ...
@@ -169,8 +169,8 @@ Bgp6142WnSCQAAABt0ZXN0IGtleSB3aXRob3V0IHBhc3NwaHJhc2UB
169 169
             b8 82 2d 29 60 66 a2 83 b2 9e e0 fc 2e 5e 9a 3f
170 170
             0b 96 00 59 f7 97 c9 cb 2f 25 9d ae 69 84 63 31
171 171
             d6 5e 24 63 40 9c 72 d4 18 b9 01 b1 cc 39 68 8f
172
-'''),
173
-        'expected_signature': bytes.fromhex('''
172
+"""),
173
+        'expected_signature': bytes.fromhex("""
174 174
             00 00 00 07 73 73 68 2d 72 73 61
175 175
             00 00 01 80
176 176
             a2 10 7c 2e f6 bb 53 a8 74 2a a1 19 99 ad 81 be
... ...
@@ -197,14 +197,14 @@ Bgp6142WnSCQAAABt0ZXN0IGtleSB3aXRob3V0IHBhc3NwaHJhc2UB
197 197
             a4 1b bc d2 29 b8 78 61 2b 78 e6 b1 52 b0 d5 ec
198 198
             de 69 2c 48 62 d9 fd d1 9b 6b b0 49 db d3 ff 38
199 199
             e7 10 d9 2d ce 9f 0d 5e 09 7b 37 d2 7b c3 bf ce
200
-'''),
200
+"""),
201 201
         'derived_passphrase': rb'ohB8Lva7U6h0KqEZma2Bvnmc7dadCU5uxRhIM5B3mWj3ngNazU4Y64l9haLurkqS9m/Ouf6GfyprMdpuGv6ipYi4RH+hdnOz7HW10Ka5FZdlCRN9lCHR+10PiyMEd8LDVSKxoAmK9Tgq1n8bhymgJdMlb8tkYQeY3BTFhPiSJF5QEWtJ5fDMKcspqRnYp3EfkQsFsQFLwl8ApbYhv/gsnWebRzsKSWt5Lfwd7Ayw5Sci1an408P530ho6fvvPNwmv8/qKUMBpuPFUZX0Zm2KVeJH7OgwRUyuR+fJpCGLZLq2iPYh+HO5yxGheHWSxlrlZP7tQtmVmeYrbzwWPCh0pHIvDT8sM2eqNRmO57URL7P3asUC4m+jQuNiGZkD6qUg56HjvMgGo7V81nZd329gRoMqCADW09mkwUGM+GBWRYHaO6IWH55OdYMX2sNTwz4ZpBu80im4eGEreOaxUrDV7N5pLEhi2f3Rm2uwSdvT/zjnENktzp8NXgl7N9J7w7/O',  # noqa: E501
202 202
     },
203 203
 }
204 204
 
205 205
 UNSUITABLE_KEYS: Mapping[str, SSHTestKey] = {
206 206
     'dsa1024': {
207
-        'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY-----
207
+        'private_key': rb"""-----BEGIN OPENSSH PRIVATE KEY-----
208 208
 b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABsQAAAAdzc2gtZH
209 209
 NzAAAAgQC7KAZXqBGNVLBQPrcMYAoNW54BhD8aIhe7BDWYzJcsaMt72VKSkguZ8+XR7nRa
210 210
 0C/ZsBi+uJp0dpxy9ZMTOWX4u5YPMeQcXEdGExZIfimGqSOAsy6fCld2IfJZJZExcCmhe9
... ...
@@ -225,10 +225,10 @@ mMbWEYp/C8jC6ikZqmtEOVK7w3woYC4b7BvWEm/zKcOapvD4h0mn8IZGs/7Xt/vISqIKqH
225 225
 u7HfrQhdOiKSa+ZO9AAojbURqrLDRfBJa5dXn2AAAAFQDJHfenj4EJ9WkehpdJatPBlqCW
226 226
 0gAAABt0ZXN0IGtleSB3aXRob3V0IHBhc3NwaHJhc2UBAgMEBQYH
227 227
 -----END OPENSSH PRIVATE KEY-----
228
-''',
229
-        'public_key': rb'''ssh-dss AAAAB3NzaC1kc3MAAACBALsoBleoEY1UsFA+twxgCg1bngGEPxoiF7sENZjMlyxoy3vZUpKSC5nz5dHudFrQL9mwGL64mnR2nHL1kxM5Zfi7lg8x5BxcR0YTFkh+KYapI4CzLp8KV3Yh8lklkTFwKaF71KyOx3dhIA8lGW45cVBz3kxmhHmEzCUgMPxDOsTtAAAAFQD32c5k6B3tocxUahelQQFyfseiywAAAIAuvYCDeHEzesp3HNVTDx9fRVU9c77f4qvyEZ7Qpz/s3BVoFUvUZDx96cG5bKekBRsfTCjeHXCQH/yFfqn5Lxye7msgGVS5U3AvD9shiiEr3wt+pNgr9X6DooP7ybfjC8SJdmarLBjnifZuSxyHU2q+P+02kvMTFLH9dLSRIzVqKAAAAIBtA1E9xUS4YOsRx/7GDm2AB6M9cE9ev8myz4KGTriSbeaKsxiMBbJZi1VyBP7uE5jG1hGKfwvIwuopGaprRDlSu8N8KGAuG+wb1hJv8ynDmqbw+IdJp/CGRrP+17f7yEqiCqh7ux360IXToikmvmTvQAKI21Eaqyw0XwSWuXV59g== test key without passphrase
230
-''',  # noqa: E501
231
-        'public_key_data': bytes.fromhex('''
228
+""",
229
+        'public_key': rb"""ssh-dss AAAAB3NzaC1kc3MAAACBALsoBleoEY1UsFA+twxgCg1bngGEPxoiF7sENZjMlyxoy3vZUpKSC5nz5dHudFrQL9mwGL64mnR2nHL1kxM5Zfi7lg8x5BxcR0YTFkh+KYapI4CzLp8KV3Yh8lklkTFwKaF71KyOx3dhIA8lGW45cVBz3kxmhHmEzCUgMPxDOsTtAAAAFQD32c5k6B3tocxUahelQQFyfseiywAAAIAuvYCDeHEzesp3HNVTDx9fRVU9c77f4qvyEZ7Qpz/s3BVoFUvUZDx96cG5bKekBRsfTCjeHXCQH/yFfqn5Lxye7msgGVS5U3AvD9shiiEr3wt+pNgr9X6DooP7ybfjC8SJdmarLBjnifZuSxyHU2q+P+02kvMTFLH9dLSRIzVqKAAAAIBtA1E9xUS4YOsRx/7GDm2AB6M9cE9ev8myz4KGTriSbeaKsxiMBbJZi1VyBP7uE5jG1hGKfwvIwuopGaprRDlSu8N8KGAuG+wb1hJv8ynDmqbw+IdJp/CGRrP+17f7yEqiCqh7ux360IXToikmvmTvQAKI21Eaqyw0XwSWuXV59g== test key without passphrase
230
+""",  # noqa: E501
231
+        'public_key_data': bytes.fromhex("""
232 232
             00 00 00 07 73 73 68 2d 64 73 73
233 233
             00 00 00 81 00
234 234
             bb 28 06 57 a8 11 8d 54 b0 50 3e b7 0c 60 0a 0d
... ...
@@ -259,12 +259,12 @@ u7HfrQhdOiKSa+ZO9AAojbURqrLDRfBJa5dXn2AAAAFQDJHfenj4EJ9WkehpdJatPBlqCW
259 259
             a6 f0 f8 87 49 a7 f0 86 46 b3 fe d7 b7 fb c8 4a
260 260
             a2 0a a8 7b bb 1d fa d0 85 d3 a2 29 26 be 64 ef
261 261
             40 02 88 db 51 1a ab 2c 34 5f 04 96 b9 75 79 f6
262
-'''),
262
+"""),
263 263
         'expected_signature': None,
264 264
         'derived_passphrase': None,
265 265
     },
266 266
     'ecdsa256': {
267
-        'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY-----
267
+        'private_key': rb"""-----BEGIN OPENSSH PRIVATE KEY-----
268 268
 b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAaAAAABNlY2RzYS
269 269
 1zaGEyLW5pc3RwMjU2AAAACG5pc3RwMjU2AAAAQQTLbU0zDwsk2Dvp+VYIrsNVf5gWwz2S
270 270
 3SZ8TbxiQRkpnGSVqyIoHJOJc+NQItAa7xlJ/8Z6gfz57Z3apUkaMJm6AAAAuKeY+YinmP
... ...
@@ -273,10 +273,10 @@ Vgiuw1V/mBbDPZLdJnxNvGJBGSmcZJWrIigck4lz41Ai0BrvGUn/xnqB/PntndqlSRowmb
273 273
 oAAAAhAKIl/3n0pKVIxpZkXTGtii782Qr4yIcvHdpxjO/QsIqKAAAAG3Rlc3Qga2V5IHdp
274 274
 dGhvdXQgcGFzc3BocmFzZQECAwQ=
275 275
 -----END OPENSSH PRIVATE KEY-----
276
-''',
277
-        'public_key': rb'''ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMttTTMPCyTYO+n5Vgiuw1V/mBbDPZLdJnxNvGJBGSmcZJWrIigck4lz41Ai0BrvGUn/xnqB/PntndqlSRowmbo= test key without passphrase
278
-''',  # noqa: E501
279
-        'public_key_data': bytes.fromhex('''
276
+""",
277
+        'public_key': rb"""ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMttTTMPCyTYO+n5Vgiuw1V/mBbDPZLdJnxNvGJBGSmcZJWrIigck4lz41Ai0BrvGUn/xnqB/PntndqlSRowmbo= test key without passphrase
278
+""",  # noqa: E501
279
+        'public_key_data': bytes.fromhex("""
280 280
             00 00 00 13 65 63 64 73 61 2d 73 68 61 32 2d 6e
281 281
             69 73 74 70 32 35 36
282 282
             00 00 00 08 6e 69 73 74 70 32 35 36
... ...
@@ -285,12 +285,12 @@ dGhvdXQgcGFzc3BocmFzZQECAwQ=
285 285
             7f 98 16 c3 3d 92 dd 26 7c 4d bc 62 41 19 29 9c
286 286
             64 95 ab 22 28 1c 93 89 73 e3 50 22 d0 1a ef 19
287 287
             49 ff c6 7a 81 fc f9 ed 9d da a5 49 1a 30 99 ba
288
-'''),
288
+"""),
289 289
         'expected_signature': None,
290 290
         'derived_passphrase': None,
291 291
     },
292 292
     'ecdsa384': {
293
-        'private_key': rb'''-----BEGIN OPENSSH PRIVATE KEY-----
293
+        'private_key': rb"""-----BEGIN OPENSSH PRIVATE KEY-----
294 294
 b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAiAAAABNlY2RzYS
295 295
 1zaGEyLW5pc3RwMzg0AAAACG5pc3RwMzg0AAAAYQSgkOjkAvq7v5vHuj3KBL4/EAWcn5hZ
296 296
 DyKcbyV0eBMGFq7hKXQlZqIahLVqeMR0QqmkxNJ2rly2VHcXneq3vZ+9fIsWCOdYk5WP3N
... ...
@@ -300,10 +300,10 @@ au4Sl0JWaiGoS1anjEdEKppMTSdq5ctlR3F53qt72fvXyLFgjnWJOVj9zWT87/ddV5+8Gx
300 300
 JAu0J3Q+cypZuKQVAAAAMQD5sTy8p+B1cn/DhOmXquui1BcxvASqzzevkBlbQoBa73y04B
301 301
 2OdqVOVRkwZWRROz0AAAAbdGVzdCBrZXkgd2l0aG91dCBwYXNzcGhyYXNlAQIDBA==
302 302
 -----END OPENSSH PRIVATE KEY-----
303
-''',
304
-        'public_key': rb'''ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBKCQ6OQC+ru/m8e6PcoEvj8QBZyfmFkPIpxvJXR4EwYWruEpdCVmohqEtWp4xHRCqaTE0nauXLZUdxed6re9n718ixYI51iTlY/c1k/O/3XVefvBsSQLtCd0PnMqWbikFQ== test key without passphrase
305
-''',  # noqa: E501
306
-        'public_key_data': bytes.fromhex('''
303
+""",
304
+        'public_key': rb"""ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBKCQ6OQC+ru/m8e6PcoEvj8QBZyfmFkPIpxvJXR4EwYWruEpdCVmohqEtWp4xHRCqaTE0nauXLZUdxed6re9n718ixYI51iTlY/c1k/O/3XVefvBsSQLtCd0PnMqWbikFQ== test key without passphrase
305
+""",  # noqa: E501
306
+        'public_key_data': bytes.fromhex("""
307 307
             00 00 00 13
308 308
             65 63 64 73 61 2d 73 68 61 32 2d 6e 69 73 74 70
309 309
             33 38 34
... ...
@@ -315,7 +315,7 @@ JAu0J3Q+cypZuKQVAAAAMQD5sTy8p+B1cn/DhOmXquui1BcxvASqzzevkBlbQoBa73y04B
315 315
             a4 c4 d2 76 ae 5c b6 54 77 17 9d ea b7 bd 9f bd
316 316
             7c 8b 16 08 e7 58 93 95 8f dc d6 4f ce ff 75 d5
317 317
             79 fb c1 b1 24 0b b4 27 74 3e 73 2a 59 b8 a4 15
318
-'''),
318
+"""),
319 319
         'expected_signature': None,
320 320
         'derived_passphrase': None,
321 321
     },
... ...
@@ -327,8 +327,16 @@ DUMMY_KEY1 = SUPPORTED_KEYS['ed25519']['public_key_data']
327 327
 DUMMY_KEY1_B64 = base64.standard_b64encode(DUMMY_KEY1).decode('ASCII')
328 328
 DUMMY_KEY2 = SUPPORTED_KEYS['rsa']['public_key_data']
329 329
 DUMMY_KEY2_B64 = base64.standard_b64encode(DUMMY_KEY2).decode('ASCII')
330
-DUMMY_CONFIG_SETTINGS = {"length": 10, "upper": 1, "lower": 1, "repeat": 5,
331
-                         "number": 1, "space": 1, "dash": 1, "symbol": 1}
330
+DUMMY_CONFIG_SETTINGS = {
331
+    'length': 10,
332
+    'upper': 1,
333
+    'lower': 1,
334
+    'repeat': 5,
335
+    'number': 1,
336
+    'space': 1,
337
+    'dash': 1,
338
+    'symbol': 1,
339
+}
332 340
 DUMMY_RESULT_PASSPHRASE = b'.2V_QJkd o'
333 341
 DUMMY_RESULT_KEY1 = b'E<b<{ -7iG'
334 342
 DUMMY_PHRASE_FROM_KEY1_RAW = (
... ...
@@ -341,30 +349,40 @@ DUMMY_PHRASE_FROM_KEY1_RAW = (
341 349
 DUMMY_PHRASE_FROM_KEY1 = b'8JgZgGwal9UmA27M42WPhmYHExkTCSEzM/nkNlMdr/0NCB/s+HObjF9VORZ8U1QsHlK7MO1/ieIvaVFV2J6mAg=='  # noqa: E501
342 350
 
343 351
 skip_if_no_agent = pytest.mark.skipif(
344
-    not os.environ.get('SSH_AUTH_SOCK'), reason='running SSH agent required')
352
+    not os.environ.get('SSH_AUTH_SOCK'), reason='running SSH agent required'
353
+)
354
+
345 355
 
346 356
 def list_keys(
347 357
     self: Any = None,
348 358
 ) -> list[ssh_agent_client.types.KeyCommentPair]:
349 359
     del self  # Unused.
350 360
     Pair = ssh_agent_client.types.KeyCommentPair  # noqa: N806
351
-    list1 = [Pair(value['public_key_data'], f'{key} test key'.encode('ASCII'))
352
-             for key, value in SUPPORTED_KEYS.items()]
353
-    list2 = [Pair(value['public_key_data'], f'{key} test key'.encode('ASCII'))
354
-             for key, value in UNSUITABLE_KEYS.items()]
361
+    list1 = [
362
+        Pair(value['public_key_data'], f'{key} test key'.encode('ASCII'))
363
+        for key, value in SUPPORTED_KEYS.items()
364
+    ]
365
+    list2 = [
366
+        Pair(value['public_key_data'], f'{key} test key'.encode('ASCII'))
367
+        for key, value in UNSUITABLE_KEYS.items()
368
+    ]
355 369
     return list1 + list2
356 370
 
371
+
357 372
 def list_keys_singleton(
358 373
     self: Any = None,
359 374
 ) -> list[ssh_agent_client.types.KeyCommentPair]:
360 375
     del self  # Unused.
361 376
     Pair = ssh_agent_client.types.KeyCommentPair  # noqa: N806
362
-    list1 = [Pair(value['public_key_data'], f'{key} test key'.encode('ASCII'))
363
-             for key, value in SUPPORTED_KEYS.items()]
377
+    list1 = [
378
+        Pair(value['public_key_data'], f'{key} test key'.encode('ASCII'))
379
+        for key, value in SUPPORTED_KEYS.items()
380
+    ]
364 381
     return list1[:1]
365 382
 
383
+
366 384
 def suitable_ssh_keys(
367
-    conn: Any
385
+    conn: Any,
368 386
 ) -> Iterator[ssh_agent_client.types.KeyCommentPair]:
369 387
     del conn  # Unused.
370 388
     yield from [
... ...
@@ -377,9 +396,12 @@ def phrase_from_key(key: bytes) -> bytes:
377 396
         return DUMMY_PHRASE_FROM_KEY1
378 397
     raise KeyError(key)  # pragma: no cover
379 398
 
399
+
380 400
 @contextlib.contextmanager
381 401
 def isolated_config(
382
-    monkeypatch: Any, runner: click.testing.CliRunner, config: Any,
402
+    monkeypatch: Any,
403
+    runner: click.testing.CliRunner,
404
+    config: Any,
383 405
 ):
384 406
     prog_name = derivepassphrase.cli.PROG_NAME
385 407
     env_name = prog_name.replace(' ', '_').upper() + '_PATH'
... ...
@@ -393,7 +415,8 @@ def isolated_config(
393 415
         )
394 416
         with open(
395 417
             derivepassphrase.cli._config_filename(),
396
-            'w', encoding='UTF-8',
418
+            'w',
419
+            encoding='UTF-8',
397 420
         ) as outfile:
398 421
             json.dump(config, outfile)
399 422
         yield
... ...
@@ -15,115 +15,149 @@ import derivepassphrase
15 15
 
16 16
 Vault = derivepassphrase.Vault
17 17
 
18
-class TestVault:
19 18
 
19
+class TestVault:
20 20
     phrase = b'She cells C shells bye the sea shoars'
21 21
     google_phrase = rb': 4TVH#5:aZl8LueOT\{'
22 22
     twitter_phrase = rb"[ (HN_N:lI&<ro=)3'g9"
23 23
 
24
-    @pytest.mark.parametrize(['service', 'expected'], [
24
+    @pytest.mark.parametrize(
25
+        ['service', 'expected'],
26
+        [
25 27
             (b'google', google_phrase),
26 28
             ('twitter', twitter_phrase),
27
-    ])
29
+        ],
30
+    )
28 31
     def test_200_basic_configuration(self, service, expected):
29 32
         assert Vault(phrase=self.phrase).generate(service) == expected
30 33
 
31 34
     def test_201_phrase_dependence(self):
32 35
         assert (
33
-            Vault(phrase=(self.phrase + b'X')).generate('google') ==
34
-            b'n+oIz6sL>K*lTEWYRO%7'
36
+            Vault(phrase=(self.phrase + b'X')).generate('google')
37
+            == b'n+oIz6sL>K*lTEWYRO%7'
35 38
         )
36 39
 
37 40
     def test_202_reproducibility_and_bytes_service_name(self):
38
-        assert (
39
-            Vault(phrase=self.phrase).generate(b'google') ==
40
-            Vault(phrase=self.phrase).generate('google')
41
-        )
41
+        assert Vault(phrase=self.phrase).generate(b'google') == Vault(
42
+            phrase=self.phrase
43
+        ).generate('google')
42 44
 
43 45
     def test_203_reproducibility_and_bytearray_service_name(self):
44
-        assert (
45
-            Vault(phrase=self.phrase).generate(b'google') ==
46
-            Vault(phrase=self.phrase).generate(bytearray(b'google'))
47
-        )
46
+        assert Vault(phrase=self.phrase).generate(b'google') == Vault(
47
+            phrase=self.phrase
48
+        ).generate(bytearray(b'google'))
48 49
 
49 50
     def test_210_nonstandard_length(self):
50 51
         assert (
51
-            Vault(phrase=self.phrase, length=4).generate('google')
52
-            == b'xDFu'
52
+            Vault(phrase=self.phrase, length=4).generate('google') == b'xDFu'
53 53
         )
54 54
 
55 55
     def test_211_repetition_limit(self):
56 56
         assert (
57
-            Vault(phrase=b'', length=24, symbol=0, number=0,
58
-                  repeat=1).generate('asd') ==
59
-            b'IVTDzACftqopUXqDHPkuCIhV'
57
+            Vault(
58
+                phrase=b'', length=24, symbol=0, number=0, repeat=1
59
+            ).generate('asd')
60
+            == b'IVTDzACftqopUXqDHPkuCIhV'
60 61
         )
61 62
 
62 63
     def test_212_without_symbols(self):
63 64
         assert (
64
-            Vault(phrase=self.phrase, symbol=0).generate('google') ==
65
-            b'XZ4wRe0bZCazbljCaMqR'
65
+            Vault(phrase=self.phrase, symbol=0).generate('google')
66
+            == b'XZ4wRe0bZCazbljCaMqR'
66 67
         )
67 68
 
68 69
     def test_213_no_numbers(self):
69 70
         assert (
70
-            Vault(phrase=self.phrase, number=0).generate('google') ==
71
-            b'_*$TVH.%^aZl(LUeOT?>'
71
+            Vault(phrase=self.phrase, number=0).generate('google')
72
+            == b'_*$TVH.%^aZl(LUeOT?>'
72 73
         )
73 74
 
74 75
     def test_214_no_lowercase_letters(self):
75 76
         assert (
76
-            Vault(phrase=self.phrase, lower=0).generate('google') ==
77
-            b':{?)+7~@OA:L]!0E$)(+'
77
+            Vault(phrase=self.phrase, lower=0).generate('google')
78
+            == b':{?)+7~@OA:L]!0E$)(+'
78 79
         )
79 80
 
80 81
     def test_215_at_least_5_digits(self):
81 82
         assert (
82
-            Vault(phrase=self.phrase, length=8, number=5)
83
-            .generate('songkick') == b'i0908.7['
83
+            Vault(phrase=self.phrase, length=8, number=5).generate('songkick')
84
+            == b'i0908.7['
84 85
         )
85 86
 
86 87
     def test_216_lots_of_spaces(self):
87 88
         assert (
88
-            Vault(phrase=self.phrase, space=12)
89
-            .generate('songkick') == b' c   6 Bq  % 5fR    '
89
+            Vault(phrase=self.phrase, space=12).generate('songkick')
90
+            == b' c   6 Bq  % 5fR    '
90 91
         )
91 92
 
92 93
     def test_217_all_character_classes(self):
93 94
         assert (
94
-            Vault(phrase=self.phrase, lower=2, upper=2, number=1,
95
-                  space=3, dash=2, symbol=1)
96
-            .generate('google') == b': : fv_wqt>a-4w1S  R'
95
+            Vault(
96
+                phrase=self.phrase,
97
+                lower=2,
98
+                upper=2,
99
+                number=1,
100
+                space=3,
101
+                dash=2,
102
+                symbol=1,
103
+            ).generate('google')
104
+            == b': : fv_wqt>a-4w1S  R'
97 105
         )
98 106
 
99 107
     def test_218_only_numbers_and_very_high_repetition_limit(self):
100
-        generated = Vault(phrase=b'', length=40, lower=0, upper=0, space=0,
101
-                          dash=0, symbol=0, repeat=4).generate('abcdef')
102
-        forbidden_substrings = {b'0000', b'1111', b'2222', b'3333', b'4444',
103
-                                b'5555', b'6666', b'7777', b'8888', b'9999'}
108
+        generated = Vault(
109
+            phrase=b'',
110
+            length=40,
111
+            lower=0,
112
+            upper=0,
113
+            space=0,
114
+            dash=0,
115
+            symbol=0,
116
+            repeat=4,
117
+        ).generate('abcdef')
118
+        forbidden_substrings = {
119
+            b'0000',
120
+            b'1111',
121
+            b'2222',
122
+            b'3333',
123
+            b'4444',
124
+            b'5555',
125
+            b'6666',
126
+            b'7777',
127
+            b'8888',
128
+            b'9999',
129
+        }
104 130
         for substring in forbidden_substrings:
105 131
             assert substring not in generated
106 132
 
107 133
     def test_219_very_limited_character_set(self):
108
-        generated = Vault(phrase=b'', length=24, lower=0, upper=0,
109
-                          space=0, symbol=0).generate('testing')
134
+        generated = Vault(
135
+            phrase=b'', length=24, lower=0, upper=0, space=0, symbol=0
136
+        ).generate('testing')
110 137
         assert generated == b'763252593304946694588866'
111 138
 
112 139
     def test_220_character_set_subtraction(self):
113 140
         assert Vault._subtract(b'be', b'abcdef') == bytearray(b'acdf')
114 141
 
115
-    @pytest.mark.parametrize(['length', 'settings', 'entropy'], [
142
+    @pytest.mark.parametrize(
143
+        ['length', 'settings', 'entropy'],
144
+        [
116 145
             (20, {}, math.log2(math.factorial(20)) + 20 * math.log2(94)),
117 146
             (
118 147
                 20,
119 148
                 {'upper': 0, 'number': 0, 'space': 0, 'symbol': 0},
120
-            math.log2(math.factorial(20)) + 20 * math.log2(26)
149
+                math.log2(math.factorial(20)) + 20 * math.log2(26),
121 150
             ),
122 151
             (0, {}, float('-inf')),
123
-        (0, {'lower': 0, 'number': 0, 'space': 0, 'symbol': 0}, float('-inf')),
152
+            (
153
+                0,
154
+                {'lower': 0, 'number': 0, 'space': 0, 'symbol': 0},
155
+                float('-inf'),
156
+            ),
124 157
             (1, {}, math.log2(94)),
125 158
             (1, {'upper': 0, 'lower': 0, 'number': 0, 'symbol': 0}, 0.0),
126
-    ])
159
+        ],
160
+    )
127 161
     def test_221_entropy(
128 162
         self, length: int, settings: dict[str, int], entropy: int
129 163
     ) -> None:
... ...
@@ -131,38 +165,54 @@ class TestVault:
131 165
         assert math.isclose(v._entropy(), entropy)
132 166
         assert v._estimate_sufficient_hash_length() > 0
133 167
         if math.isfinite(entropy) and entropy:
134
-            assert (
135
-                v._estimate_sufficient_hash_length(1.0) ==
136
-                math.ceil(entropy / 8)
168
+            assert v._estimate_sufficient_hash_length(1.0) == math.ceil(
169
+                entropy / 8
137 170
             )
138 171
         assert v._estimate_sufficient_hash_length(8.0) >= entropy
139 172
 
140 173
     def test_222_hash_length_estimation(self) -> None:
141
-        v = Vault(phrase=self.phrase, lower=0, upper=0, number=0,
142
-                   symbol=0, space=1, length=1)
174
+        v = Vault(
175
+            phrase=self.phrase,
176
+            lower=0,
177
+            upper=0,
178
+            number=0,
179
+            symbol=0,
180
+            space=1,
181
+            length=1,
182
+        )
143 183
         assert v._entropy() == 0.0
144 184
         assert v._estimate_sufficient_hash_length() > 0
145 185
 
146
-    @pytest.mark.parametrize(['service', 'expected'], [
186
+    @pytest.mark.parametrize(
187
+        ['service', 'expected'],
188
+        [
147 189
             (b'google', google_phrase),
148 190
             ('twitter', twitter_phrase),
149
-    ])
191
+        ],
192
+    )
150 193
     def test_223_hash_length_expansion(
151 194
         self, monkeypatch: Any, service: str | bytes, expected: bytes
152 195
     ) -> None:
153 196
         v = Vault(phrase=self.phrase)
154
-        monkeypatch.setattr(v,
197
+        monkeypatch.setattr(
198
+            v,
155 199
             '_estimate_sufficient_hash_length',
156
-                            lambda *args, **kwargs: 1)  # noqa: ARG005
200
+            lambda *args, **kwargs: 1,  # noqa: ARG005
201
+        )
157 202
         assert v._estimate_sufficient_hash_length() < len(self.phrase)
158 203
         assert v.generate(service) == expected
159 204
 
160
-    @pytest.mark.parametrize(['s', 'raises'], [
161
-        ('ñ', True), ('Düsseldorf', True),
162
-        ('liberté, egalité, fraternité', True), ('ASCII', False),
205
+    @pytest.mark.parametrize(
206
+        ['s', 'raises'],
207
+        [
208
+            ('ñ', True),
209
+            ('Düsseldorf', True),
210
+            ('liberté, egalité, fraternité', True),
211
+            ('ASCII', False),
163 212
             (b'D\xc3\xbcsseldorf', False),
164 213
             (bytearray([2, 3, 5, 7, 11, 13]), False),
165
-    ])
214
+        ],
215
+    )
166 216
     def test_224_binary_strings(
167 217
         self, s: str | bytes | bytearray, raises: bool
168 218
     ) -> None:
... ...
@@ -181,15 +231,22 @@ class TestVault:
181 231
             assert binstr(binstr(s)) == bytes(s)
182 232
 
183 233
     def test_310_too_many_symbols(self):
184
-        with pytest.raises(ValueError,
185
-                           match='requested passphrase length too short'):
234
+        with pytest.raises(
235
+            ValueError, match='requested passphrase length too short'
236
+        ):
186 237
             Vault(phrase=self.phrase, symbol=100)
187 238
 
188 239
     def test_311_no_viable_characters(self):
189
-        with pytest.raises(ValueError,
190
-                           match='no allowed characters left'):
191
-            Vault(phrase=self.phrase, lower=0, upper=0, number=0,
192
-                  space=0, dash=0, symbol=0)
240
+        with pytest.raises(ValueError, match='no allowed characters left'):
241
+            Vault(
242
+                phrase=self.phrase,
243
+                lower=0,
244
+                upper=0,
245
+                number=0,
246
+                space=0,
247
+                dash=0,
248
+                symbol=0,
249
+            )
193 250
 
194 251
     def test_320_character_set_subtraction_duplicate(self):
195 252
         with pytest.raises(ValueError, match='duplicate characters'):
... ...
@@ -199,9 +256,9 @@ class TestVault:
199 256
 
200 257
     def test_322_hash_length_estimation(self) -> None:
201 258
         v = Vault(phrase=self.phrase)
202
-        with pytest.raises(ValueError,
203
-                           match='invalid safety factor'):
259
+        with pytest.raises(ValueError, match='invalid safety factor'):
204 260
             assert v._estimate_sufficient_hash_length(-1.0)
205
-        with pytest.raises(TypeError,
206
-                           match='invalid safety factor: not a float'):
261
+        with pytest.raises(
262
+            TypeError, match='invalid safety factor: not a float'
263
+        ):
207 264
             assert v._estimate_sufficient_hash_length(None)  # type: ignore
... ...
@@ -56,68 +58,91 @@ class OptionCombination(NamedTuple):
56 58
 
57 59
 
58 60
 PASSWORD_GENERATION_OPTIONS: list[tuple[str, ...]] = [
59
-    ('--phrase',), ('--key',), ('--length', '20'), ('--repeat', '20'),
60
-    ('--lower', '1'), ('--upper', '1'), ('--number', '1'),
61
-    ('--space', '1'), ('--dash', '1'), ('--symbol', '1')
61
+    ('--phrase',),
62
+    ('--key',),
63
+    ('--length', '20'),
64
+    ('--repeat', '20'),
65
+    ('--lower', '1'),
66
+    ('--upper', '1'),
67
+    ('--number', '1'),
68
+    ('--space', '1'),
69
+    ('--dash', '1'),
70
+    ('--symbol', '1'),
62 71
 ]
63 72
 CONFIGURATION_OPTIONS: list[tuple[str, ...]] = [
64
-    ('--notes',), ('--config',), ('--delete',), ('--delete-globals',),
65
-    ('--clear',)
73
+    ('--notes',),
74
+    ('--config',),
75
+    ('--delete',),
76
+    ('--delete-globals',),
77
+    ('--clear',),
66 78
 ]
67 79
 CONFIGURATION_COMMANDS: list[tuple[str, ...]] = [
68
-    ('--notes',), ('--delete',), ('--delete-globals',), ('--clear',)
69
-]
70
-STORAGE_OPTIONS: list[tuple[str, ...]] = [
71
-    ('--export', '-'), ('--import', '-')
80
+    ('--notes',),
81
+    ('--delete',),
82
+    ('--delete-globals',),
83
+    ('--clear',),
72 84
 ]
85
+STORAGE_OPTIONS: list[tuple[str, ...]] = [('--export', '-'), ('--import', '-')]
73 86
 INCOMPATIBLE: dict[tuple[str, ...], IncompatibleConfiguration] = {
74 87
     ('--phrase',): IncompatibleConfiguration(
75 88
         [('--key',), *CONFIGURATION_COMMANDS, *STORAGE_OPTIONS],
76
-        True, DUMMY_PASSPHRASE),
89
+        True,
90
+        DUMMY_PASSPHRASE,
91
+    ),
77 92
     ('--key',): IncompatibleConfiguration(
78
-        CONFIGURATION_COMMANDS + STORAGE_OPTIONS,
79
-        True, DUMMY_PASSPHRASE),
93
+        CONFIGURATION_COMMANDS + STORAGE_OPTIONS, True, DUMMY_PASSPHRASE
94
+    ),
80 95
     ('--length', '20'): IncompatibleConfiguration(
81
-        CONFIGURATION_COMMANDS + STORAGE_OPTIONS,
82
-        True, DUMMY_PASSPHRASE),
96
+        CONFIGURATION_COMMANDS + STORAGE_OPTIONS, True, DUMMY_PASSPHRASE
97
+    ),
83 98
     ('--repeat', '20'): IncompatibleConfiguration(
84
-        CONFIGURATION_COMMANDS + STORAGE_OPTIONS,
85
-        True, DUMMY_PASSPHRASE),
99
+        CONFIGURATION_COMMANDS + STORAGE_OPTIONS, True, DUMMY_PASSPHRASE
100
+    ),
86 101
     ('--lower', '1'): IncompatibleConfiguration(
87
-        CONFIGURATION_COMMANDS + STORAGE_OPTIONS,
88
-        True, DUMMY_PASSPHRASE),
102
+        CONFIGURATION_COMMANDS + STORAGE_OPTIONS, True, DUMMY_PASSPHRASE
103
+    ),
89 104
     ('--upper', '1'): IncompatibleConfiguration(
90
-        CONFIGURATION_COMMANDS + STORAGE_OPTIONS,
91
-        True, DUMMY_PASSPHRASE),
105
+        CONFIGURATION_COMMANDS + STORAGE_OPTIONS, True, DUMMY_PASSPHRASE
106
+    ),
92 107
     ('--number', '1'): IncompatibleConfiguration(
93
-        CONFIGURATION_COMMANDS + STORAGE_OPTIONS,
94
-        True, DUMMY_PASSPHRASE),
108
+        CONFIGURATION_COMMANDS + STORAGE_OPTIONS, True, DUMMY_PASSPHRASE
109
+    ),
95 110
     ('--space', '1'): IncompatibleConfiguration(
96
-        CONFIGURATION_COMMANDS + STORAGE_OPTIONS,
97
-        True, DUMMY_PASSPHRASE),
111
+        CONFIGURATION_COMMANDS + STORAGE_OPTIONS, True, DUMMY_PASSPHRASE
112
+    ),
98 113
     ('--dash', '1'): IncompatibleConfiguration(
99
-        CONFIGURATION_COMMANDS + STORAGE_OPTIONS,
100
-        True, DUMMY_PASSPHRASE),
114
+        CONFIGURATION_COMMANDS + STORAGE_OPTIONS, True, DUMMY_PASSPHRASE
115
+    ),
101 116
     ('--symbol', '1'): IncompatibleConfiguration(
102
-        CONFIGURATION_COMMANDS + STORAGE_OPTIONS,
103
-        True, DUMMY_PASSPHRASE),
117
+        CONFIGURATION_COMMANDS + STORAGE_OPTIONS, True, DUMMY_PASSPHRASE
118
+    ),
104 119
     ('--notes',): IncompatibleConfiguration(
105
-        [('--config',), ('--delete',), ('--delete-globals',),
106
-         ('--clear',), *STORAGE_OPTIONS],
107
-        True, None),
120
+        [
121
+            ('--config',),
122
+            ('--delete',),
123
+            ('--delete-globals',),
124
+            ('--clear',),
125
+            *STORAGE_OPTIONS,
126
+        ],
127
+        True,
128
+        None,
129
+    ),
108 130
     ('--config', '-p'): IncompatibleConfiguration(
109
-        [('--delete',), ('--delete-globals',),
110
-         ('--clear',), *STORAGE_OPTIONS],
111
-        None, DUMMY_PASSPHRASE),
131
+        [('--delete',), ('--delete-globals',), ('--clear',), *STORAGE_OPTIONS],
132
+        None,
133
+        DUMMY_PASSPHRASE,
134
+    ),
112 135
     ('--delete',): IncompatibleConfiguration(
113
-        [('--delete-globals',), ('--clear',), *STORAGE_OPTIONS], True, None),
136
+        [('--delete-globals',), ('--clear',), *STORAGE_OPTIONS], True, None
137
+    ),
114 138
     ('--delete-globals',): IncompatibleConfiguration(
115
-        [('--clear',), *STORAGE_OPTIONS], False, None),
139
+        [('--clear',), *STORAGE_OPTIONS], False, None
140
+    ),
116 141
     ('--clear',): IncompatibleConfiguration(STORAGE_OPTIONS, False, None),
117 142
     ('--export', '-'): IncompatibleConfiguration(
118
-        [('--import', '-')], False, None),
119
-    ('--import', '-'): IncompatibleConfiguration(
120
-        [], False, None),
143
+        [('--import', '-')], False, None
144
+    ),
145
+    ('--import', '-'): IncompatibleConfiguration([], False, None),
121 146
 }
122 147
 SINGLES: dict[tuple[str, ...], SingleConfiguration] = {
123 148
     ('--phrase',): SingleConfiguration(True, DUMMY_PASSPHRASE, True),
... ...
@@ -143,37 +168,50 @@ config: IncompatibleConfiguration | SingleConfiguration
143 168
 for opt, config in INCOMPATIBLE.items():
144 169
     for opt2 in config.other_options:
145 170
         INTERESTING_OPTION_COMBINATIONS.extend([
146
-            OptionCombination(options=list(opt + opt2), incompatible=True,
171
+            OptionCombination(
172
+                options=list(opt + opt2),
173
+                incompatible=True,
147 174
                 needs_service=config.needs_service,
148
-                              input=config.input, check_success=False),
149
-            OptionCombination(options=list(opt2 + opt), incompatible=True,
175
+                input=config.input,
176
+                check_success=False,
177
+            ),
178
+            OptionCombination(
179
+                options=list(opt2 + opt),
180
+                incompatible=True,
150 181
                 needs_service=config.needs_service,
151
-                              input=config.input, check_success=False)
182
+                input=config.input,
183
+                check_success=False,
184
+            ),
152 185
         ])
153 186
 for opt, config in SINGLES.items():
154 187
     INTERESTING_OPTION_COMBINATIONS.append(
155
-        OptionCombination(options=list(opt), incompatible=False,
188
+        OptionCombination(
189
+            options=list(opt),
190
+            incompatible=False,
156 191
             needs_service=config.needs_service,
157 192
             input=config.input,
158
-                          check_success=config.check_success))
193
+            check_success=config.check_success,
194
+        )
195
+    )
159 196
 
160 197
 
161 198
 class TestCLI:
162 199
     def test_200_help_output(self):
163 200
         runner = click.testing.CliRunner(mix_stderr=False)
164
-        result = runner.invoke(cli.derivepassphrase, ['--help'],
165
-                               catch_exceptions=False)
166
-        assert result.exit_code == 0
167
-        assert 'Password generation:\n' in result.output, (
168
-            'Option groups not respected in help text.'
169
-        )
170
-        assert 'Use NUMBER=0, e.g. "--symbol 0"' in result.output, (
171
-            'Option group epilog not printed.'
201
+        result = runner.invoke(
202
+            cli.derivepassphrase, ['--help'], catch_exceptions=False
172 203
         )
204
+        assert result.exit_code == 0
205
+        assert (
206
+            'Password generation:\n' in result.output
207
+        ), 'Option groups not respected in help text.'
208
+        assert (
209
+            'Use NUMBER=0, e.g. "--symbol 0"' in result.output
210
+        ), 'Option group epilog not printed.'
173 211
 
174
-    @pytest.mark.parametrize('charset_name',
175
-                             ['lower', 'upper', 'number', 'space',
176
-                              'dash', 'symbol'])
212
+    @pytest.mark.parametrize(
213
+        'charset_name', ['lower', 'upper', 'number', 'space', 'dash', 'symbol']
214
+    )
177 215
     def test_201_disable_character_set(
178 216
         self, monkeypatch: Any, charset_name: str
179 217
     ) -> None:
... ...
@@ -181,15 +219,18 @@ class TestCLI:
181 219
         option = f'--{charset_name}'
182 220
         charset = dpp.Vault._CHARSETS[charset_name].decode('ascii')
183 221
         runner = click.testing.CliRunner(mix_stderr=False)
184
-        result = runner.invoke(cli.derivepassphrase,
222
+        result = runner.invoke(
223
+            cli.derivepassphrase,
185 224
             [option, '0', '-p', DUMMY_SERVICE],
186
-                               input=DUMMY_PASSPHRASE, catch_exceptions=False)
187
-        assert result.exit_code == 0, (
188
-            f'program died unexpectedly with exit code {result.exit_code}'
189
-        )
190
-        assert not result.stderr_bytes, (
191
-            f'program barfed on stderr: {result.stderr_bytes!r}'
225
+            input=DUMMY_PASSPHRASE,
226
+            catch_exceptions=False,
192 227
         )
228
+        assert (
229
+            result.exit_code == 0
230
+        ), f'program died unexpectedly with exit code {result.exit_code}'
231
+        assert (
232
+            not result.stderr_bytes
233
+        ), f'program barfed on stderr: {result.stderr_bytes!r}'
193 234
         for c in charset:
194 235
             assert c not in result.stdout, (
195 236
                 f'derived password contains forbidden character {c!r}: '
... ...
@@ -199,15 +240,18 @@ class TestCLI:
199 240
     def test_202_disable_repetition(self, monkeypatch: Any) -> None:
200 241
         monkeypatch.setattr(cli, '_prompt_for_passphrase', tests.auto_prompt)
201 242
         runner = click.testing.CliRunner(mix_stderr=False)
202
-        result = runner.invoke(cli.derivepassphrase,
243
+        result = runner.invoke(
244
+            cli.derivepassphrase,
203 245
             ['--repeat', '0', '-p', DUMMY_SERVICE],
204
-                               input=DUMMY_PASSPHRASE, catch_exceptions=False)
205
-        assert result.exit_code == 0, (
206
-            f'program died unexpectedly with exit code {result.exit_code}'
207
-        )
208
-        assert not result.stderr_bytes, (
209
-            f'program barfed on stderr: {result.stderr_bytes!r}'
246
+            input=DUMMY_PASSPHRASE,
247
+            catch_exceptions=False,
210 248
         )
249
+        assert (
250
+            result.exit_code == 0
251
+        ), f'program died unexpectedly with exit code {result.exit_code}'
252
+        assert (
253
+            not result.stderr_bytes
254
+        ), f'program barfed on stderr: {result.stderr_bytes!r}'
211 255
         passphrase = result.stdout.rstrip('\r\n')
212 256
         for i in range(len(passphrase) - 1):
213 257
             assert passphrase[i : i + 1] != passphrase[i + 1 : i + 2], (
... ...
@@ -215,391 +259,497 @@ class TestCLI:
215 259
                 f'at position {i}: {result.stdout!r}'
216 260
             )
217 261
 
218
-    @pytest.mark.parametrize('config', [
219
-        pytest.param({'global': {'key': DUMMY_KEY1_B64},
220
-                      'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}},
221
-                     id='global'),
222
-        pytest.param({'global': {'phrase': DUMMY_PASSPHRASE.rstrip(b'\n')
223
-                                           .decode('ASCII')},
224
-                      'services': {DUMMY_SERVICE: {'key': DUMMY_KEY1_B64,
225
-                                                   **DUMMY_CONFIG_SETTINGS}}},
226
-                     id='service'),
227
-    ])
262
+    @pytest.mark.parametrize(
263
+        'config',
264
+        [
265
+            pytest.param(
266
+                {
267
+                    'global': {'key': DUMMY_KEY1_B64},
268
+                    'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS},
269
+                },
270
+                id='global',
271
+            ),
272
+            pytest.param(
273
+                {
274
+                    'global': {
275
+                        'phrase': DUMMY_PASSPHRASE.rstrip(b'\n').decode(
276
+                            'ASCII'
277
+                        )
278
+                    },
279
+                    'services': {
280
+                        DUMMY_SERVICE: {
281
+                            'key': DUMMY_KEY1_B64,
282
+                            **DUMMY_CONFIG_SETTINGS,
283
+                        }
284
+                    },
285
+                },
286
+                id='service',
287
+            ),
288
+        ],
289
+    )
228 290
     def test_204a_key_from_config(
229
-        self, monkeypatch: Any, config: dpp.types.VaultConfig,
291
+        self,
292
+        monkeypatch: Any,
293
+        config: dpp.types.VaultConfig,
230 294
     ) -> None:
231 295
         runner = click.testing.CliRunner(mix_stderr=False)
232
-        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
233
-                                   config=config):
234
-            monkeypatch.setattr(dpp.Vault, 'phrase_from_key',
235
-                                tests.phrase_from_key)
236
-            result = runner.invoke(cli.derivepassphrase, [DUMMY_SERVICE],
237
-                                   catch_exceptions=False)
238
-            assert (result.exit_code, result.stderr_bytes) == (0, b''), (
239
-                'program exited with failure'
296
+        with tests.isolated_config(
297
+            monkeypatch=monkeypatch, runner=runner, config=config
298
+        ):
299
+            monkeypatch.setattr(
300
+                dpp.Vault, 'phrase_from_key', tests.phrase_from_key
301
+            )
302
+            result = runner.invoke(
303
+                cli.derivepassphrase, [DUMMY_SERVICE], catch_exceptions=False
240 304
             )
305
+            assert (result.exit_code, result.stderr_bytes) == (
306
+                0,
307
+                b'',
308
+            ), 'program exited with failure'
241 309
             assert (
242 310
                 result.stdout_bytes.rstrip(b'\n') != DUMMY_RESULT_PASSPHRASE
243
-            ), (
244
-                'program generated unexpected result (phrase instead of key)'
245
-            )
246
-            assert result.stdout_bytes.rstrip(b'\n') == DUMMY_RESULT_KEY1, (
247
-                'program generated unexpected result (wrong settings?)'
248
-            )
311
+            ), 'program generated unexpected result (phrase instead of key)'
312
+            assert (
313
+                result.stdout_bytes.rstrip(b'\n') == DUMMY_RESULT_KEY1
314
+            ), 'program generated unexpected result (wrong settings?)'
249 315
 
250 316
     def test_204b_key_from_command_line(self, monkeypatch: Any) -> None:
251 317
         runner = click.testing.CliRunner(mix_stderr=False)
252
-        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
253
-                                   config={'services': {DUMMY_SERVICE:
254
-                                               DUMMY_CONFIG_SETTINGS}}):
255
-            monkeypatch.setattr(cli, '_get_suitable_ssh_keys',
256
-                                tests.suitable_ssh_keys)
257
-            monkeypatch.setattr(dpp.Vault, 'phrase_from_key',
258
-                                tests.phrase_from_key)
259
-            result = runner.invoke(cli.derivepassphrase,
318
+        with tests.isolated_config(
319
+            monkeypatch=monkeypatch,
320
+            runner=runner,
321
+            config={'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}},
322
+        ):
323
+            monkeypatch.setattr(
324
+                cli, '_get_suitable_ssh_keys', tests.suitable_ssh_keys
325
+            )
326
+            monkeypatch.setattr(
327
+                dpp.Vault, 'phrase_from_key', tests.phrase_from_key
328
+            )
329
+            result = runner.invoke(
330
+                cli.derivepassphrase,
260 331
                 ['-k', DUMMY_SERVICE],
261
-                                   input=b'1\n', catch_exceptions=False)
332
+                input=b'1\n',
333
+                catch_exceptions=False,
334
+            )
262 335
             assert result.exit_code == 0, 'program exited with failure'
263 336
             assert result.stdout_bytes, 'program output expected'
264 337
             last_line = result.stdout_bytes.splitlines(True)[-1]
265
-            assert last_line.rstrip(b'\n') != DUMMY_RESULT_PASSPHRASE, (
266
-                'program generated unexpected result (phrase instead of key)'
267
-            )
268
-            assert last_line.rstrip(b'\n') == DUMMY_RESULT_KEY1, (
269
-                'program generated unexpected result (wrong settings?)'
270
-            )
338
+            assert (
339
+                last_line.rstrip(b'\n') != DUMMY_RESULT_PASSPHRASE
340
+            ), 'program generated unexpected result (phrase instead of key)'
341
+            assert (
342
+                last_line.rstrip(b'\n') == DUMMY_RESULT_KEY1
343
+            ), 'program generated unexpected result (wrong settings?)'
271 344
 
272 345
     def test_205_service_phrase_if_key_in_global_config(
273
-        self, monkeypatch: Any,
346
+        self,
347
+        monkeypatch: Any,
274 348
     ) -> None:
275 349
         runner = click.testing.CliRunner(mix_stderr=False)
276 350
         with tests.isolated_config(
277
-            monkeypatch=monkeypatch, runner=runner,
351
+            monkeypatch=monkeypatch,
352
+            runner=runner,
278 353
             config={
279 354
                 'global': {'key': DUMMY_KEY1_B64},
280 355
                 'services': {
281 356
                     DUMMY_SERVICE: {
282
-                        'phrase': DUMMY_PASSPHRASE.rstrip(b'\n')
283
-                                  .decode('ASCII'),
284
-                        **DUMMY_CONFIG_SETTINGS}}}
357
+                        'phrase': DUMMY_PASSPHRASE.rstrip(b'\n').decode(
358
+                            'ASCII'
359
+                        ),
360
+                        **DUMMY_CONFIG_SETTINGS,
361
+                    }
362
+                },
363
+            },
285 364
         ):
286
-            result = runner.invoke(cli.derivepassphrase, [DUMMY_SERVICE],
287
-                                   catch_exceptions=False)
365
+            result = runner.invoke(
366
+                cli.derivepassphrase, [DUMMY_SERVICE], catch_exceptions=False
367
+            )
288 368
             assert result.exit_code == 0, 'program exited with failure'
289 369
             assert result.stdout_bytes, 'program output expected'
290 370
             last_line = result.stdout_bytes.splitlines(True)[-1]
291
-            assert last_line.rstrip(b'\n') != DUMMY_RESULT_KEY1, (
292
-                'program generated unexpected result (key instead of phrase)'
293
-            )
294
-            assert last_line.rstrip(b'\n') == DUMMY_RESULT_PASSPHRASE, (
295
-                'program generated unexpected result (wrong settings?)'
296
-            )
371
+            assert (
372
+                last_line.rstrip(b'\n') != DUMMY_RESULT_KEY1
373
+            ), 'program generated unexpected result (key instead of phrase)'
374
+            assert (
375
+                last_line.rstrip(b'\n') == DUMMY_RESULT_PASSPHRASE
376
+            ), 'program generated unexpected result (wrong settings?)'
297 377
 
298
-    @pytest.mark.parametrize('option',
299
-                             ['--lower', '--upper', '--number',
300
-                              '--space', '--dash', '--symbol',
301
-                              '--repeat', '--length'])
378
+    @pytest.mark.parametrize(
379
+        'option',
380
+        [
381
+            '--lower',
382
+            '--upper',
383
+            '--number',
384
+            '--space',
385
+            '--dash',
386
+            '--symbol',
387
+            '--repeat',
388
+            '--length',
389
+        ],
390
+    )
302 391
     def test_210_invalid_argument_range(self, option: str) -> None:
303 392
         runner = click.testing.CliRunner(mix_stderr=False)
304 393
         value: str | int
305 394
         for value in '-42', 'invalid':
306
-            result = runner.invoke(cli.derivepassphrase,
307
-                                   [option, cast(str, value), '-p',
308
-                                    DUMMY_SERVICE],
395
+            result = runner.invoke(
396
+                cli.derivepassphrase,
397
+                [option, cast(str, value), '-p', DUMMY_SERVICE],
309 398
                 input=DUMMY_PASSPHRASE,
310
-                                   catch_exceptions=False)
311
-            assert result.exit_code > 0, (
312
-                'program unexpectedly succeeded'
313
-            )
314
-            assert result.stderr_bytes, (
315
-                'program did not print any error message'
316
-            )
317
-            assert b'Error: Invalid value' in result.stderr_bytes, (
318
-                'program did not print the expected error message'
399
+                catch_exceptions=False,
319 400
             )
401
+            assert result.exit_code > 0, 'program unexpectedly succeeded'
402
+            assert (
403
+                result.stderr_bytes
404
+            ), 'program did not print any error message'
405
+            assert (
406
+                b'Error: Invalid value' in result.stderr_bytes
407
+            ), 'program did not print the expected error message'
320 408
 
321 409
     @pytest.mark.parametrize(
322 410
         ['options', 'service', 'input', 'check_success'],
323
-        [(o.options, o.needs_service, o.input, o.check_success)
324
-         for o in INTERESTING_OPTION_COMBINATIONS if not o.incompatible],
411
+        [
412
+            (o.options, o.needs_service, o.input, o.check_success)
413
+            for o in INTERESTING_OPTION_COMBINATIONS
414
+            if not o.incompatible
415
+        ],
325 416
     )
326 417
     def test_211_service_needed(
327
-        self, monkeypatch: Any, options: list[str],
328
-        service: bool | None, input: bytes | None, check_success: bool,
418
+        self,
419
+        monkeypatch: Any,
420
+        options: list[str],
421
+        service: bool | None,
422
+        input: bytes | None,
423
+        check_success: bool,
329 424
     ) -> None:
330 425
         monkeypatch.setattr(cli, '_prompt_for_passphrase', tests.auto_prompt)
331 426
         runner = click.testing.CliRunner(mix_stderr=False)
332
-        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
333
-                                   config={'global': {'phrase': 'abc'},
334
-                                           'services': {}}):
335
-            result = runner.invoke(cli.derivepassphrase,
336
-                                   options if service
337
-                                   else [*options, DUMMY_SERVICE],
338
-                                   input=input, catch_exceptions=False)
339
-            if service is not None:
340
-                assert result.exit_code > 0, (
341
-                    'program unexpectedly succeeded'
342
-                )
343
-                assert result.stderr_bytes, (
344
-                    'program did not print any error message'
427
+        with tests.isolated_config(
428
+            monkeypatch=monkeypatch,
429
+            runner=runner,
430
+            config={'global': {'phrase': 'abc'}, 'services': {}},
431
+        ):
432
+            result = runner.invoke(
433
+                cli.derivepassphrase,
434
+                options if service else [*options, DUMMY_SERVICE],
435
+                input=input,
436
+                catch_exceptions=False,
345 437
             )
346
-                err_msg = (b' requires a SERVICE' if service
347
-                           else b' does not take a SERVICE argument')
348
-                assert err_msg in result.stderr_bytes, (
349
-                    'program did not print the expected error message'
438
+            if service is not None:
439
+                assert result.exit_code > 0, 'program unexpectedly succeeded'
440
+                assert (
441
+                    result.stderr_bytes
442
+                ), 'program did not print any error message'
443
+                err_msg = (
444
+                    b' requires a SERVICE'
445
+                    if service
446
+                    else b' does not take a SERVICE argument'
350 447
                 )
448
+                assert (
449
+                    err_msg in result.stderr_bytes
450
+                ), 'program did not print the expected error message'
351 451
             else:
352
-                assert (result.exit_code, result.stderr_bytes) == (0, b''), (
353
-                    'program unexpectedly failed'
354
-                )
452
+                assert (result.exit_code, result.stderr_bytes) == (
453
+                    0,
454
+                    b'',
455
+                ), 'program unexpectedly failed'
355 456
         if check_success:
356
-            with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
357
-                                       config={'global': {'phrase': 'abc'},
358
-                                               'services': {}}):
359
-                monkeypatch.setattr(cli, '_prompt_for_passphrase',
360
-                                    tests.auto_prompt)
361
-                result = runner.invoke(cli.derivepassphrase,
362
-                                       [*options, DUMMY_SERVICE]
363
-                                       if service else options,
364
-                                       input=input, catch_exceptions=False)
365
-                assert (result.exit_code, result.stderr_bytes) == (0, b''), (
366
-                    'program unexpectedly failed'
457
+            with tests.isolated_config(
458
+                monkeypatch=monkeypatch,
459
+                runner=runner,
460
+                config={'global': {'phrase': 'abc'}, 'services': {}},
461
+            ):
462
+                monkeypatch.setattr(
463
+                    cli, '_prompt_for_passphrase', tests.auto_prompt
464
+                )
465
+                result = runner.invoke(
466
+                    cli.derivepassphrase,
467
+                    [*options, DUMMY_SERVICE] if service else options,
468
+                    input=input,
469
+                    catch_exceptions=False,
367 470
                 )
471
+                assert (result.exit_code, result.stderr_bytes) == (
472
+                    0,
473
+                    b'',
474
+                ), 'program unexpectedly failed'
368 475
 
369 476
     @pytest.mark.parametrize(
370 477
         ['options', 'service'],
371
-        [(o.options, o.needs_service)
372
-         for o in INTERESTING_OPTION_COMBINATIONS if o.incompatible],
478
+        [
479
+            (o.options, o.needs_service)
480
+            for o in INTERESTING_OPTION_COMBINATIONS
481
+            if o.incompatible
482
+        ],
373 483
     )
374 484
     def test_212_incompatible_options(
375
-        self, options: list[str], service: bool | None,
485
+        self,
486
+        options: list[str],
487
+        service: bool | None,
376 488
     ) -> None:
377 489
         runner = click.testing.CliRunner(mix_stderr=False)
378
-        result = runner.invoke(cli.derivepassphrase,
379
-                               [*options, DUMMY_SERVICE] if service
380
-                               else options,
381
-                               input=DUMMY_PASSPHRASE, catch_exceptions=False)
382
-        assert result.exit_code > 0, (
383
-            'program unexpectedly succeeded'
384
-        )
385
-        assert result.stderr_bytes, (
386
-            'program did not print any error message'
387
-        )
388
-        assert b'mutually exclusive with ' in result.stderr_bytes, (
389
-            'program did not print the expected error message'
490
+        result = runner.invoke(
491
+            cli.derivepassphrase,
492
+            [*options, DUMMY_SERVICE] if service else options,
493
+            input=DUMMY_PASSPHRASE,
494
+            catch_exceptions=False,
390 495
         )
496
+        assert result.exit_code > 0, 'program unexpectedly succeeded'
497
+        assert result.stderr_bytes, 'program did not print any error message'
498
+        assert (
499
+            b'mutually exclusive with ' in result.stderr_bytes
500
+        ), 'program did not print the expected error message'
391 501
 
392 502
     def test_213_import_bad_config_not_vault_config(
393
-        self, monkeypatch: Any,
503
+        self,
504
+        monkeypatch: Any,
394 505
     ) -> None:
395 506
         runner = click.testing.CliRunner(mix_stderr=False)
396
-        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
397
-                                   config={'services': {}}):
398
-            result = runner.invoke(cli.derivepassphrase, ['--import', '-'],
399
-                                   input=b'null', catch_exceptions=False)
400
-            assert result.exit_code > 0, (
401
-                'program unexpectedly succeeded'
402
-            )
403
-            assert result.stderr_bytes, (
404
-                'program did not print any error message'
405
-            )
406
-            assert b'not a valid config' in result.stderr_bytes, (
407
-                'program did not print the expected error message'
507
+        with tests.isolated_config(
508
+            monkeypatch=monkeypatch, runner=runner, config={'services': {}}
509
+        ):
510
+            result = runner.invoke(
511
+                cli.derivepassphrase,
512
+                ['--import', '-'],
513
+                input=b'null',
514
+                catch_exceptions=False,
408 515
             )
516
+            assert result.exit_code > 0, 'program unexpectedly succeeded'
517
+            assert (
518
+                result.stderr_bytes
519
+            ), 'program did not print any error message'
520
+            assert (
521
+                b'not a valid config' in result.stderr_bytes
522
+            ), 'program did not print the expected error message'
409 523
 
410 524
     def test_213a_import_bad_config_not_json_data(
411
-        self, monkeypatch: Any,
525
+        self,
526
+        monkeypatch: Any,
412 527
     ) -> None:
413 528
         runner = click.testing.CliRunner(mix_stderr=False)
414
-        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
415
-                                   config={'services': {}}):
416
-            result = runner.invoke(cli.derivepassphrase, ['--import', '-'],
529
+        with tests.isolated_config(
530
+            monkeypatch=monkeypatch, runner=runner, config={'services': {}}
531
+        ):
532
+            result = runner.invoke(
533
+                cli.derivepassphrase,
534
+                ['--import', '-'],
417 535
                 input=b'This string is not valid JSON.',
418
-                                   catch_exceptions=False)
419
-            assert result.exit_code > 0, (
420
-                'program unexpectedly succeeded'
421
-            )
422
-            assert result.stderr_bytes, (
423
-                'program did not print any error message'
424
-            )
425
-            assert b'cannot decode JSON' in result.stderr_bytes, (
426
-                'program did not print the expected error message'
536
+                catch_exceptions=False,
427 537
             )
538
+            assert result.exit_code > 0, 'program unexpectedly succeeded'
539
+            assert (
540
+                result.stderr_bytes
541
+            ), 'program did not print any error message'
542
+            assert (
543
+                b'cannot decode JSON' in result.stderr_bytes
544
+            ), 'program did not print the expected error message'
428 545
 
429 546
     def test_213b_import_bad_config_not_a_file(
430
-        self, monkeypatch: Any,
547
+        self,
548
+        monkeypatch: Any,
431 549
     ) -> None:
432 550
         runner = click.testing.CliRunner(mix_stderr=False)
433 551
         # `isolated_config` validates the configuration.  So, to pass an
434 552
         # actual broken configuration, we must open the configuration file
435 553
         # ourselves afterwards, inside the context.
436
-        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
437
-                                   config={'services': {}}):
438
-            with open(cli._config_filename(), 'w',
439
-                      encoding='UTF-8') as outfile:
554
+        with tests.isolated_config(
555
+            monkeypatch=monkeypatch, runner=runner, config={'services': {}}
556
+        ):
557
+            with open(
558
+                cli._config_filename(), 'w', encoding='UTF-8'
559
+            ) as outfile:
440 560
                 print('This string is not valid JSON.', file=outfile)
441 561
             dname = os.path.dirname(cli._config_filename())
442 562
             result = runner.invoke(
443 563
                 cli.derivepassphrase,
444 564
                 ['--import', os.fsdecode(dname)],
445
-                catch_exceptions=False)
446
-            assert result.exit_code > 0, (
447
-                'program unexpectedly succeeded'
448
-            )
449
-            assert result.stderr_bytes, (
450
-                'program did not print any error message'
565
+                catch_exceptions=False,
451 566
             )
567
+            assert result.exit_code > 0, 'program unexpectedly succeeded'
568
+            assert (
569
+                result.stderr_bytes
570
+            ), 'program did not print any error message'
452 571
             # Don't test the actual error message, because it is subject to
453 572
             # locale settings.  TODO: find a way anyway.
454 573
 
455 574
     def test_214_export_settings_no_stored_settings(
456
-        self, monkeypatch: Any,
575
+        self,
576
+        monkeypatch: Any,
457 577
     ) -> None:
458 578
         runner = click.testing.CliRunner(mix_stderr=False)
459
-        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
460
-                                   config={'services': {}}):
579
+        with tests.isolated_config(
580
+            monkeypatch=monkeypatch, runner=runner, config={'services': {}}
581
+        ):
461 582
             with contextlib.suppress(FileNotFoundError):
462 583
                 os.remove(cli._config_filename())
463
-            result = runner.invoke(cli.derivepassphrase, ['--export', '-'],
464
-                                   catch_exceptions=False)
465
-            assert (result.exit_code, result.stderr_bytes) == (0, b''), (
466
-                'program exited with failure'
584
+            result = runner.invoke(
585
+                cli.derivepassphrase, ['--export', '-'], catch_exceptions=False
467 586
             )
587
+            assert (result.exit_code, result.stderr_bytes) == (
588
+                0,
589
+                b'',
590
+            ), 'program exited with failure'
468 591
 
469 592
     def test_214a_export_settings_bad_stored_config(
470
-        self, monkeypatch: Any,
593
+        self,
594
+        monkeypatch: Any,
471 595
     ) -> None:
472 596
         runner = click.testing.CliRunner(mix_stderr=False)
473
-        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
474
-                                   config={}):
475
-            result = runner.invoke(cli.derivepassphrase, ['--export', '-'],
476
-                                   input=b'null', catch_exceptions=False)
477
-            assert result.exit_code > 0, (
478
-                'program unexpectedly succeeded'
479
-            )
480
-            assert result.stderr_bytes, (
481
-                'program did not print any error message'
482
-            )
483
-            assert b'cannot load config' in result.stderr_bytes, (
484
-                'program did not print the expected error message'
597
+        with tests.isolated_config(
598
+            monkeypatch=monkeypatch, runner=runner, config={}
599
+        ):
600
+            result = runner.invoke(
601
+                cli.derivepassphrase,
602
+                ['--export', '-'],
603
+                input=b'null',
604
+                catch_exceptions=False,
485 605
             )
606
+            assert result.exit_code > 0, 'program unexpectedly succeeded'
607
+            assert (
608
+                result.stderr_bytes
609
+            ), 'program did not print any error message'
610
+            assert (
611
+                b'cannot load config' in result.stderr_bytes
612
+            ), 'program did not print the expected error message'
486 613
 
487 614
     def test_214b_export_settings_not_a_file(
488
-        self, monkeypatch: Any,
615
+        self,
616
+        monkeypatch: Any,
489 617
     ) -> None:
490 618
         runner = click.testing.CliRunner(mix_stderr=False)
491
-        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
492
-                                   config={'services': {}}):
619
+        with tests.isolated_config(
620
+            monkeypatch=monkeypatch, runner=runner, config={'services': {}}
621
+        ):
493 622
             with contextlib.suppress(FileNotFoundError):
494 623
                 os.remove(cli._config_filename())
495 624
             os.makedirs(cli._config_filename())
496
-            result = runner.invoke(cli.derivepassphrase, ['--export', '-'],
497
-                                   input=b'null', catch_exceptions=False)
498
-            assert result.exit_code > 0, (
499
-                'program unexpectedly succeeded'
500
-            )
501
-            assert result.stderr_bytes, (
502
-                'program did not print any error message'
503
-            )
504
-            assert b'cannot load config' in result.stderr_bytes, (
505
-                'program did not print the expected error message'
625
+            result = runner.invoke(
626
+                cli.derivepassphrase,
627
+                ['--export', '-'],
628
+                input=b'null',
629
+                catch_exceptions=False,
506 630
             )
631
+            assert result.exit_code > 0, 'program unexpectedly succeeded'
632
+            assert (
633
+                result.stderr_bytes
634
+            ), 'program did not print any error message'
635
+            assert (
636
+                b'cannot load config' in result.stderr_bytes
637
+            ), 'program did not print the expected error message'
507 638
 
508 639
     def test_214c_export_settings_target_not_a_file(
509
-        self, monkeypatch: Any,
640
+        self,
641
+        monkeypatch: Any,
510 642
     ) -> None:
511 643
         runner = click.testing.CliRunner(mix_stderr=False)
512
-        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
513
-                                   config={'services': {}}):
644
+        with tests.isolated_config(
645
+            monkeypatch=monkeypatch, runner=runner, config={'services': {}}
646
+        ):
514 647
             dname = os.path.dirname(cli._config_filename())
515
-            result = runner.invoke(cli.derivepassphrase,
648
+            result = runner.invoke(
649
+                cli.derivepassphrase,
516 650
                 ['--export', os.fsdecode(dname)],
517
-                                   input=b'null', catch_exceptions=False)
518
-            assert result.exit_code > 0, (
519
-                'program unexpectedly succeeded'
520
-            )
521
-            assert result.stderr_bytes, (
522
-                'program did not print any error message'
523
-            )
524
-            assert b'cannot write config' in result.stderr_bytes, (
525
-                'program did not print the expected error message'
651
+                input=b'null',
652
+                catch_exceptions=False,
526 653
             )
654
+            assert result.exit_code > 0, 'program unexpectedly succeeded'
655
+            assert (
656
+                result.stderr_bytes
657
+            ), 'program did not print any error message'
658
+            assert (
659
+                b'cannot write config' in result.stderr_bytes
660
+            ), 'program did not print the expected error message'
527 661
 
528 662
     def test_220_edit_notes_successfully(self, monkeypatch: Any) -> None:
529
-        edit_result = '''
663
+        edit_result = """
530 664
 
531 665
 # - - - - - >8 - - - - - >8 - - - - - >8 - - - - - >8 - - - - -
532 666
 contents go here
533
-'''
667
+"""
534 668
         runner = click.testing.CliRunner(mix_stderr=False)
535
-        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
536
-                                   config={'global': {'phrase': 'abc'},
537
-                                           'services': {}}):
538
-            monkeypatch.setattr(click, 'edit',
539
-                                lambda *a, **kw: edit_result)  # noqa: ARG005
540
-            result = runner.invoke(cli.derivepassphrase, ['--notes', 'sv'],
541
-                                   catch_exceptions=False)
542
-            assert (result.exit_code, result.stderr_bytes) == (0, b''), (
543
-                'program exited with failure'
669
+        with tests.isolated_config(
670
+            monkeypatch=monkeypatch,
671
+            runner=runner,
672
+            config={'global': {'phrase': 'abc'}, 'services': {}},
673
+        ):
674
+            monkeypatch.setattr(click, 'edit', lambda *a, **kw: edit_result)  # noqa: ARG005
675
+            result = runner.invoke(
676
+                cli.derivepassphrase, ['--notes', 'sv'], catch_exceptions=False
544 677
             )
678
+            assert (result.exit_code, result.stderr_bytes) == (
679
+                0,
680
+                b'',
681
+            ), 'program exited with failure'
545 682
             with open(cli._config_filename(), encoding='UTF-8') as infile:
546 683
                 config = json.load(infile)
547
-            assert config == {'global': {'phrase': 'abc'},
548
-                              'services': {'sv': {'notes':
549
-                                                      'contents go here'}}}
684
+            assert config == {
685
+                'global': {'phrase': 'abc'},
686
+                'services': {'sv': {'notes': 'contents go here'}},
687
+            }
550 688
 
551 689
     def test_221_edit_notes_noop(self, monkeypatch: Any) -> None:
552 690
         runner = click.testing.CliRunner(mix_stderr=False)
553
-        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
554
-                                   config={'global': {'phrase': 'abc'},
555
-                                           'services': {}}):
556
-            monkeypatch.setattr(click, 'edit',
557
-                                lambda *a, **kw: None)  # noqa: ARG005
558
-            result = runner.invoke(cli.derivepassphrase, ['--notes', 'sv'],
559
-                                   catch_exceptions=False)
560
-            assert (result.exit_code, result.stderr_bytes) == (0, b''), (
561
-                'program exited with failure'
691
+        with tests.isolated_config(
692
+            monkeypatch=monkeypatch,
693
+            runner=runner,
694
+            config={'global': {'phrase': 'abc'}, 'services': {}},
695
+        ):
696
+            monkeypatch.setattr(click, 'edit', lambda *a, **kw: None)  # noqa: ARG005
697
+            result = runner.invoke(
698
+                cli.derivepassphrase, ['--notes', 'sv'], catch_exceptions=False
562 699
             )
700
+            assert (result.exit_code, result.stderr_bytes) == (
701
+                0,
702
+                b'',
703
+            ), 'program exited with failure'
563 704
             with open(cli._config_filename(), encoding='UTF-8') as infile:
564 705
                 config = json.load(infile)
565 706
             assert config == {'global': {'phrase': 'abc'}, 'services': {}}
566 707
 
567 708
     def test_222_edit_notes_marker_removed(self, monkeypatch: Any) -> None:
568 709
         runner = click.testing.CliRunner(mix_stderr=False)
569
-        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
570
-                                   config={'global': {'phrase': 'abc'},
571
-                                           'services': {}}):
572
-            monkeypatch.setattr(click, 'edit',
573
-                                lambda *a, **kw: 'long\ntext')  # noqa: ARG005
574
-            result = runner.invoke(cli.derivepassphrase, ['--notes', 'sv'],
575
-                                   catch_exceptions=False)
576
-            assert (result.exit_code, result.stderr_bytes) == (0, b''), (
577
-                'program exited with failure'
710
+        with tests.isolated_config(
711
+            monkeypatch=monkeypatch,
712
+            runner=runner,
713
+            config={'global': {'phrase': 'abc'}, 'services': {}},
714
+        ):
715
+            monkeypatch.setattr(click, 'edit', lambda *a, **kw: 'long\ntext')  # noqa: ARG005
716
+            result = runner.invoke(
717
+                cli.derivepassphrase, ['--notes', 'sv'], catch_exceptions=False
578 718
             )
719
+            assert (result.exit_code, result.stderr_bytes) == (
720
+                0,
721
+                b'',
722
+            ), 'program exited with failure'
579 723
             with open(cli._config_filename(), encoding='UTF-8') as infile:
580 724
                 config = json.load(infile)
581
-            assert config == {'global': {'phrase': 'abc'},
582
-                              'services': {'sv': {'notes': 'long\ntext'}}}
725
+            assert config == {
726
+                'global': {'phrase': 'abc'},
727
+                'services': {'sv': {'notes': 'long\ntext'}},
728
+            }
583 729
 
584 730
     def test_223_edit_notes_abort(self, monkeypatch: Any) -> None:
585 731
         runner = click.testing.CliRunner(mix_stderr=False)
586
-        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
587
-                                   config={'global': {'phrase': 'abc'},
588
-                                           'services': {}}):
589
-            monkeypatch.setattr(click, 'edit',
590
-                                lambda *a, **kw: '\n\n')  # noqa: ARG005
591
-            result = runner.invoke(cli.derivepassphrase, ['--notes', 'sv'],
592
-                                   catch_exceptions=False)
732
+        with tests.isolated_config(
733
+            monkeypatch=monkeypatch,
734
+            runner=runner,
735
+            config={'global': {'phrase': 'abc'}, 'services': {}},
736
+        ):
737
+            monkeypatch.setattr(click, 'edit', lambda *a, **kw: '\n\n')  # noqa: ARG005
738
+            result = runner.invoke(
739
+                cli.derivepassphrase, ['--notes', 'sv'], catch_exceptions=False
740
+            )
593 741
             assert result.exit_code != 0, 'program unexpectedly succeeded'
594 742
             assert result.stderr_bytes is not None
595
-            assert b'user aborted request' in result.stderr_bytes, (
596
-                'expected error message missing'
597
-            )
743
+            assert (
744
+                b'user aborted request' in result.stderr_bytes
745
+            ), 'expected error message missing'
598 746
             with open(cli._config_filename(), encoding='UTF-8') as infile:
599 747
                 config = json.load(infile)
600 748
             assert config == {'global': {'phrase': 'abc'}, 'services': {}}
601 749
 
602
-    @pytest.mark.parametrize(['command_line', 'input', 'result_config'], [
750
+    @pytest.mark.parametrize(
751
+        ['command_line', 'input', 'result_config'],
752
+        [
603 753
             (
604 754
                 ['--phrase'],
605 755
                 b'my passphrase\n',
... ...
@@ -613,44 +763,66 @@ contents go here
613 763
             (
614 764
                 ['--phrase', 'sv'],
615 765
                 b'my passphrase\n',
616
-            {'global': {'phrase': 'abc'},
617
-             'services': {'sv': {'phrase': 'my passphrase'}}},
766
+                {
767
+                    'global': {'phrase': 'abc'},
768
+                    'services': {'sv': {'phrase': 'my passphrase'}},
769
+                },
618 770
             ),
619 771
             (
620 772
                 ['--key', 'sv'],
621 773
                 b'1\n',
622
-            {'global': {'phrase': 'abc'},
623
-             'services': {'sv': {'key': DUMMY_KEY1_B64}}},
774
+                {
775
+                    'global': {'phrase': 'abc'},
776
+                    'services': {'sv': {'key': DUMMY_KEY1_B64}},
777
+                },
624 778
             ),
625 779
             (
626 780
                 ['--key', '--length', '15', 'sv'],
627 781
                 b'1\n',
628
-            {'global': {'phrase': 'abc'},
629
-             'services': {'sv': {'key': DUMMY_KEY1_B64, 'length': 15}}},
782
+                {
783
+                    'global': {'phrase': 'abc'},
784
+                    'services': {'sv': {'key': DUMMY_KEY1_B64, 'length': 15}},
785
+                },
630 786
             ),
631
-    ])
787
+        ],
788
+    )
632 789
     def test_224_store_config_good(
633
-        self, monkeypatch: Any, command_line: list[str], input: bytes,
790
+        self,
791
+        monkeypatch: Any,
792
+        command_line: list[str],
793
+        input: bytes,
634 794
         result_config: Any,
635 795
     ) -> None:
636 796
         runner = click.testing.CliRunner(mix_stderr=False)
637
-        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
638
-                                   config={'global': {'phrase': 'abc'},
639
-                                           'services': {}}):
640
-            monkeypatch.setattr(cli, '_get_suitable_ssh_keys',
641
-                                tests.suitable_ssh_keys)
642
-            result = runner.invoke(cli.derivepassphrase,
797
+        with tests.isolated_config(
798
+            monkeypatch=monkeypatch,
799
+            runner=runner,
800
+            config={'global': {'phrase': 'abc'}, 'services': {}},
801
+        ):
802
+            monkeypatch.setattr(
803
+                cli, '_get_suitable_ssh_keys', tests.suitable_ssh_keys
804
+            )
805
+            result = runner.invoke(
806
+                cli.derivepassphrase,
643 807
                 ['--config', *command_line],
644
-                                   catch_exceptions=False, input=input)
808
+                catch_exceptions=False,
809
+                input=input,
810
+            )
645 811
             assert result.exit_code == 0, 'program exited with failure'
646 812
             with open(cli._config_filename(), encoding='UTF-8') as infile:
647 813
                 config = json.load(infile)
648
-            assert config == result_config, (
649
-                'stored config does not match expectation'
650
-            )
814
+            assert (
815
+                config == result_config
816
+            ), 'stored config does not match expectation'
651 817
 
652
-    @pytest.mark.parametrize(['command_line', 'input', 'err_text'], [
653
-        ([], b'', b'cannot update global settings without actual settings'),
818
+    @pytest.mark.parametrize(
819
+        ['command_line', 'input', 'err_text'],
820
+        [
821
+            (
822
+                [],
823
+                b'',
824
+                b'cannot update global settings without actual settings',
825
+            ),
654 826
             (
655 827
                 ['sv'],
656 828
                 b'',
... ...
@@ -658,65 +830,84 @@ contents go here
658 830
             ),
659 831
             (['--phrase', 'sv'], b'', b'no passphrase given'),
660 832
             (['--key'], b'', b'no valid SSH key selected'),
661
-    ])
833
+        ],
834
+    )
662 835
     def test_225_store_config_fail(
663
-        self, monkeypatch: Any, command_line: list[str],
664
-        input: bytes, err_text: bytes,
836
+        self,
837
+        monkeypatch: Any,
838
+        command_line: list[str],
839
+        input: bytes,
840
+        err_text: bytes,
665 841
     ) -> None:
666 842
         runner = click.testing.CliRunner(mix_stderr=False)
667
-        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
668
-                                   config={'global': {'phrase': 'abc'},
669
-                                           'services': {}}):
670
-            monkeypatch.setattr(cli, '_get_suitable_ssh_keys',
671
-                                tests.suitable_ssh_keys)
672
-            result = runner.invoke(cli.derivepassphrase,
843
+        with tests.isolated_config(
844
+            monkeypatch=monkeypatch,
845
+            runner=runner,
846
+            config={'global': {'phrase': 'abc'}, 'services': {}},
847
+        ):
848
+            monkeypatch.setattr(
849
+                cli, '_get_suitable_ssh_keys', tests.suitable_ssh_keys
850
+            )
851
+            result = runner.invoke(
852
+                cli.derivepassphrase,
673 853
                 ['--config', *command_line],
674
-                                   catch_exceptions=False, input=input)
854
+                catch_exceptions=False,
855
+                input=input,
856
+            )
675 857
             assert result.exit_code != 0, 'program unexpectedly succeeded?!'
676 858
             assert result.stderr_bytes is not None
677
-            assert err_text in result.stderr_bytes, (
678
-                'expected error message missing'
679
-            )
859
+            assert (
860
+                err_text in result.stderr_bytes
861
+            ), 'expected error message missing'
680 862
 
681 863
     def test_225a_store_config_fail_manual_no_ssh_key_selection(
682
-        self, monkeypatch: Any,
864
+        self,
865
+        monkeypatch: Any,
683 866
     ) -> None:
684 867
         runner = click.testing.CliRunner(mix_stderr=False)
685
-        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
686
-                                   config={'global': {'phrase': 'abc'},
687
-                                           'services': {}}):
868
+        with tests.isolated_config(
869
+            monkeypatch=monkeypatch,
870
+            runner=runner,
871
+            config={'global': {'phrase': 'abc'}, 'services': {}},
872
+        ):
688 873
             custom_error = 'custom error message'
874
+
689 875
             def raiser():
690 876
                 raise RuntimeError(custom_error)
877
+
691 878
             monkeypatch.setattr(cli, '_select_ssh_key', raiser)
692
-            result = runner.invoke(cli.derivepassphrase,
879
+            result = runner.invoke(
880
+                cli.derivepassphrase,
693 881
                 ['--key', '--config'],
694
-                                   catch_exceptions=False)
882
+                catch_exceptions=False,
883
+            )
695 884
             assert result.exit_code != 0, 'program unexpectedly succeeded'
696 885
             assert result.stderr_bytes is not None
697
-            assert custom_error.encode() in result.stderr_bytes, (
698
-                'expected error message missing'
699
-            )
886
+            assert (
887
+                custom_error.encode() in result.stderr_bytes
888
+            ), 'expected error message missing'
700 889
 
701 890
     def test_226_no_arguments(self) -> None:
702 891
         runner = click.testing.CliRunner(mix_stderr=False)
703
-        result = runner.invoke(cli.derivepassphrase, [],
704
-                               catch_exceptions=False)
892
+        result = runner.invoke(
893
+            cli.derivepassphrase, [], catch_exceptions=False
894
+        )
705 895
         assert result.exit_code != 0, 'program unexpectedly succeeded'
706 896
         assert result.stderr_bytes is not None
707
-        assert b'SERVICE is required' in result.stderr_bytes, (
708
-            'expected error message missing'
709
-        )
897
+        assert (
898
+            b'SERVICE is required' in result.stderr_bytes
899
+        ), 'expected error message missing'
710 900
 
711 901
     def test_226a_no_passphrase_or_key(self) -> None:
712 902
         runner = click.testing.CliRunner(mix_stderr=False)
713
-        result = runner.invoke(cli.derivepassphrase, [DUMMY_SERVICE],
714
-                               catch_exceptions=False)
903
+        result = runner.invoke(
904
+            cli.derivepassphrase, [DUMMY_SERVICE], catch_exceptions=False
905
+        )
715 906
         assert result.exit_code != 0, 'program unexpectedly succeeded'
716 907
         assert result.stderr_bytes is not None
717
-        assert b'no passphrase or key given' in result.stderr_bytes, (
718
-            'expected error message missing'
719
-        )
908
+        assert (
909
+            b'no passphrase or key given' in result.stderr_bytes
910
+        ), 'expected error message missing'
720 911
 
721 912
 
722 913
 class TestCLIUtils:
... ...
@@ -724,9 +914,10 @@ class TestCLIUtils:
724 914
     def test_100_save_bad_config(self, monkeypatch: Any) -> None:
725 915
         runner = click.testing.CliRunner()
726 916
         with (
727
-            tests.isolated_config(monkeypatch=monkeypatch, runner=runner,
728
-                                  config={}),
729
-            pytest.raises(ValueError, match='Invalid vault config')
917
+            tests.isolated_config(
918
+                monkeypatch=monkeypatch, runner=runner, config={}
919
+            ),
920
+            pytest.raises(ValueError, match='Invalid vault config'),
730 921
         ):
731 922
             cli._save_config(None)  # type: ignore
732 923
 
... ...
@@ -746,11 +936,15 @@ class TestCLIUtils:
746 936
                 'Spam, bacon, sausage and spam',
747 937
                 'Spam, egg, spam, spam, bacon and spam',
748 938
                 'Spam, spam, spam, egg and spam',
749
-                ('Spam, spam, spam, spam, spam, spam, baked beans, '
750
-                 'spam, spam, spam and spam'),
751
-                ('Lobster thermidor aux crevettes with a mornay sauce '
939
+                (
940
+                    'Spam, spam, spam, spam, spam, spam, baked beans, '
941
+                    'spam, spam, spam and spam'
942
+                ),
943
+                (
944
+                    'Lobster thermidor aux crevettes with a mornay sauce '
752 945
                     'garnished with truffle paté, brandy '
753
-                 'and a fried egg on top and spam'),
946
+                    'and a fried egg on top and spam'
947
+                ),
754 948
             ]
755 949
             index = cli._prompt_for_selection(items, heading=heading)
756 950
             click.echo('A fine choice: ', nl=False)
... ...
@@ -759,7 +954,9 @@ class TestCLIUtils:
759 954
         runner = click.testing.CliRunner(mix_stderr=True)
760 955
         result = runner.invoke(driver, [], input='9')
761 956
         assert result.exit_code == 0, 'driver program failed'
762
-        assert result.stdout == '''\
957
+        assert (
958
+            result.stdout
959
+            == """\
763 960
 Our menu:
764 961
 [1] Egg and bacon
765 962
 [2] Egg, sausage and bacon
... ...
@@ -774,11 +971,15 @@ Our menu:
774 971
 Your selection? (1-10, leave empty to abort): 9
775 972
 A fine choice: Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam
776 973
 (Note: Vikings strictly optional.)
777
-''', 'driver program produced unexpected output'  # noqa: E501
778
-        result = runner.invoke(driver, ['--heading='], input='',
779
-                               catch_exceptions=True)
974
+"""  # noqa: E501
975
+        ), 'driver program produced unexpected output'
976
+        result = runner.invoke(
977
+            driver, ['--heading='], input='', catch_exceptions=True
978
+        )
780 979
         assert result.exit_code > 0, 'driver program succeeded?!'
781
-        assert result.stdout == '''\
980
+        assert (
981
+            result.stdout
982
+            == """\
782 983
 [1] Egg and bacon
783 984
 [2] Egg, sausage and bacon
784 985
 [3] Egg and spam
... ...
@@ -789,13 +990,12 @@ A fine choice: Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam
789 990
 [8] Spam, spam, spam, egg and spam
790 991
 [9] Spam, spam, spam, spam, spam, spam, baked beans, spam, spam, spam and spam
791 992
 [10] Lobster thermidor aux crevettes with a mornay sauce garnished with truffle paté, brandy and a fried egg on top and spam
792
-Your selection? (1-10, leave empty to abort): \n''', (  # noqa: E501
793
-            'driver program produced unexpected output'
794
-        )
795
-        assert isinstance(result.exception, IndexError), (
796
-            'driver program did not raise IndexError?!'
797
-        )
798
-
993
+Your selection? (1-10, leave empty to abort):\x20
994
+"""  # noqa: E501
995
+        ), 'driver program produced unexpected output'
996
+        assert isinstance(
997
+            result.exception, IndexError
998
+        ), 'driver program did not raise IndexError?!'
799 999
 
800 1000
     def test_102_prompt_for_selection_single(self) -> None:
801 1001
         @click.command()
... ...
@@ -803,42 +1003,52 @@ Your selection? (1-10, leave empty to abort): \n''', (  # noqa: E501
803 1003
         @click.argument('prompt')
804 1004
         def driver(item, prompt):
805 1005
             try:
806
-                cli._prompt_for_selection([item], heading='',
807
-                                          single_choice_prompt=prompt)
1006
+                cli._prompt_for_selection(
1007
+                    [item], heading='', single_choice_prompt=prompt
1008
+                )
808 1009
             except IndexError:
809 1010
                 click.echo('Boo.')
810 1011
                 raise
811 1012
             else:
812 1013
                 click.echo('Great!')
1014
+
813 1015
         runner = click.testing.CliRunner(mix_stderr=True)
814
-        result = runner.invoke(driver,
815
-                               ['Will replace with spam. Confirm, y/n?'],
816
-                               input='y')
1016
+        result = runner.invoke(
1017
+            driver, ['Will replace with spam. Confirm, y/n?'], input='y'
1018
+        )
817 1019
         assert result.exit_code == 0, 'driver program failed'
818
-        assert result.stdout == '''\
1020
+        assert (
1021
+            result.stdout
1022
+            == """\
819 1023
 [1] baked beans
820 1024
 Will replace with spam. Confirm, y/n? y
821 1025
 Great!
822
-''', 'driver program produced unexpected output'
823
-        result = runner.invoke(driver,
824
-                               ['Will replace with spam, okay? '
825
-                                '(Please say "y" or "n".)'],
826
-                               input='')
1026
+"""
1027
+        ), 'driver program produced unexpected output'
1028
+        result = runner.invoke(
1029
+            driver,
1030
+            ['Will replace with spam, okay? ' '(Please say "y" or "n".)'],
1031
+            input='',
1032
+        )
827 1033
         assert result.exit_code > 0, 'driver program succeeded?!'
828
-        assert result.stdout == '''\
1034
+        assert (
1035
+            result.stdout
1036
+            == """\
829 1037
 [1] baked beans
830 1038
 Will replace with spam, okay? (Please say "y" or "n".):\x20
831 1039
 Boo.
832
-''', 'driver program produced unexpected output'
833
-        assert isinstance(result.exception, IndexError), (
834
-            'driver program did not raise IndexError?!'
835
-        )
836
-
1040
+"""
1041
+        ), 'driver program produced unexpected output'
1042
+        assert isinstance(
1043
+            result.exception, IndexError
1044
+        ), 'driver program did not raise IndexError?!'
837 1045
 
838 1046
     def test_103_prompt_for_passphrase(self, monkeypatch: Any) -> None:
839
-        monkeypatch.setattr(click, 'prompt',
840
-                            lambda *a, **kw:
841
-                                json.dumps({'args': a, 'kwargs': kw}))
1047
+        monkeypatch.setattr(
1048
+            click,
1049
+            'prompt',
1050
+            lambda *a, **kw: json.dumps({'args': a, 'kwargs': kw}),
1051
+        )
842 1052
         res = json.loads(cli._prompt_for_passphrase())
843 1053
         err_msg = 'missing arguments to passphrase prompt'
844 1054
         assert 'args' in res, err_msg
... ...
@@ -849,48 +1059,70 @@ Boo.
849 1059
         assert res['kwargs'].get('err'), err_msg
850 1060
         assert res['kwargs'].get('hide_input'), err_msg
851 1061
 
852
-
853
-    @pytest.mark.parametrize(['command_line', 'config', 'result_config'], [
854
-        (['--delete-globals'],
855
-         {'global': {'phrase': 'abc'}, 'services': {}}, {'services': {}}),
856
-        (['--delete', DUMMY_SERVICE],
857
-         {'global': {'phrase': 'abc'},
858
-          'services': {DUMMY_SERVICE: {'notes': '...'}}},
859
-         {'global': {'phrase': 'abc'}, 'services': {}}),
860
-        (['--clear'],
861
-         {'global': {'phrase': 'abc'},
862
-          'services': {DUMMY_SERVICE: {'notes': '...'}}},
863
-         {'services': {}}),
864
-    ])
1062
+    @pytest.mark.parametrize(
1063
+        ['command_line', 'config', 'result_config'],
1064
+        [
1065
+            (
1066
+                ['--delete-globals'],
1067
+                {'global': {'phrase': 'abc'}, 'services': {}},
1068
+                {'services': {}},
1069
+            ),
1070
+            (
1071
+                ['--delete', DUMMY_SERVICE],
1072
+                {
1073
+                    'global': {'phrase': 'abc'},
1074
+                    'services': {DUMMY_SERVICE: {'notes': '...'}},
1075
+                },
1076
+                {'global': {'phrase': 'abc'}, 'services': {}},
1077
+            ),
1078
+            (
1079
+                ['--clear'],
1080
+                {
1081
+                    'global': {'phrase': 'abc'},
1082
+                    'services': {DUMMY_SERVICE: {'notes': '...'}},
1083
+                },
1084
+                {'services': {}},
1085
+            ),
1086
+        ],
1087
+    )
865 1088
     def test_203_repeated_config_deletion(
866
-        self, monkeypatch: Any, command_line: list[str],
867
-        config: dpp.types.VaultConfig, result_config: dpp.types.VaultConfig,
1089
+        self,
1090
+        monkeypatch: Any,
1091
+        command_line: list[str],
1092
+        config: dpp.types.VaultConfig,
1093
+        result_config: dpp.types.VaultConfig,
868 1094
     ) -> None:
869 1095
         runner = click.testing.CliRunner(mix_stderr=False)
870 1096
         for start_config in [config, result_config]:
871
-            with tests.isolated_config(monkeypatch=monkeypatch,
872
-                                       runner=runner, config=start_config):
873
-                result = runner.invoke(cli.derivepassphrase, command_line,
874
-                                       catch_exceptions=False)
875
-                assert (result.exit_code, result.stderr_bytes) == (0, b''), (
876
-                    'program exited with failure'
1097
+            with tests.isolated_config(
1098
+                monkeypatch=monkeypatch, runner=runner, config=start_config
1099
+            ):
1100
+                result = runner.invoke(
1101
+                    cli.derivepassphrase, command_line, catch_exceptions=False
877 1102
                 )
1103
+                assert (result.exit_code, result.stderr_bytes) == (
1104
+                    0,
1105
+                    b'',
1106
+                ), 'program exited with failure'
878 1107
                 with open(cli._config_filename(), encoding='UTF-8') as infile:
879 1108
                     config_readback = json.load(infile)
880 1109
                 assert config_readback == result_config
881 1110
 
882
-
883 1111
     def test_204_phrase_from_key_manually(self) -> None:
884 1112
         assert (
885
-            dpp.Vault(phrase=DUMMY_PHRASE_FROM_KEY1, **DUMMY_CONFIG_SETTINGS)
886
-            .generate(DUMMY_SERVICE) == DUMMY_RESULT_KEY1
1113
+            dpp.Vault(
1114
+                phrase=DUMMY_PHRASE_FROM_KEY1, **DUMMY_CONFIG_SETTINGS
1115
+            ).generate(DUMMY_SERVICE)
1116
+            == DUMMY_RESULT_KEY1
887 1117
         )
888 1118
 
889
-
890
-    @pytest.mark.parametrize(['vfunc', 'input'], [
1119
+    @pytest.mark.parametrize(
1120
+        ['vfunc', 'input'],
1121
+        [
891 1122
             (cli._validate_occurrence_constraint, 20),
892 1123
             (cli._validate_length, 20),
893
-    ])
1124
+        ],
1125
+    )
894 1126
     def test_210a_validate_constraints_manually(
895 1127
         self,
896 1128
         vfunc: Callable[[click.Context, click.Parameter, Any], int | None],
... ...
@@ -904,10 +1135,13 @@ Boo.
904 1135
     @tests.skip_if_no_agent
905 1136
     @pytest.mark.parametrize('conn_hint', ['none', 'socket', 'client'])
906 1137
     def test_227_get_suitable_ssh_keys(
907
-        self, monkeypatch: Any, conn_hint: str,
1138
+        self,
1139
+        monkeypatch: Any,
1140
+        conn_hint: str,
908 1141
     ) -> None:
909
-        monkeypatch.setattr(ssh_agent_client.SSHAgentClient,
910
-                            'list_keys', tests.list_keys)
1142
+        monkeypatch.setattr(
1143
+            ssh_agent_client.SSHAgentClient, 'list_keys', tests.list_keys
1144
+        )
911 1145
         hint: ssh_agent_client.SSHAgentClient | socket.socket | None
912 1146
         match conn_hint:
913 1147
             case 'client':
... ...
@@ -10,47 +10,88 @@ from typing_extensions import Any
10 10
 import derivepassphrase.types
11 11
 
12 12
 
13
-@pytest.mark.parametrize(['obj', 'comment'], [
13
+@pytest.mark.parametrize(
14
+    ['obj', 'comment'],
15
+    [
14 16
         (None, 'not a dict'),
15 17
         ({}, 'missing required keys'),
16 18
         ({'global': None, 'services': {}}, 'bad config value: global'),
17
-    ({'global': {'key': 123}, 'services': {}},
18
-     'bad config value: global.key'),
19
-    ({'global': {'phrase': 'abc', 'key': '...'}, 'services': {}},
20
-     'incompatible config values: global.key and global.phrase'),
19
+        (
20
+            {'global': {'key': 123}, 'services': {}},
21
+            'bad config value: global.key',
22
+        ),
23
+        (
24
+            {'global': {'phrase': 'abc', 'key': '...'}, 'services': {}},
25
+            'incompatible config values: global.key and global.phrase',
26
+        ),
21 27
         ({'services': None}, 'bad config value: services'),
22 28
         ({'services': {2: {}}}, 'bad config value: services."2"'),
23 29
         ({'services': {'2': 2}}, 'bad config value: services."2"'),
24
-    ({'services': {'sv': {'notes': False}}},
25
-     'bad config value: services.sv.notes'),
30
+        (
31
+            {'services': {'sv': {'notes': False}}},
32
+            'bad config value: services.sv.notes',
33
+        ),
26 34
         ({'services': {'sv': {'notes': 'blah blah blah'}}}, ''),
27
-    ({'services': {'sv': {'length': '200'}}},
28
-     'bad config value: services.sv.length'),
29
-    ({'services': {'sv': {'length': 0.5}}},
30
-     'bad config value: services.sv.length'),
31
-    ({'services': {'sv': {'length': -10}}},
32
-     'bad config value: services.sv.length'),
33
-    ({'services': {'sv': {'upper': -10}}},
34
-     'bad config value: services.sv.upper'),
35
-    ({'global': {'phrase': 'my secret phrase'},
36
-      'services': {'sv': {'length': 10}}},
37
-     ''),
35
+        (
36
+            {'services': {'sv': {'length': '200'}}},
37
+            'bad config value: services.sv.length',
38
+        ),
39
+        (
40
+            {'services': {'sv': {'length': 0.5}}},
41
+            'bad config value: services.sv.length',
42
+        ),
43
+        (
44
+            {'services': {'sv': {'length': -10}}},
45
+            'bad config value: services.sv.length',
46
+        ),
47
+        (
48
+            {'services': {'sv': {'upper': -10}}},
49
+            'bad config value: services.sv.upper',
50
+        ),
51
+        (
52
+            {
53
+                'global': {'phrase': 'my secret phrase'},
54
+                'services': {'sv': {'length': 10}},
55
+            },
56
+            '',
57
+        ),
38 58
         ({'services': {'sv': {'length': 10, 'phrase': '...'}}}, ''),
39 59
         ({'services': {'sv': {'length': 10, 'key': '...'}}}, ''),
40 60
         ({'services': {'sv': {'upper': 10, 'key': '...'}}}, ''),
41
-    ({'services': {'sv': {'phrase': 'abc', 'key': '...'}}},
42
-     'incompatible config values: services.sv.key and services.sv.phrase'),
43
-    ({'global': {'phrase': 'abc'},
44
-      'services': {'sv': {'phrase': 'abc', 'length': 10}}}, ''),
45
-    ({'global': {'key': '...'},
46
-      'services': {'sv': {'phrase': 'abc', 'length': 10}}}, ''),
47
-    ({'global': {'key': '...'},
48
-      'services': {'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1},
49
-                   'sv2': {'length': 10, 'repeat': 1, 'lower': 1}}}, ''),
50
-])
61
+        (
62
+            {'services': {'sv': {'phrase': 'abc', 'key': '...'}}},
63
+            'incompatible config values: services.sv.key and services.sv.phrase',  # noqa: E501
64
+        ),
65
+        (
66
+            {
67
+                'global': {'phrase': 'abc'},
68
+                'services': {'sv': {'phrase': 'abc', 'length': 10}},
69
+            },
70
+            '',
71
+        ),
72
+        (
73
+            {
74
+                'global': {'key': '...'},
75
+                'services': {'sv': {'phrase': 'abc', 'length': 10}},
76
+            },
77
+            '',
78
+        ),
79
+        (
80
+            {
81
+                'global': {'key': '...'},
82
+                'services': {
83
+                    'sv1': {'phrase': 'abc', 'length': 10, 'upper': 1},
84
+                    'sv2': {'length': 10, 'repeat': 1, 'lower': 1},
85
+                },
86
+            },
87
+            '',
88
+        ),
89
+    ],
90
+)
51 91
 def test_200_is_vault_config(obj: Any, comment: str) -> None:
52 92
     is_vault_config = derivepassphrase.types.is_vault_config
53 93
     assert is_vault_config(obj) == (not comment), (
54
-        'failed to complain about: ' + comment if comment
94
+        'failed to complain about: ' + comment
95
+        if comment
55 96
         else 'failed on valid example'
56 97
     )
... ...
@@ -12,53 +12,107 @@ import sequin
12 12
 
13 13
 
14 14
 class TestStaticFunctionality:
15
-
16
-    @pytest.mark.parametrize(['sequence', 'base', 'expected'], [
15
+    @pytest.mark.parametrize(
16
+        ['sequence', 'base', 'expected'],
17
+        [
17 18
             ([1, 2, 3, 4, 5, 6], 10, 123456),
18 19
             ([1, 2, 3, 4, 5, 6], 100, 10203040506),
19 20
             ([0, 0, 1, 4, 9, 7], 10, 1497),
20 21
             ([1, 0, 0, 1, 0, 0, 0, 0], 2, 144),
21 22
             ([1, 7, 5, 5], 8, 0o1755),
22
-    ])
23
+        ],
24
+    )
23 25
     def test_200_big_endian_number(self, sequence, base, expected):
24 26
         assert (
25 27
             sequin.Sequin._big_endian_number(sequence, base=base)
26 28
         ) == expected
27 29
 
28 30
     @pytest.mark.parametrize(
29
-        ['exc_type', 'exc_pattern', 'sequence', 'base'], [
31
+        ['exc_type', 'exc_pattern', 'sequence', 'base'],
32
+        [
30 33
             (ValueError, 'invalid base 3 digit:', [-1], 3),
31 34
             (ValueError, 'invalid base:', [0], 1),
32 35
             (TypeError, 'not an integer:', [0.0, 1.0, 0.0, 1.0], 2),
33
-        ]
36
+        ],
34 37
     )
35
-    def test_300_big_endian_number_exceptions(self, exc_type, exc_pattern,
36
-                                              sequence, base):
38
+    def test_300_big_endian_number_exceptions(
39
+        self, exc_type, exc_pattern, sequence, base
40
+    ):
37 41
         with pytest.raises(exc_type, match=exc_pattern):
38 42
             sequin.Sequin._big_endian_number(sequence, base=base)
39 43
 
40
-class TestSequin:
41 44
 
42
-    @pytest.mark.parametrize(['sequence', 'is_bitstring', 'expected'], [
43
-        ([1, 0, 0, 1, 0, 1], False, [0, 0, 0, 0, 0, 0, 0, 1,
44
-                                     0, 0, 0, 0, 0, 0, 0, 0,
45
-                                     0, 0, 0, 0, 0, 0, 0, 0,
46
-                                     0, 0, 0, 0, 0, 0, 0, 1,
47
-                                     0, 0, 0, 0, 0, 0, 0, 0,
48
-                                     0, 0, 0, 0, 0, 0, 0, 1]),
45
+class TestSequin:
46
+    @pytest.mark.parametrize(
47
+        ['sequence', 'is_bitstring', 'expected'],
48
+        [
49
+            (
50
+                [1, 0, 0, 1, 0, 1],
51
+                False,
52
+                [
53
+                    0,
54
+                    0,
55
+                    0,
56
+                    0,
57
+                    0,
58
+                    0,
59
+                    0,
60
+                    1,
61
+                    0,
62
+                    0,
63
+                    0,
64
+                    0,
65
+                    0,
66
+                    0,
67
+                    0,
68
+                    0,
69
+                    0,
70
+                    0,
71
+                    0,
72
+                    0,
73
+                    0,
74
+                    0,
75
+                    0,
76
+                    0,
77
+                    0,
78
+                    0,
79
+                    0,
80
+                    0,
81
+                    0,
82
+                    0,
83
+                    0,
84
+                    1,
85
+                    0,
86
+                    0,
87
+                    0,
88
+                    0,
89
+                    0,
90
+                    0,
91
+                    0,
92
+                    0,
93
+                    0,
94
+                    0,
95
+                    0,
96
+                    0,
97
+                    0,
98
+                    0,
99
+                    0,
100
+                    1,
101
+                ],
102
+            ),
49 103
             ([1, 0, 0, 1, 0, 1], True, [1, 0, 0, 1, 0, 1]),
50
-        (b'OK', False, [0, 1, 0, 0, 1, 1, 1, 1,
51
-                        0, 1, 0, 0, 1, 0, 1, 1]),
52
-        ('OK', False, [0, 1, 0, 0, 1, 1, 1, 1,
53
-                       0, 1, 0, 0, 1, 0, 1, 1]),
54
-    ])
104
+            (b'OK', False, [0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1]),
105
+            ('OK', False, [0, 1, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 1, 0, 1, 1]),
106
+        ],
107
+    )
55 108
     def test_200_constructor(self, sequence, is_bitstring, expected):
56 109
         seq = sequin.Sequin(sequence, is_bitstring=is_bitstring)
57 110
         assert seq.bases == {2: collections.deque(expected)}
58 111
 
59 112
     def test_201_generating(self):
60
-        seq = sequin.Sequin([1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1],
61
-                            is_bitstring=True)
113
+        seq = sequin.Sequin(
114
+            [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], is_bitstring=True
115
+        )
62 116
         assert seq.generate(1) == 0
63 117
         assert seq.generate(5) == 3
64 118
         assert seq.generate(5) == 3
... ...
@@ -67,21 +121,24 @@ class TestSequin:
67 121
             seq.generate(5)
68 122
         with pytest.raises(sequin.SequinExhaustedError):
69 123
             seq.generate(1)
70
-        seq = sequin.Sequin([1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1],
71
-                            is_bitstring=True)
124
+        seq = sequin.Sequin(
125
+            [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], is_bitstring=True
126
+        )
72 127
         with pytest.raises(ValueError, match='invalid target range'):
73 128
             seq.generate(0)
74 129
 
75 130
     def test_210_internal_generating(self):
76
-        seq = sequin.Sequin([1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1],
77
-                            is_bitstring=True)
131
+        seq = sequin.Sequin(
132
+            [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], is_bitstring=True
133
+        )
78 134
         assert seq._generate_inner(5) == 3
79 135
         assert seq._generate_inner(5) == 3
80 136
         assert seq._generate_inner(5) == 1
81 137
         assert seq._generate_inner(5) == 5
82 138
         assert seq._generate_inner(1) == 0
83
-        seq = sequin.Sequin([1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1],
84
-                            is_bitstring=True)
139
+        seq = sequin.Sequin(
140
+            [1, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1], is_bitstring=True
141
+        )
85 142
         assert seq._generate_inner(1) == 0
86 143
         with pytest.raises(ValueError, match='invalid target range'):
87 144
             seq._generate_inner(0)
... ...
@@ -90,8 +147,9 @@ class TestSequin:
90 147
 
91 148
     def test_211_shifting(self):
92 149
         seq = sequin.Sequin([1, 0, 1, 0, 0, 1, 0, 0, 0, 1], is_bitstring=True)
93
-        assert seq.bases == {2: collections.deque([
94
-            1, 0, 1, 0, 0, 1, 0, 0, 0, 1])}
150
+        assert seq.bases == {
151
+            2: collections.deque([1, 0, 1, 0, 0, 1, 0, 0, 0, 1])
152
+        }
95 153
 
96 154
         assert seq._all_or_nothing_shift(3) == (1, 0, 1)
97 155
         assert seq._all_or_nothing_shift(3) == (0, 0, 1)
... ...
@@ -106,13 +164,17 @@ class TestSequin:
106 164
     @pytest.mark.parametrize(
107 165
         ['sequence', 'is_bitstring', 'exc_type', 'exc_pattern'],
108 166
         [
109
-            ([0, 1, 2, 3, 4, 5, 6, 7], True,
110
-             ValueError, 'sequence item out of range'),
111
-            ('こんにちは。', False,
112
-             ValueError, 'sequence item out of range'),
113
-        ]
167
+            (
168
+                [0, 1, 2, 3, 4, 5, 6, 7],
169
+                True,
170
+                ValueError,
171
+                'sequence item out of range',
172
+            ),
173
+            ('こんにちは。', False, ValueError, 'sequence item out of range'),
174
+        ],
114 175
     )
115
-    def test_300_constructor_exceptions(self, sequence, is_bitstring,
116
-                                        exc_type, exc_pattern):
176
+    def test_300_constructor_exceptions(
177
+        self, sequence, is_bitstring, exc_type, exc_pattern
178
+    ):
117 179
         with pytest.raises(exc_type, match=exc_pattern):
118 180
             sequin.Sequin(sequence, is_bitstring=is_bitstring)
... ...
@@ -24,10 +24,13 @@ import tests
24 24
 
25 25
 
26 26
 class TestStaticFunctionality:
27
-
28
-    @pytest.mark.parametrize(['public_key', 'public_key_data'],
29
-                             [(val['public_key'], val['public_key_data'])
30
-                              for val in tests.SUPPORTED_KEYS.values()])
27
+    @pytest.mark.parametrize(
28
+        ['public_key', 'public_key_data'],
29
+        [
30
+            (val['public_key'], val['public_key_data'])
31
+            for val in tests.SUPPORTED_KEYS.values()
32
+        ],
33
+    )
31 34
     def test_100_key_decoding(self, public_key, public_key_data):
32 35
         keydata = base64.b64decode(public_key.split(None, 2)[1])
33 36
         assert (
... ...
@@ -37,77 +40,101 @@ class TestStaticFunctionality:
37 40
     def test_200_constructor_no_running_agent(self, monkeypatch):
38 41
         monkeypatch.delenv('SSH_AUTH_SOCK', raising=False)
39 42
         sock = socket.socket(family=socket.AF_UNIX)
40
-        with pytest.raises(KeyError,
41
-                           match='SSH_AUTH_SOCK environment variable'):
43
+        with pytest.raises(
44
+            KeyError, match='SSH_AUTH_SOCK environment variable'
45
+        ):
42 46
             ssh_agent_client.SSHAgentClient(socket=sock)
43 47
 
44
-    @pytest.mark.parametrize(['input', 'expected'], [
48
+    @pytest.mark.parametrize(
49
+        ['input', 'expected'],
50
+        [
45 51
             (16777216, b'\x01\x00\x00\x00'),
46
-    ])
52
+        ],
53
+    )
47 54
     def test_210_uint32(self, input, expected):
48 55
         uint32 = ssh_agent_client.SSHAgentClient.uint32
49 56
         assert uint32(input) == expected
50 57
 
51
-    @pytest.mark.parametrize(['input', 'expected'], [
58
+    @pytest.mark.parametrize(
59
+        ['input', 'expected'],
60
+        [
52 61
             (b'ssh-rsa', b'\x00\x00\x00\x07ssh-rsa'),
53 62
             (b'ssh-ed25519', b'\x00\x00\x00\x0bssh-ed25519'),
54 63
             (
55 64
                 ssh_agent_client.SSHAgentClient.string(b'ssh-ed25519'),
56 65
                 b'\x00\x00\x00\x0f\x00\x00\x00\x0bssh-ed25519',
57 66
             ),
58
-    ])
67
+        ],
68
+    )
59 69
     def test_211_string(self, input, expected):
60 70
         string = ssh_agent_client.SSHAgentClient.string
61 71
         assert bytes(string(input)) == expected
62 72
 
63
-    @pytest.mark.parametrize(['input', 'expected'], [
73
+    @pytest.mark.parametrize(
74
+        ['input', 'expected'],
75
+        [
64 76
             (b'\x00\x00\x00\x07ssh-rsa', b'ssh-rsa'),
65 77
             (
66 78
                 ssh_agent_client.SSHAgentClient.string(b'ssh-ed25519'),
67 79
                 b'ssh-ed25519',
68 80
             ),
69
-    ])
81
+        ],
82
+    )
70 83
     def test_212_unstring(self, input, expected):
71 84
         unstring = ssh_agent_client.SSHAgentClient.unstring
72 85
         unstring_prefix = ssh_agent_client.SSHAgentClient.unstring_prefix
73 86
         assert bytes(unstring(input)) == expected
74
-        assert tuple(
75
-            bytes(x) for x in unstring_prefix(input)
76
-        ) == (expected, b'')
87
+        assert tuple(bytes(x) for x in unstring_prefix(input)) == (
88
+            expected,
89
+            b'',
90
+        )
77 91
 
78
-    @pytest.mark.parametrize(['value', 'exc_type', 'exc_pattern'], [
92
+    @pytest.mark.parametrize(
93
+        ['value', 'exc_type', 'exc_pattern'],
94
+        [
79 95
             (10000000000000000, OverflowError, 'int too big to convert'),
80 96
             (-1, OverflowError, "can't convert negative int to unsigned"),
81
-    ])
97
+        ],
98
+    )
82 99
     def test_310_uint32_exceptions(self, value, exc_type, exc_pattern):
83 100
         uint32 = ssh_agent_client.SSHAgentClient.uint32
84 101
         with pytest.raises(exc_type, match=exc_pattern):
85 102
             uint32(value)
86 103
 
87
-    @pytest.mark.parametrize(['input', 'exc_type', 'exc_pattern'], [
104
+    @pytest.mark.parametrize(
105
+        ['input', 'exc_type', 'exc_pattern'],
106
+        [
88 107
             ('some string', TypeError, 'invalid payload type'),
89
-    ])
108
+        ],
109
+    )
90 110
     def test_311_string_exceptions(self, input, exc_type, exc_pattern):
91 111
         string = ssh_agent_client.SSHAgentClient.string
92 112
         with pytest.raises(exc_type, match=exc_pattern):
93 113
             string(input)
94 114
 
95 115
     @pytest.mark.parametrize(
96
-        ['input', 'exc_type', 'exc_pattern', 'has_trailer', 'parts'], [
116
+        ['input', 'exc_type', 'exc_pattern', 'has_trailer', 'parts'],
117
+        [
97 118
             (b'ssh', ValueError, 'malformed SSH byte string', False, None),
98 119
             (
99 120
                 b'\x00\x00\x00\x08ssh-rsa',
100
-                ValueError, 'malformed SSH byte string',
101
-                False, None,
121
+                ValueError,
122
+                'malformed SSH byte string',
123
+                False,
124
+                None,
102 125
             ),
103 126
             (
104 127
                 b'\x00\x00\x00\x04XXX trailing text',
105
-                ValueError, 'malformed SSH byte string',
106
-                True, (b'XXX ', b'trailing text'),
128
+                ValueError,
129
+                'malformed SSH byte string',
130
+                True,
131
+                (b'XXX ', b'trailing text'),
107 132
             ),
108
-    ])
109
-    def test_312_unstring_exceptions(self, input, exc_type, exc_pattern,
110
-                                     has_trailer, parts):
133
+        ],
134
+    )
135
+    def test_312_unstring_exceptions(
136
+        self, input, exc_type, exc_pattern, has_trailer, parts
137
+    ):
111 138
         unstring = ssh_agent_client.SSHAgentClient.unstring
112 139
         unstring_prefix = ssh_agent_client.SSHAgentClient.unstring_prefix
113 140
         with pytest.raises(exc_type, match=exc_pattern):
... ...
@@ -118,22 +145,26 @@ class TestStaticFunctionality:
118 145
             with pytest.raises(exc_type, match=exc_pattern):
119 146
                 unstring_prefix(input)
120 147
 
148
+
121 149
 @tests.skip_if_no_agent
122 150
 class TestAgentInteraction:
123
-
124
-    @pytest.mark.parametrize(['keytype', 'data_dict'],
125
-                             list(tests.SUPPORTED_KEYS.items()))
151
+    @pytest.mark.parametrize(
152
+        ['keytype', 'data_dict'], list(tests.SUPPORTED_KEYS.items())
153
+    )
126 154
     def test_200_sign_data_via_agent(self, keytype, data_dict):
127 155
         del keytype  # Unused.
128 156
         private_key = data_dict['private_key']
129 157
         try:
130
-            _ = subprocess.run(['ssh-add', '-t', '30', '-q', '-'],
131
-                               input=private_key, check=True,
132
-                               capture_output=True)
158
+            _ = subprocess.run(
159
+                ['ssh-add', '-t', '30', '-q', '-'],
160
+                input=private_key,
161
+                check=True,
162
+                capture_output=True,
163
+            )
133 164
         except subprocess.CalledProcessError as e:
134 165
             pytest.skip(
135
-                f"uploading test key: {e!r}, stdout={e.stdout!r}, "
136
-                f"stderr={e.stderr!r}"
166
+                f'uploading test key: {e!r}, stdout={e.stdout!r}, '
167
+                f'stderr={e.stderr!r}'
137 168
             )
138 169
         else:
139 170
             try:
... ...
@@ -141,37 +172,48 @@ class TestAgentInteraction:
141 172
             except OSError:  # pragma: no cover
142 173
                 pytest.skip('communication error with the SSH agent')
143 174
         with client:
144
-            key_comment_pairs = {bytes(k): bytes(c)
145
-                                 for k, c in client.list_keys()}
175
+            key_comment_pairs = {
176
+                bytes(k): bytes(c) for k, c in client.list_keys()
177
+            }
146 178
             public_key_data = data_dict['public_key_data']
147 179
             expected_signature = data_dict['expected_signature']
148 180
             derived_passphrase = data_dict['derived_passphrase']
149 181
             if public_key_data not in key_comment_pairs:  # pragma: no cover
150 182
                 pytest.skip('prerequisite SSH key not loaded')
151
-            signature = bytes(client.sign(
152
-                payload=derivepassphrase.Vault._UUID, key=public_key_data))
183
+            signature = bytes(
184
+                client.sign(
185
+                    payload=derivepassphrase.Vault._UUID, key=public_key_data
186
+                )
187
+            )
153 188
             assert signature == expected_signature, 'SSH signature mismatch'
154
-            signature2 = bytes(client.sign(
155
-                payload=derivepassphrase.Vault._UUID, key=public_key_data))
189
+            signature2 = bytes(
190
+                client.sign(
191
+                    payload=derivepassphrase.Vault._UUID, key=public_key_data
192
+                )
193
+            )
156 194
             assert signature2 == expected_signature, 'SSH signature mismatch'
157 195
             assert (
158
-                derivepassphrase.Vault.phrase_from_key(public_key_data) ==
159
-                derived_passphrase
196
+                derivepassphrase.Vault.phrase_from_key(public_key_data)
197
+                == derived_passphrase
160 198
             ), 'SSH signature mismatch'
161 199
 
162
-    @pytest.mark.parametrize(['keytype', 'data_dict'],
163
-                             list(tests.UNSUITABLE_KEYS.items()))
200
+    @pytest.mark.parametrize(
201
+        ['keytype', 'data_dict'], list(tests.UNSUITABLE_KEYS.items())
202
+    )
164 203
     def test_201_sign_data_via_agent_unsupported(self, keytype, data_dict):
165 204
         del keytype  # Unused.
166 205
         private_key = data_dict['private_key']
167 206
         try:
168
-            _ = subprocess.run(['ssh-add', '-t', '30', '-q', '-'],
169
-                               input=private_key, check=True,
170
-                               capture_output=True)
207
+            _ = subprocess.run(
208
+                ['ssh-add', '-t', '30', '-q', '-'],
209
+                input=private_key,
210
+                check=True,
211
+                capture_output=True,
212
+            )
171 213
         except subprocess.CalledProcessError as e:  # pragma: no cover
172 214
             pytest.skip(
173
-                f"uploading test key: {e!r}, stdout={e.stdout!r}, "
174
-                f"stderr={e.stderr!r}"
215
+                f'uploading test key: {e!r}, stdout={e.stdout!r}, '
216
+                f'stderr={e.stderr!r}'
175 217
             )
176 218
         else:
177 219
             try:
... ...
@@ -179,16 +221,23 @@ class TestAgentInteraction:
179 221
             except OSError:  # pragma: no cover
180 222
                 pytest.skip('communication error with the SSH agent')
181 223
         with client:
182
-            key_comment_pairs = {bytes(k): bytes(c)
183
-                                 for k, c in client.list_keys()}
224
+            key_comment_pairs = {
225
+                bytes(k): bytes(c) for k, c in client.list_keys()
226
+            }
184 227
             public_key_data = data_dict['public_key_data']
185 228
             _ = data_dict['expected_signature']
186 229
             if public_key_data not in key_comment_pairs:  # pragma: no cover
187 230
                 pytest.skip('prerequisite SSH key not loaded')
188
-            signature = bytes(client.sign(
189
-                payload=derivepassphrase.Vault._UUID, key=public_key_data))
190
-            signature2 = bytes(client.sign(
191
-                payload=derivepassphrase.Vault._UUID, key=public_key_data))
231
+            signature = bytes(
232
+                client.sign(
233
+                    payload=derivepassphrase.Vault._UUID, key=public_key_data
234
+                )
235
+            )
236
+            signature2 = bytes(
237
+                client.sign(
238
+                    payload=derivepassphrase.Vault._UUID, key=public_key_data
239
+                )
240
+            )
192 241
             assert signature != signature2, 'SSH signature repeatable?!'
193 242
             with pytest.raises(ValueError, match='unsuitable SSH key'):
194 243
                 derivepassphrase.Vault.phrase_from_key(public_key_data)
... ...
@@ -207,20 +256,32 @@ class TestAgentInteraction:
207 256
     @pytest.mark.parametrize(['key', 'single'], list(_params()))
208 257
     def test_210_ssh_key_selector(self, monkeypatch, key, single):
209 258
         def key_is_suitable(key: bytes):
210
-            return key in {v['public_key_data']
211
-                           for v in tests.SUPPORTED_KEYS.values()}
259
+            return key in {
260
+                v['public_key_data'] for v in tests.SUPPORTED_KEYS.values()
261
+            }
262
+
212 263
         if single:
213
-            monkeypatch.setattr(ssh_agent_client.SSHAgentClient,
214
-                                'list_keys', tests.list_keys_singleton)
215
-            keys = [pair.key for pair in tests.list_keys_singleton()
216
-                    if key_is_suitable(pair.key)]
264
+            monkeypatch.setattr(
265
+                ssh_agent_client.SSHAgentClient,
266
+                'list_keys',
267
+                tests.list_keys_singleton,
268
+            )
269
+            keys = [
270
+                pair.key
271
+                for pair in tests.list_keys_singleton()
272
+                if key_is_suitable(pair.key)
273
+            ]
217 274
             index = '1'
218 275
             text = 'Use this key? yes\n'
219 276
         else:
220
-            monkeypatch.setattr(ssh_agent_client.SSHAgentClient,
221
-                                'list_keys', tests.list_keys)
222
-            keys = [pair.key for pair in tests.list_keys()
223
-                    if key_is_suitable(pair.key)]
277
+            monkeypatch.setattr(
278
+                ssh_agent_client.SSHAgentClient, 'list_keys', tests.list_keys
279
+            )
280
+            keys = [
281
+                pair.key
282
+                for pair in tests.list_keys()
283
+                if key_is_suitable(pair.key)
284
+            ]
224 285
             index = str(1 + keys.index(key))
225 286
             n = len(keys)
226 287
             text = f'Your selection? (1-{n}, leave empty to abort): {index}\n'
... ...
@@ -232,31 +293,36 @@ class TestAgentInteraction:
232 293
             click.echo(base64.standard_b64encode(key).decode('ASCII'))
233 294
 
234 295
         runner = click.testing.CliRunner(mix_stderr=True)
235
-        result = runner.invoke(driver, [],
296
+        result = runner.invoke(
297
+            driver,
298
+            [],
236 299
             input=('yes\n' if single else f'{index}\n'),
237
-                               catch_exceptions=True)
238
-        assert result.stdout.startswith('Suitable SSH keys:\n'), (
239
-            'missing expected output'
300
+            catch_exceptions=True,
240 301
         )
302
+        assert result.stdout.startswith(
303
+            'Suitable SSH keys:\n'
304
+        ), 'missing expected output'
241 305
         assert text in result.stdout, 'missing expected output'
242
-        assert (
243
-            result.stdout.endswith(f'\n{b64_key}\n')
306
+        assert result.stdout.endswith(
307
+            f'\n{b64_key}\n'
244 308
         ), 'missing expected output'
245 309
         assert result.exit_code == 0, 'driver program failed?!'
246 310
 
247 311
     del _params
248 312
 
249 313
     def test_300_constructor_bad_running_agent(self, monkeypatch):
250
-        monkeypatch.setenv('SSH_AUTH_SOCK',
251
-                           os.environ['SSH_AUTH_SOCK'] + '~')
314
+        monkeypatch.setenv('SSH_AUTH_SOCK', os.environ['SSH_AUTH_SOCK'] + '~')
252 315
         sock = socket.socket(family=socket.AF_UNIX)
253 316
         with pytest.raises(OSError):  # noqa: PT011
254 317
             ssh_agent_client.SSHAgentClient(socket=sock)
255 318
 
256
-    @pytest.mark.parametrize('response', [
319
+    @pytest.mark.parametrize(
320
+        'response',
321
+        [
257 322
             b'\x00\x00',
258 323
             b'\x00\x00\x00\x1f some bytes missing',
259
-    ])
324
+        ],
325
+    )
260 326
     def test_310_truncated_server_response(self, monkeypatch, response):
261 327
         client = ssh_agent_client.SSHAgentClient()
262 328
         response_stream = io.BytesIO(response)
... ...
@@ -282,13 +351,17 @@ class TestAgentInteraction:
282 351
                 ssh_agent_client.TrailingDataError,
283 352
                 'Overlong response',
284 353
             ),
285
-        ]
354
+        ],
286 355
     )
287
-    def test_320_list_keys_error_responses(self, monkeypatch, response_code,
288
-                                           response, exc_type, exc_pattern):
356
+    def test_320_list_keys_error_responses(
357
+        self, monkeypatch, response_code, response, exc_type, exc_pattern
358
+    ):
289 359
         client = ssh_agent_client.SSHAgentClient()
290
-        monkeypatch.setattr(client, 'request',
291
-                            lambda *a, **kw: (response_code, response))  # noqa: ARG005
360
+        monkeypatch.setattr(
361
+            client,
362
+            'request',
363
+            lambda *a, **kw: (response_code, response),  # noqa: ARG005
364
+        )
292 365
         with pytest.raises(exc_type, match=exc_pattern):
293 366
             client.list_keys()
294 367
 
... ...
@@ -309,16 +382,19 @@ class TestAgentInteraction:
309 382
                 (255, b''),
310 383
                 RuntimeError,
311 384
                 'signing data failed:',
385
+            ),
386
+        ],
312 387
     )
313
-        ]
314
-    )
315
-    def test_330_sign_error_responses(self, monkeypatch, key, check,
316
-                                      response, exc_type, exc_pattern):
388
+    def test_330_sign_error_responses(
389
+        self, monkeypatch, key, check, response, exc_type, exc_pattern
390
+    ):
317 391
         client = ssh_agent_client.SSHAgentClient()
318 392
         monkeypatch.setattr(client, 'request', lambda a, b: response)  # noqa: ARG005
319 393
         KeyCommentPair = ssh_agent_client.types.KeyCommentPair  # noqa: N806
320
-        loaded_keys = [KeyCommentPair(v['public_key_data'], b'no comment')
321
-                       for v in tests.SUPPORTED_KEYS.values()]
394
+        loaded_keys = [
395
+            KeyCommentPair(v['public_key_data'], b'no comment')
396
+            for v in tests.SUPPORTED_KEYS.values()
397
+        ]
322 398
         monkeypatch.setattr(client, 'list_keys', lambda: loaded_keys)
323 399
         with pytest.raises(exc_type, match=exc_pattern):
324 400
             client.sign(key, b'abc', check_if_key_loaded=check)
325 401