Remove use of the monkeypatch test fixture in favor of the context manager
Marco Ricci

Marco Ricci commited on 2025-01-25 22:15:53
Zeige 4 geänderte Dateien mit 999 Einfügungen und 360 Löschungen.


The `pytest.MonkeyPatch.context` context manager is just as easy to use,
and does not interfere with hypothesis.
... ...
@@ -348,17 +348,23 @@ class TestAllCLI:
348 348
     )
349 349
     def test_200_eager_options(
350 350
         self,
351
-        monkeypatch: pytest.MonkeyPatch,
352 351
         command: list[str],
353 352
         arguments: list[str],
354 353
         non_eager_arguments: list[str],
355 354
     ) -> None:
356 355
         """Eager options terminate option and argument processing."""
357 356
         runner = click.testing.CliRunner(mix_stderr=False)
358
-        with tests.isolated_config(
357
+        # TODO(the-13th-letter): Rewrite using parenthesized
358
+        # with-statements.
359
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
360
+        with contextlib.ExitStack() as stack:
361
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
362
+            stack.enter_context(
363
+                tests.isolated_config(
359 364
                     monkeypatch=monkeypatch,
360 365
                     runner=runner,
361
-        ):
366
+                )
367
+            )
362 368
             result_ = runner.invoke(
363 369
                 cli.derivepassphrase,
364 370
                 [*command, *arguments, *non_eager_arguments],
... ...
@@ -394,7 +400,6 @@ class TestAllCLI:
394 400
     )
395 401
     def test_201_no_color_force_color(
396 402
         self,
397
-        monkeypatch: pytest.MonkeyPatch,
398 403
         no_color: bool,
399 404
         force_color: bool,
400 405
         isatty: bool,
... ...
@@ -406,10 +411,17 @@ class TestAllCLI:
406 411
         # no_color.  Otherwise set color if and only if we have a TTY.
407 412
         color = force_color or not no_color if isatty else force_color
408 413
         runner = click.testing.CliRunner(mix_stderr=False)
409
-        with tests.isolated_config(
414
+        # TODO(the-13th-letter): Rewrite using parenthesized
415
+        # with-statements.
416
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
417
+        with contextlib.ExitStack() as stack:
418
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
419
+            stack.enter_context(
420
+                tests.isolated_config(
410 421
                     monkeypatch=monkeypatch,
411 422
                     runner=runner,
412
-        ):
423
+                )
424
+            )
413 425
             if no_color:
414 426
                 monkeypatch.setenv('NO_COLOR', 'yes')
415 427
             if force_color:
... ...
@@ -437,14 +449,20 @@ class TestCLI:
437 449
 
438 450
     def test_200_help_output(
439 451
         self,
440
-        monkeypatch: pytest.MonkeyPatch,
441 452
     ) -> None:
442 453
         """The `--help` option emits help text."""
443 454
         runner = click.testing.CliRunner(mix_stderr=False)
444
-        with tests.isolated_config(
455
+        # TODO(the-13th-letter): Rewrite using parenthesized
456
+        # with-statements.
457
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
458
+        with contextlib.ExitStack() as stack:
459
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
460
+            stack.enter_context(
461
+                tests.isolated_config(
445 462
                     monkeypatch=monkeypatch,
446 463
                     runner=runner,
447
-        ):
464
+                )
465
+            )
448 466
             result_ = runner.invoke(
449 467
                 cli.derivepassphrase_vault,
450 468
                 ['--help'],
... ...
@@ -460,14 +478,20 @@ class TestCLI:
460 478
 
461 479
     def test_200a_version_output(
462 480
         self,
463
-        monkeypatch: pytest.MonkeyPatch,
464 481
     ) -> None:
465 482
         """The `--version` option emits version information."""
466 483
         runner = click.testing.CliRunner(mix_stderr=False)
467
-        with tests.isolated_config(
484
+        # TODO(the-13th-letter): Rewrite using parenthesized
485
+        # with-statements.
486
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
487
+        with contextlib.ExitStack() as stack:
488
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
489
+            stack.enter_context(
490
+                tests.isolated_config(
468 491
                     monkeypatch=monkeypatch,
469 492
                     runner=runner,
470
-        ):
493
+                )
494
+            )
471 495
             result_ = runner.invoke(
472 496
                 cli.derivepassphrase_vault,
473 497
                 ['--version'],
... ...
@@ -485,17 +509,27 @@ class TestCLI:
485 509
         'charset_name', ['lower', 'upper', 'number', 'space', 'dash', 'symbol']
486 510
     )
487 511
     def test_201_disable_character_set(
488
-        self, monkeypatch: pytest.MonkeyPatch, charset_name: str
512
+        self,
513
+        charset_name: str,
489 514
     ) -> None:
490 515
         """Named character classes can be disabled on the command-line."""
491
-        monkeypatch.setattr(cli, '_prompt_for_passphrase', tests.auto_prompt)
492 516
         option = f'--{charset_name}'
493 517
         charset = vault.Vault._CHARSETS[charset_name].decode('ascii')
494 518
         runner = click.testing.CliRunner(mix_stderr=False)
495
-        with tests.isolated_config(
519
+        # TODO(the-13th-letter): Rewrite using parenthesized
520
+        # with-statements.
521
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
522
+        with contextlib.ExitStack() as stack:
523
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
524
+            stack.enter_context(
525
+                tests.isolated_config(
496 526
                     monkeypatch=monkeypatch,
497 527
                     runner=runner,
498
-        ):
528
+                )
529
+            )
530
+            monkeypatch.setattr(
531
+                cli, '_prompt_for_passphrase', tests.auto_prompt
532
+            )
499 533
             result_ = runner.invoke(
500 534
                 cli.derivepassphrase_vault,
501 535
                 [option, '0', '-p', '--', DUMMY_SERVICE],
... ...
@@ -510,15 +544,24 @@ class TestCLI:
510 544
             )
511 545
 
512 546
     def test_202_disable_repetition(
513
-        self, monkeypatch: pytest.MonkeyPatch
547
+        self,
514 548
     ) -> None:
515 549
         """Character repetition can be disabled on the command-line."""
516
-        monkeypatch.setattr(cli, '_prompt_for_passphrase', tests.auto_prompt)
517 550
         runner = click.testing.CliRunner(mix_stderr=False)
518
-        with tests.isolated_config(
551
+        # TODO(the-13th-letter): Rewrite using parenthesized
552
+        # with-statements.
553
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
554
+        with contextlib.ExitStack() as stack:
555
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
556
+            stack.enter_context(
557
+                tests.isolated_config(
519 558
                     monkeypatch=monkeypatch,
520 559
                     runner=runner,
521
-        ):
560
+                )
561
+            )
562
+            monkeypatch.setattr(
563
+                cli, '_prompt_for_passphrase', tests.auto_prompt
564
+            )
522 565
             result_ = runner.invoke(
523 566
                 cli.derivepassphrase_vault,
524 567
                 ['--repeat', '0', '-p', '--', DUMMY_SERVICE],
... ...
@@ -562,14 +605,22 @@ class TestCLI:
562 605
     )
563 606
     def test_204a_key_from_config(
564 607
         self,
565
-        monkeypatch: pytest.MonkeyPatch,
566 608
         config: _types.VaultConfig,
567 609
     ) -> None:
568 610
         """A stored configured SSH key will be used."""
569 611
         runner = click.testing.CliRunner(mix_stderr=False)
570
-        with tests.isolated_vault_config(
571
-            monkeypatch=monkeypatch, runner=runner, vault_config=config
572
-        ):
612
+        # TODO(the-13th-letter): Rewrite using parenthesized
613
+        # with-statements.
614
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
615
+        with contextlib.ExitStack() as stack:
616
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
617
+            stack.enter_context(
618
+                tests.isolated_vault_config(
619
+                    monkeypatch=monkeypatch,
620
+                    runner=runner,
621
+                    vault_config=config,
622
+                )
623
+            )
573 624
             monkeypatch.setattr(
574 625
                 vault.Vault, 'phrase_from_key', tests.phrase_from_key
575 626
             )
... ...
@@ -591,15 +642,24 @@ class TestCLI:
591 642
         )
592 643
 
593 644
     def test_204b_key_from_command_line(
594
-        self, monkeypatch: pytest.MonkeyPatch
645
+        self,
595 646
     ) -> None:
596 647
         """An SSH key requested on the command-line will be used."""
597 648
         runner = click.testing.CliRunner(mix_stderr=False)
598
-        with tests.isolated_vault_config(
649
+        # TODO(the-13th-letter): Rewrite using parenthesized
650
+        # with-statements.
651
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
652
+        with contextlib.ExitStack() as stack:
653
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
654
+            stack.enter_context(
655
+                tests.isolated_vault_config(
599 656
                     monkeypatch=monkeypatch,
600 657
                     runner=runner,
601
-            vault_config={'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}},
602
-        ):
658
+                    vault_config={
659
+                        'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}
660
+                    },
661
+                )
662
+            )
603 663
             monkeypatch.setattr(
604 664
                 cli, '_get_suitable_ssh_keys', tests.suitable_ssh_keys
605 665
             )
... ...
@@ -649,22 +709,29 @@ class TestCLI:
649 709
     @pytest.mark.parametrize('key_index', [1, 2, 3], ids=lambda i: f'index{i}')
650 710
     def test_204c_key_override_on_command_line(
651 711
         self,
652
-        monkeypatch: pytest.MonkeyPatch,
653 712
         running_ssh_agent: tests.RunningSSHAgentInfo,
654 713
         config: dict[str, Any],
655 714
         key_index: int,
656 715
     ) -> None:
657 716
         """A command-line SSH key will override the configured key."""
658
-        with monkeypatch.context():
717
+        runner = click.testing.CliRunner(mix_stderr=False)
718
+        # TODO(the-13th-letter): Rewrite using parenthesized
719
+        # with-statements.
720
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
721
+        with contextlib.ExitStack() as stack:
722
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
723
+            stack.enter_context(
724
+                tests.isolated_vault_config(
725
+                    monkeypatch=monkeypatch,
726
+                    runner=runner,
727
+                    vault_config=config,
728
+                )
729
+            )
659 730
             monkeypatch.setenv('SSH_AUTH_SOCK', running_ssh_agent.socket)
660 731
             monkeypatch.setattr(
661 732
                 ssh_agent.SSHAgentClient, 'list_keys', tests.list_keys
662 733
             )
663 734
             monkeypatch.setattr(ssh_agent.SSHAgentClient, 'sign', tests.sign)
664
-            runner = click.testing.CliRunner(mix_stderr=False)
665
-            with tests.isolated_vault_config(
666
-                monkeypatch=monkeypatch, runner=runner, vault_config=config
667
-            ):
668 735
             result_ = runner.invoke(
669 736
                 cli.derivepassphrase_vault,
670 737
                 ['-k', '--', DUMMY_SERVICE],
... ...
@@ -680,18 +747,17 @@ class TestCLI:
680 747
 
681 748
     def test_205_service_phrase_if_key_in_global_config(
682 749
         self,
683
-        monkeypatch: pytest.MonkeyPatch,
684 750
         running_ssh_agent: tests.RunningSSHAgentInfo,
685 751
     ) -> None:
686 752
         """A command-line passphrase will override the configured key."""
687
-        with monkeypatch.context():
688
-            monkeypatch.setenv('SSH_AUTH_SOCK', running_ssh_agent.socket)
689
-            monkeypatch.setattr(
690
-                ssh_agent.SSHAgentClient, 'list_keys', tests.list_keys
691
-            )
692
-            monkeypatch.setattr(ssh_agent.SSHAgentClient, 'sign', tests.sign)
693 753
         runner = click.testing.CliRunner(mix_stderr=False)
694
-            with tests.isolated_vault_config(
754
+        # TODO(the-13th-letter): Rewrite using parenthesized
755
+        # with-statements.
756
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
757
+        with contextlib.ExitStack() as stack:
758
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
759
+            stack.enter_context(
760
+                tests.isolated_vault_config(
695 761
                     monkeypatch=monkeypatch,
696 762
                     runner=runner,
697 763
                     vault_config={
... ...
@@ -703,7 +769,13 @@ class TestCLI:
703 769
                             }
704 770
                         },
705 771
                     },
706
-            ):
772
+                )
773
+            )
774
+            monkeypatch.setenv('SSH_AUTH_SOCK', running_ssh_agent.socket)
775
+            monkeypatch.setattr(
776
+                ssh_agent.SSHAgentClient, 'list_keys', tests.list_keys
777
+            )
778
+            monkeypatch.setattr(ssh_agent.SSHAgentClient, 'sign', tests.sign)
707 779
             result_ = runner.invoke(
708 780
                 cli.derivepassphrase_vault,
709 781
                 ['--', DUMMY_SERVICE],
... ...
@@ -755,25 +827,30 @@ class TestCLI:
755 827
     )
756 828
     def test_206_setting_phrase_thus_overriding_key_in_config(
757 829
         self,
758
-        monkeypatch: pytest.MonkeyPatch,
759 830
         running_ssh_agent: tests.RunningSSHAgentInfo,
760 831
         caplog: pytest.LogCaptureFixture,
761 832
         config: _types.VaultConfig,
762 833
         command_line: list[str],
763 834
     ) -> None:
764 835
         """Configuring a passphrase atop an SSH key works, but warns."""
765
-        with monkeypatch.context():
836
+        runner = click.testing.CliRunner(mix_stderr=False)
837
+        # TODO(the-13th-letter): Rewrite using parenthesized
838
+        # with-statements.
839
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
840
+        with contextlib.ExitStack() as stack:
841
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
842
+            stack.enter_context(
843
+                tests.isolated_vault_config(
844
+                    monkeypatch=monkeypatch,
845
+                    runner=runner,
846
+                    vault_config=config,
847
+                )
848
+            )
766 849
             monkeypatch.setenv('SSH_AUTH_SOCK', running_ssh_agent.socket)
767 850
             monkeypatch.setattr(
768 851
                 ssh_agent.SSHAgentClient, 'list_keys', tests.list_keys
769 852
             )
770 853
             monkeypatch.setattr(ssh_agent.SSHAgentClient, 'sign', tests.sign)
771
-            runner = click.testing.CliRunner(mix_stderr=False)
772
-            with tests.isolated_vault_config(
773
-                monkeypatch=monkeypatch,
774
-                runner=runner,
775
-                vault_config=config,
776
-            ):
777 854
             result_ = runner.invoke(
778 855
                 cli.derivepassphrase_vault,
779 856
                 command_line,
... ...
@@ -812,14 +889,22 @@ class TestCLI:
812 889
         ],
813 890
     )
814 891
     def test_210_invalid_argument_range(
815
-        self, monkeypatch: pytest.MonkeyPatch, option: str
892
+        self,
893
+        option: str,
816 894
     ) -> None:
817 895
         """Requesting invalidly many characters from a class fails."""
818 896
         runner = click.testing.CliRunner(mix_stderr=False)
819
-        with tests.isolated_config(
897
+        # TODO(the-13th-letter): Rewrite using parenthesized
898
+        # with-statements.
899
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
900
+        with contextlib.ExitStack() as stack:
901
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
902
+            stack.enter_context(
903
+                tests.isolated_config(
820 904
                     monkeypatch=monkeypatch,
821 905
                     runner=runner,
822
-        ):
906
+                )
907
+            )
823 908
             for value in '-42', 'invalid':
824 909
                 result_ = runner.invoke(
825 910
                     cli.derivepassphrase_vault,
... ...
@@ -848,20 +933,28 @@ class TestCLI:
848 933
     )
849 934
     def test_211_service_needed(
850 935
         self,
851
-        monkeypatch: pytest.MonkeyPatch,
852 936
         options: list[str],
853 937
         service: bool | None,
854 938
         input: str | None,
855 939
         check_success: bool,
856 940
     ) -> None:
857 941
         """We require or forbid a service argument, depending on options."""
858
-        monkeypatch.setattr(cli, '_prompt_for_passphrase', tests.auto_prompt)
859 942
         runner = click.testing.CliRunner(mix_stderr=False)
860
-        with tests.isolated_vault_config(
943
+        # TODO(the-13th-letter): Rewrite using parenthesized
944
+        # with-statements.
945
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
946
+        with contextlib.ExitStack() as stack:
947
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
948
+            stack.enter_context(
949
+                tests.isolated_vault_config(
861 950
                     monkeypatch=monkeypatch,
862 951
                     runner=runner,
863 952
                     vault_config={'global': {'phrase': 'abc'}, 'services': {}},
864
-        ):
953
+                )
954
+            )
955
+            monkeypatch.setattr(
956
+                cli, '_prompt_for_passphrase', tests.auto_prompt
957
+            )
865 958
             result_ = runner.invoke(
866 959
                 cli.derivepassphrase_vault,
867 960
                 options if service else [*options, '--', DUMMY_SERVICE],
... ...
@@ -883,11 +976,18 @@ class TestCLI:
883 976
                     'expected clean exit'
884 977
                 )
885 978
         if check_success:
886
-            with tests.isolated_vault_config(
979
+            # TODO(the-13th-letter): Rewrite using parenthesized
980
+            # with-statements.
981
+            # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
982
+            with contextlib.ExitStack() as stack:
983
+                monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
984
+                stack.enter_context(
985
+                    tests.isolated_vault_config(
887 986
                         monkeypatch=monkeypatch,
888 987
                         runner=runner,
889 988
                         vault_config={'global': {'phrase': 'abc'}, 'services': {}},
890
-            ):
989
+                    )
990
+                )
891 991
                 monkeypatch.setattr(
892 992
                     cli, '_prompt_for_passphrase', tests.auto_prompt
893 993
                 )
... ...
@@ -902,7 +1002,6 @@ class TestCLI:
902 1002
 
903 1003
     def test_211a_empty_service_name_causes_warning(
904 1004
         self,
905
-        monkeypatch: pytest.MonkeyPatch,
906 1005
         caplog: pytest.LogCaptureFixture,
907 1006
     ) -> None:
908 1007
         """Using an empty service name (where permissible) warns.
... ...
@@ -918,13 +1017,22 @@ class TestCLI:
918 1017
                 'An empty SERVICE is not supported by vault(1)', [record]
919 1018
             )
920 1019
 
921
-        monkeypatch.setattr(cli, '_prompt_for_passphrase', tests.auto_prompt)
922 1020
         runner = click.testing.CliRunner(mix_stderr=False)
923
-        with tests.isolated_vault_config(
1021
+        # TODO(the-13th-letter): Rewrite using parenthesized
1022
+        # with-statements.
1023
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1024
+        with contextlib.ExitStack() as stack:
1025
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1026
+            stack.enter_context(
1027
+                tests.isolated_vault_config(
924 1028
                     monkeypatch=monkeypatch,
925 1029
                     runner=runner,
926 1030
                     vault_config={'services': {}},
927
-        ):
1031
+                )
1032
+            )
1033
+            monkeypatch.setattr(
1034
+                cli, '_prompt_for_passphrase', tests.auto_prompt
1035
+            )
928 1036
             result_ = runner.invoke(
929 1037
                 cli.derivepassphrase_vault,
930 1038
                 ['--config', '--length=30', '--', ''],
... ...
@@ -968,16 +1076,22 @@ class TestCLI:
968 1076
     )
969 1077
     def test_212_incompatible_options(
970 1078
         self,
971
-        monkeypatch: pytest.MonkeyPatch,
972 1079
         options: list[str],
973 1080
         service: bool | None,
974 1081
     ) -> None:
975 1082
         """Incompatible options are detected."""
976 1083
         runner = click.testing.CliRunner(mix_stderr=False)
977
-        with tests.isolated_config(
1084
+        # TODO(the-13th-letter): Rewrite using parenthesized
1085
+        # with-statements.
1086
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1087
+        with contextlib.ExitStack() as stack:
1088
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1089
+            stack.enter_context(
1090
+                tests.isolated_config(
978 1091
                     monkeypatch=monkeypatch,
979 1092
                     runner=runner,
980
-        ):
1093
+                )
1094
+            )
981 1095
             result_ = runner.invoke(
982 1096
                 cli.derivepassphrase_vault,
983 1097
                 [*options, '--', DUMMY_SERVICE] if service else options,
... ...
@@ -999,17 +1113,23 @@ class TestCLI:
999 1113
     )
1000 1114
     def test_213_import_config_success(
1001 1115
         self,
1002
-        monkeypatch: pytest.MonkeyPatch,
1003 1116
         caplog: pytest.LogCaptureFixture,
1004 1117
         config: Any,
1005 1118
     ) -> None:
1006 1119
         """Importing a configuration works."""
1007 1120
         runner = click.testing.CliRunner(mix_stderr=False)
1008
-        with tests.isolated_vault_config(
1121
+        # TODO(the-13th-letter): Rewrite using parenthesized
1122
+        # with-statements.
1123
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1124
+        with contextlib.ExitStack() as stack:
1125
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1126
+            stack.enter_context(
1127
+                tests.isolated_vault_config(
1009 1128
                     monkeypatch=monkeypatch,
1010 1129
                     runner=runner,
1011 1130
                     vault_config={'services': {}},
1012
-        ):
1131
+                )
1132
+            )
1013 1133
             result_ = runner.invoke(
1014 1134
                 cli.derivepassphrase_vault,
1015 1135
                 ['--import', '-'],
... ...
@@ -1051,11 +1171,18 @@ class TestCLI:
1051 1171
         config2 = copy.deepcopy(config)
1052 1172
         _types.clean_up_falsy_vault_config_values(config2)
1053 1173
         runner = click.testing.CliRunner(mix_stderr=False)
1054
-        with tests.isolated_vault_config(
1055
-            monkeypatch=pytest.MonkeyPatch(),
1174
+        # TODO(the-13th-letter): Rewrite using parenthesized
1175
+        # with-statements.
1176
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1177
+        with contextlib.ExitStack() as stack:
1178
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1179
+            stack.enter_context(
1180
+                tests.isolated_vault_config(
1181
+                    monkeypatch=monkeypatch,
1056 1182
                     runner=runner,
1057 1183
                     vault_config={'services': {}},
1058
-        ):
1184
+                )
1185
+            )
1059 1186
             result_ = runner.invoke(
1060 1187
                 cli.derivepassphrase_vault,
1061 1188
                 ['--import', '-'],
... ...
@@ -1075,11 +1202,20 @@ class TestCLI:
1075 1202
 
1076 1203
     def test_213b_import_bad_config_not_vault_config(
1077 1204
         self,
1078
-        monkeypatch: pytest.MonkeyPatch,
1079 1205
     ) -> None:
1080 1206
         """Importing an invalid config fails."""
1081 1207
         runner = click.testing.CliRunner(mix_stderr=False)
1082
-        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner):
1208
+        # TODO(the-13th-letter): Rewrite using parenthesized
1209
+        # with-statements.
1210
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1211
+        with contextlib.ExitStack() as stack:
1212
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1213
+            stack.enter_context(
1214
+                tests.isolated_config(
1215
+                    monkeypatch=monkeypatch,
1216
+                    runner=runner,
1217
+                )
1218
+            )
1083 1219
             result_ = runner.invoke(
1084 1220
                 cli.derivepassphrase_vault,
1085 1221
                 ['--import', '-'],
... ...
@@ -1093,11 +1229,20 @@ class TestCLI:
1093 1229
 
1094 1230
     def test_213c_import_bad_config_not_json_data(
1095 1231
         self,
1096
-        monkeypatch: pytest.MonkeyPatch,
1097 1232
     ) -> None:
1098 1233
         """Importing an invalid config fails."""
1099 1234
         runner = click.testing.CliRunner(mix_stderr=False)
1100
-        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner):
1235
+        # TODO(the-13th-letter): Rewrite using parenthesized
1236
+        # with-statements.
1237
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1238
+        with contextlib.ExitStack() as stack:
1239
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1240
+            stack.enter_context(
1241
+                tests.isolated_config(
1242
+                    monkeypatch=monkeypatch,
1243
+                    runner=runner,
1244
+                )
1245
+            )
1101 1246
             result_ = runner.invoke(
1102 1247
                 cli.derivepassphrase_vault,
1103 1248
                 ['--import', '-'],
... ...
@@ -1111,15 +1256,26 @@ class TestCLI:
1111 1256
 
1112 1257
     def test_213d_import_bad_config_not_a_file(
1113 1258
         self,
1114
-        monkeypatch: pytest.MonkeyPatch,
1115 1259
     ) -> None:
1116 1260
         """Importing an invalid config fails."""
1117 1261
         runner = click.testing.CliRunner(mix_stderr=False)
1118
-        # `isolated_vault_config` validates the configuration.  So, to
1119
-        # pass an actual broken configuration, we must open the
1120
-        # configuration file ourselves afterwards, inside the context.
1121
-        # We also might as well use `isolated_config` instead.
1122
-        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner):
1262
+        # `isolated_vault_config` ensures the configuration is valid
1263
+        # JSON.  So, to pass an actual broken configuration, we must
1264
+        # open the configuration file ourselves afterwards, inside the
1265
+        # context.
1266
+        #
1267
+        # TODO(the-13th-letter): Rewrite using parenthesized
1268
+        # with-statements.
1269
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1270
+        with contextlib.ExitStack() as stack:
1271
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1272
+            stack.enter_context(
1273
+                tests.isolated_vault_config(
1274
+                    monkeypatch=monkeypatch,
1275
+                    runner=runner,
1276
+                    vault_config={'services': {}},
1277
+                )
1278
+            )
1123 1279
             cli._config_filename(subsystem='vault').write_text(
1124 1280
                 'This string is not valid JSON.\n', encoding='UTF-8'
1125 1281
             )
... ...
@@ -1143,12 +1299,21 @@ class TestCLI:
1143 1299
     )
1144 1300
     def test_214_export_settings_no_stored_settings(
1145 1301
         self,
1146
-        monkeypatch: pytest.MonkeyPatch,
1147 1302
         export_options: list[str],
1148 1303
     ) -> None:
1149 1304
         """Exporting the default, empty config works."""
1150 1305
         runner = click.testing.CliRunner(mix_stderr=False)
1151
-        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner):
1306
+        # TODO(the-13th-letter): Rewrite using parenthesized
1307
+        # with-statements.
1308
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1309
+        with contextlib.ExitStack() as stack:
1310
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1311
+            stack.enter_context(
1312
+                tests.isolated_config(
1313
+                    monkeypatch=monkeypatch,
1314
+                    runner=runner,
1315
+                )
1316
+            )
1152 1317
             cli._config_filename(subsystem='vault').unlink(missing_ok=True)
1153 1318
             result_ = runner.invoke(
1154 1319
                 # Test parent context navigation by not calling
... ...
@@ -1171,14 +1336,22 @@ class TestCLI:
1171 1336
     )
1172 1337
     def test_214a_export_settings_bad_stored_config(
1173 1338
         self,
1174
-        monkeypatch: pytest.MonkeyPatch,
1175 1339
         export_options: list[str],
1176 1340
     ) -> None:
1177 1341
         """Exporting an invalid config fails."""
1178 1342
         runner = click.testing.CliRunner(mix_stderr=False)
1179
-        with tests.isolated_vault_config(
1180
-            monkeypatch=monkeypatch, runner=runner, vault_config={}
1181
-        ):
1343
+        # TODO(the-13th-letter): Rewrite using parenthesized
1344
+        # with-statements.
1345
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1346
+        with contextlib.ExitStack() as stack:
1347
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1348
+            stack.enter_context(
1349
+                tests.isolated_vault_config(
1350
+                    monkeypatch=monkeypatch,
1351
+                    runner=runner,
1352
+                    vault_config={},
1353
+                )
1354
+            )
1182 1355
             result_ = runner.invoke(
1183 1356
                 cli.derivepassphrase_vault,
1184 1357
                 ['--export', '-', *export_options],
... ...
@@ -1199,12 +1372,21 @@ class TestCLI:
1199 1372
     )
1200 1373
     def test_214b_export_settings_not_a_file(
1201 1374
         self,
1202
-        monkeypatch: pytest.MonkeyPatch,
1203 1375
         export_options: list[str],
1204 1376
     ) -> None:
1205 1377
         """Exporting an invalid config fails."""
1206 1378
         runner = click.testing.CliRunner(mix_stderr=False)
1207
-        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner):
1379
+        # TODO(the-13th-letter): Rewrite using parenthesized
1380
+        # with-statements.
1381
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1382
+        with contextlib.ExitStack() as stack:
1383
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1384
+            stack.enter_context(
1385
+                tests.isolated_config(
1386
+                    monkeypatch=monkeypatch,
1387
+                    runner=runner,
1388
+                )
1389
+            )
1208 1390
             config_file = cli._config_filename(subsystem='vault')
1209 1391
             config_file.unlink(missing_ok=True)
1210 1392
             config_file.mkdir(parents=True, exist_ok=True)
... ...
@@ -1228,12 +1410,21 @@ class TestCLI:
1228 1410
     )
1229 1411
     def test_214c_export_settings_target_not_a_file(
1230 1412
         self,
1231
-        monkeypatch: pytest.MonkeyPatch,
1232 1413
         export_options: list[str],
1233 1414
     ) -> None:
1234 1415
         """Exporting an invalid config fails."""
1235 1416
         runner = click.testing.CliRunner(mix_stderr=False)
1236
-        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner):
1417
+        # TODO(the-13th-letter): Rewrite using parenthesized
1418
+        # with-statements.
1419
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1420
+        with contextlib.ExitStack() as stack:
1421
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1422
+            stack.enter_context(
1423
+                tests.isolated_config(
1424
+                    monkeypatch=monkeypatch,
1425
+                    runner=runner,
1426
+                )
1427
+            )
1237 1428
             dname = cli._config_filename(subsystem=None)
1238 1429
             result_ = runner.invoke(
1239 1430
                 cli.derivepassphrase_vault,
... ...
@@ -1255,12 +1446,21 @@ class TestCLI:
1255 1446
     )
1256 1447
     def test_214d_export_settings_settings_directory_not_a_directory(
1257 1448
         self,
1258
-        monkeypatch: pytest.MonkeyPatch,
1259 1449
         export_options: list[str],
1260 1450
     ) -> None:
1261 1451
         """Exporting an invalid config fails."""
1262 1452
         runner = click.testing.CliRunner(mix_stderr=False)
1263
-        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner):
1453
+        # TODO(the-13th-letter): Rewrite using parenthesized
1454
+        # with-statements.
1455
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1456
+        with contextlib.ExitStack() as stack:
1457
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1458
+            stack.enter_context(
1459
+                tests.isolated_config(
1460
+                    monkeypatch=monkeypatch,
1461
+                    runner=runner,
1462
+                )
1463
+            )
1264 1464
             config_dir = cli._config_filename(subsystem=None)
1265 1465
             with contextlib.suppress(FileNotFoundError):
1266 1466
                 shutil.rmtree(config_dir)
... ...
@@ -1279,7 +1479,7 @@ class TestCLI:
1279 1479
         )
1280 1480
 
1281 1481
     def test_220_edit_notes_successfully(
1282
-        self, monkeypatch: pytest.MonkeyPatch
1482
+        self,
1283 1483
     ) -> None:
1284 1484
         """Editing notes works."""
1285 1485
         edit_result = """
... ...
@@ -1288,11 +1488,18 @@ class TestCLI:
1288 1488
 contents go here
1289 1489
 """
1290 1490
         runner = click.testing.CliRunner(mix_stderr=False)
1291
-        with tests.isolated_vault_config(
1491
+        # TODO(the-13th-letter): Rewrite using parenthesized
1492
+        # with-statements.
1493
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1494
+        with contextlib.ExitStack() as stack:
1495
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1496
+            stack.enter_context(
1497
+                tests.isolated_vault_config(
1292 1498
                     monkeypatch=monkeypatch,
1293 1499
                     runner=runner,
1294 1500
                     vault_config={'global': {'phrase': 'abc'}, 'services': {}},
1295
-        ):
1501
+                )
1502
+            )
1296 1503
             monkeypatch.setattr(click, 'edit', lambda *a, **kw: edit_result)  # noqa: ARG005
1297 1504
             result_ = runner.invoke(
1298 1505
                 cli.derivepassphrase_vault,
... ...
@@ -1311,15 +1518,22 @@ contents go here
1311 1518
             }
1312 1519
 
1313 1520
     def test_221_edit_notes_noop(
1314
-        self, monkeypatch: pytest.MonkeyPatch
1521
+        self,
1315 1522
     ) -> None:
1316 1523
         """Abandoning edited notes works."""
1317 1524
         runner = click.testing.CliRunner(mix_stderr=False)
1318
-        with tests.isolated_vault_config(
1525
+        # TODO(the-13th-letter): Rewrite using parenthesized
1526
+        # with-statements.
1527
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1528
+        with contextlib.ExitStack() as stack:
1529
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1530
+            stack.enter_context(
1531
+                tests.isolated_vault_config(
1319 1532
                     monkeypatch=monkeypatch,
1320 1533
                     runner=runner,
1321 1534
                     vault_config={'global': {'phrase': 'abc'}, 'services': {}},
1322
-        ):
1535
+                )
1536
+            )
1323 1537
             monkeypatch.setattr(click, 'edit', lambda *a, **kw: None)  # noqa: ARG005
1324 1538
             result_ = runner.invoke(
1325 1539
                 cli.derivepassphrase_vault,
... ...
@@ -1337,7 +1551,7 @@ contents go here
1337 1551
     # TODO(the-13th-letter): Keep this behavior or not, with or without
1338 1552
     # warning?
1339 1553
     def test_222_edit_notes_marker_removed(
1340
-        self, monkeypatch: pytest.MonkeyPatch
1554
+        self,
1341 1555
     ) -> None:
1342 1556
         """Removing the notes marker still saves the notes.
1343 1557
 
... ...
@@ -1345,11 +1559,18 @@ contents go here
1345 1559
 
1346 1560
         """
1347 1561
         runner = click.testing.CliRunner(mix_stderr=False)
1348
-        with tests.isolated_vault_config(
1562
+        # TODO(the-13th-letter): Rewrite using parenthesized
1563
+        # with-statements.
1564
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1565
+        with contextlib.ExitStack() as stack:
1566
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1567
+            stack.enter_context(
1568
+                tests.isolated_vault_config(
1349 1569
                     monkeypatch=monkeypatch,
1350 1570
                     runner=runner,
1351 1571
                     vault_config={'global': {'phrase': 'abc'}, 'services': {}},
1352
-        ):
1572
+                )
1573
+            )
1353 1574
             monkeypatch.setattr(click, 'edit', lambda *a, **kw: 'long\ntext')  # noqa: ARG005
1354 1575
             result_ = runner.invoke(
1355 1576
                 cli.derivepassphrase_vault,
... ...
@@ -1368,15 +1589,22 @@ contents go here
1368 1589
             }
1369 1590
 
1370 1591
     def test_223_edit_notes_abort(
1371
-        self, monkeypatch: pytest.MonkeyPatch
1592
+        self,
1372 1593
     ) -> None:
1373 1594
         """Aborting editing notes works."""
1374 1595
         runner = click.testing.CliRunner(mix_stderr=False)
1375
-        with tests.isolated_vault_config(
1596
+        # TODO(the-13th-letter): Rewrite using parenthesized
1597
+        # with-statements.
1598
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1599
+        with contextlib.ExitStack() as stack:
1600
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1601
+            stack.enter_context(
1602
+                tests.isolated_vault_config(
1376 1603
                     monkeypatch=monkeypatch,
1377 1604
                     runner=runner,
1378 1605
                     vault_config={'global': {'phrase': 'abc'}, 'services': {}},
1379
-        ):
1606
+                )
1607
+            )
1380 1608
             monkeypatch.setattr(click, 'edit', lambda *a, **kw: '\n\n')  # noqa: ARG005
1381 1609
             result_ = runner.invoke(
1382 1610
                 cli.derivepassphrase_vault,
... ...
@@ -1442,18 +1670,24 @@ contents go here
1442 1670
     )
1443 1671
     def test_224_store_config_good(
1444 1672
         self,
1445
-        monkeypatch: pytest.MonkeyPatch,
1446 1673
         command_line: list[str],
1447 1674
         input: str,
1448 1675
         result_config: Any,
1449 1676
     ) -> None:
1450 1677
         """Storing valid settings via `--config` works."""
1451 1678
         runner = click.testing.CliRunner(mix_stderr=False)
1452
-        with tests.isolated_vault_config(
1679
+        # TODO(the-13th-letter): Rewrite using parenthesized
1680
+        # with-statements.
1681
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1682
+        with contextlib.ExitStack() as stack:
1683
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1684
+            stack.enter_context(
1685
+                tests.isolated_vault_config(
1453 1686
                     monkeypatch=monkeypatch,
1454 1687
                     runner=runner,
1455 1688
                     vault_config={'global': {'phrase': 'abc'}, 'services': {}},
1456
-        ):
1689
+                )
1690
+            )
1457 1691
             monkeypatch.setattr(
1458 1692
                 cli, '_get_suitable_ssh_keys', tests.suitable_ssh_keys
1459 1693
             )
... ...
@@ -1504,18 +1738,24 @@ contents go here
1504 1738
     )
1505 1739
     def test_225_store_config_fail(
1506 1740
         self,
1507
-        monkeypatch: pytest.MonkeyPatch,
1508 1741
         command_line: list[str],
1509 1742
         input: str,
1510 1743
         err_text: str,
1511 1744
     ) -> None:
1512 1745
         """Storing invalid settings via `--config` fails."""
1513 1746
         runner = click.testing.CliRunner(mix_stderr=False)
1514
-        with tests.isolated_vault_config(
1747
+        # TODO(the-13th-letter): Rewrite using parenthesized
1748
+        # with-statements.
1749
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1750
+        with contextlib.ExitStack() as stack:
1751
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1752
+            stack.enter_context(
1753
+                tests.isolated_vault_config(
1515 1754
                     monkeypatch=monkeypatch,
1516 1755
                     runner=runner,
1517 1756
                     vault_config={'global': {'phrase': 'abc'}, 'services': {}},
1518
-        ):
1757
+                )
1758
+            )
1519 1759
             monkeypatch.setattr(
1520 1760
                 cli, '_get_suitable_ssh_keys', tests.suitable_ssh_keys
1521 1761
             )
... ...
@@ -1532,15 +1772,21 @@ contents go here
1532 1772
 
1533 1773
     def test_225a_store_config_fail_manual_no_ssh_key_selection(
1534 1774
         self,
1535
-        monkeypatch: pytest.MonkeyPatch,
1536 1775
     ) -> None:
1537 1776
         """Not selecting an SSH key during `--config --key` fails."""
1538 1777
         runner = click.testing.CliRunner(mix_stderr=False)
1539
-        with tests.isolated_vault_config(
1778
+        # TODO(the-13th-letter): Rewrite using parenthesized
1779
+        # with-statements.
1780
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1781
+        with contextlib.ExitStack() as stack:
1782
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1783
+            stack.enter_context(
1784
+                tests.isolated_vault_config(
1540 1785
                     monkeypatch=monkeypatch,
1541 1786
                     runner=runner,
1542 1787
                     vault_config={'global': {'phrase': 'abc'}, 'services': {}},
1543
-        ):
1788
+                )
1789
+            )
1544 1790
             custom_error = 'custom error message'
1545 1791
 
1546 1792
             def raiser(*_args: Any, **_kwargs: Any) -> None:
... ...
@@ -1559,17 +1805,23 @@ contents go here
1559 1805
 
1560 1806
     def test_225b_store_config_fail_manual_no_ssh_agent(
1561 1807
         self,
1562
-        monkeypatch: pytest.MonkeyPatch,
1563 1808
         skip_if_no_af_unix_support: None,
1564 1809
     ) -> None:
1565 1810
         """Not running an SSH agent during `--config --key` fails."""
1566 1811
         del skip_if_no_af_unix_support
1567 1812
         runner = click.testing.CliRunner(mix_stderr=False)
1568
-        with tests.isolated_vault_config(
1813
+        # TODO(the-13th-letter): Rewrite using parenthesized
1814
+        # with-statements.
1815
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1816
+        with contextlib.ExitStack() as stack:
1817
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1818
+            stack.enter_context(
1819
+                tests.isolated_vault_config(
1569 1820
                     monkeypatch=monkeypatch,
1570 1821
                     runner=runner,
1571 1822
                     vault_config={'global': {'phrase': 'abc'}, 'services': {}},
1572
-        ):
1823
+                )
1824
+            )
1573 1825
             monkeypatch.delenv('SSH_AUTH_SOCK', raising=False)
1574 1826
             result_ = runner.invoke(
1575 1827
                 cli.derivepassphrase_vault,
... ...
@@ -1583,15 +1835,21 @@ contents go here
1583 1835
 
1584 1836
     def test_225c_store_config_fail_manual_bad_ssh_agent_connection(
1585 1837
         self,
1586
-        monkeypatch: pytest.MonkeyPatch,
1587 1838
     ) -> None:
1588 1839
         """Not running a reachable SSH agent during `--config --key` fails."""
1589 1840
         runner = click.testing.CliRunner(mix_stderr=False)
1590
-        with tests.isolated_vault_config(
1841
+        # TODO(the-13th-letter): Rewrite using parenthesized
1842
+        # with-statements.
1843
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1844
+        with contextlib.ExitStack() as stack:
1845
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1846
+            stack.enter_context(
1847
+                tests.isolated_vault_config(
1591 1848
                     monkeypatch=monkeypatch,
1592 1849
                     runner=runner,
1593 1850
                     vault_config={'global': {'phrase': 'abc'}, 'services': {}},
1594
-        ):
1851
+                )
1852
+            )
1595 1853
             cwd = pathlib.Path.cwd().resolve()
1596 1854
             monkeypatch.setenv('SSH_AUTH_SOCK', str(cwd))
1597 1855
             result_ = runner.invoke(
... ...
@@ -1607,16 +1865,22 @@ contents go here
1607 1865
     @pytest.mark.parametrize('try_race_free_implementation', [True, False])
1608 1866
     def test_225d_store_config_fail_manual_read_only_file(
1609 1867
         self,
1610
-        monkeypatch: pytest.MonkeyPatch,
1611 1868
         try_race_free_implementation: bool,
1612 1869
     ) -> None:
1613 1870
         """Using a read-only configuration file with `--config` fails."""
1614 1871
         runner = click.testing.CliRunner(mix_stderr=False)
1615
-        with tests.isolated_vault_config(
1872
+        # TODO(the-13th-letter): Rewrite using parenthesized
1873
+        # with-statements.
1874
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1875
+        with contextlib.ExitStack() as stack:
1876
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1877
+            stack.enter_context(
1878
+                tests.isolated_vault_config(
1616 1879
                     monkeypatch=monkeypatch,
1617 1880
                     runner=runner,
1618 1881
                     vault_config={'global': {'phrase': 'abc'}, 'services': {}},
1619
-        ):
1882
+                )
1883
+            )
1620 1884
             tests.make_file_readonly(
1621 1885
                 cli._config_filename(subsystem='vault'),
1622 1886
                 try_race_free_implementation=try_race_free_implementation,
... ...
@@ -1633,15 +1897,21 @@ contents go here
1633 1897
 
1634 1898
     def test_225e_store_config_fail_manual_custom_error(
1635 1899
         self,
1636
-        monkeypatch: pytest.MonkeyPatch,
1637 1900
     ) -> None:
1638 1901
         """OS-erroring with `--config` fails."""
1639 1902
         runner = click.testing.CliRunner(mix_stderr=False)
1640
-        with tests.isolated_vault_config(
1903
+        # TODO(the-13th-letter): Rewrite using parenthesized
1904
+        # with-statements.
1905
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1906
+        with contextlib.ExitStack() as stack:
1907
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1908
+            stack.enter_context(
1909
+                tests.isolated_vault_config(
1641 1910
                     monkeypatch=monkeypatch,
1642 1911
                     runner=runner,
1643 1912
                     vault_config={'global': {'phrase': 'abc'}, 'services': {}},
1644
-        ):
1913
+                )
1914
+            )
1645 1915
             custom_error = 'custom error message'
1646 1916
 
1647 1917
             def raiser(config: Any) -> None:
... ...
@@ -1661,15 +1931,21 @@ contents go here
1661 1931
 
1662 1932
     def test_225f_store_config_fail_unset_and_set_same_settings(
1663 1933
         self,
1664
-        monkeypatch: pytest.MonkeyPatch,
1665 1934
     ) -> None:
1666 1935
         """Issuing conflicting settings to `--config` fails."""
1667 1936
         runner = click.testing.CliRunner(mix_stderr=False)
1668
-        with tests.isolated_vault_config(
1937
+        # TODO(the-13th-letter): Rewrite using parenthesized
1938
+        # with-statements.
1939
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1940
+        with contextlib.ExitStack() as stack:
1941
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1942
+            stack.enter_context(
1943
+                tests.isolated_vault_config(
1669 1944
                     monkeypatch=monkeypatch,
1670 1945
                     runner=runner,
1671 1946
                     vault_config={'global': {'phrase': 'abc'}, 'services': {}},
1672
-        ):
1947
+                )
1948
+            )
1673 1949
             result_ = runner.invoke(
1674 1950
                 cli.derivepassphrase_vault,
1675 1951
                 [
... ...
@@ -1688,17 +1964,23 @@ contents go here
1688 1964
 
1689 1965
     def test_225g_store_config_fail_manual_ssh_agent_no_keys_loaded(
1690 1966
         self,
1691
-        monkeypatch: pytest.MonkeyPatch,
1692 1967
         running_ssh_agent: tests.RunningSSHAgentInfo,
1693 1968
     ) -> None:
1694 1969
         """Not holding any SSH keys during `--config --key` fails."""
1695 1970
         del running_ssh_agent
1696 1971
         runner = click.testing.CliRunner(mix_stderr=False)
1697
-        with tests.isolated_vault_config(
1972
+        # TODO(the-13th-letter): Rewrite using parenthesized
1973
+        # with-statements.
1974
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
1975
+        with contextlib.ExitStack() as stack:
1976
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
1977
+            stack.enter_context(
1978
+                tests.isolated_vault_config(
1698 1979
                     monkeypatch=monkeypatch,
1699 1980
                     runner=runner,
1700 1981
                     vault_config={'global': {'phrase': 'abc'}, 'services': {}},
1701
-        ):
1982
+                )
1983
+            )
1702 1984
 
1703 1985
             def func(
1704 1986
                 *_args: Any,
... ...
@@ -1719,17 +2001,23 @@ contents go here
1719 2001
 
1720 2002
     def test_225h_store_config_fail_manual_ssh_agent_runtime_error(
1721 2003
         self,
1722
-        monkeypatch: pytest.MonkeyPatch,
1723 2004
         running_ssh_agent: tests.RunningSSHAgentInfo,
1724 2005
     ) -> None:
1725 2006
         """The SSH agent erroring during `--config --key` fails."""
1726 2007
         del running_ssh_agent
1727 2008
         runner = click.testing.CliRunner(mix_stderr=False)
1728
-        with tests.isolated_vault_config(
2009
+        # TODO(the-13th-letter): Rewrite using parenthesized
2010
+        # with-statements.
2011
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2012
+        with contextlib.ExitStack() as stack:
2013
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2014
+            stack.enter_context(
2015
+                tests.isolated_vault_config(
1729 2016
                     monkeypatch=monkeypatch,
1730 2017
                     runner=runner,
1731 2018
                     vault_config={'global': {'phrase': 'abc'}, 'services': {}},
1732
-        ):
2019
+                )
2020
+            )
1733 2021
 
1734 2022
             def raiser(*_args: Any, **_kwargs: Any) -> None:
1735 2023
                 raise ssh_agent.TrailingDataError()
... ...
@@ -1747,17 +2035,23 @@ contents go here
1747 2035
 
1748 2036
     def test_225i_store_config_fail_manual_ssh_agent_refuses(
1749 2037
         self,
1750
-        monkeypatch: pytest.MonkeyPatch,
1751 2038
         running_ssh_agent: tests.RunningSSHAgentInfo,
1752 2039
     ) -> None:
1753 2040
         """The SSH agent refusing during `--config --key` fails."""
1754 2041
         del running_ssh_agent
1755 2042
         runner = click.testing.CliRunner(mix_stderr=False)
1756
-        with tests.isolated_vault_config(
2043
+        # TODO(the-13th-letter): Rewrite using parenthesized
2044
+        # with-statements.
2045
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2046
+        with contextlib.ExitStack() as stack:
2047
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2048
+            stack.enter_context(
2049
+                tests.isolated_vault_config(
1757 2050
                     monkeypatch=monkeypatch,
1758 2051
                     runner=runner,
1759 2052
                     vault_config={'global': {'phrase': 'abc'}, 'services': {}},
1760
-        ):
2053
+                )
2054
+            )
1761 2055
 
1762 2056
             def func(*_args: Any, **_kwargs: Any) -> NoReturn:
1763 2057
                 raise ssh_agent.SSHAgentFailedError(
... ...
@@ -1775,13 +2069,20 @@ contents go here
1775 2069
             'expected error exit and known error message'
1776 2070
         )
1777 2071
 
1778
-    def test_226_no_arguments(self, monkeypatch: pytest.MonkeyPatch) -> None:
2072
+    def test_226_no_arguments(self) -> None:
1779 2073
         """Calling `derivepassphrase vault` without any arguments fails."""
1780 2074
         runner = click.testing.CliRunner(mix_stderr=False)
1781
-        with tests.isolated_config(
2075
+        # TODO(the-13th-letter): Rewrite using parenthesized
2076
+        # with-statements.
2077
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2078
+        with contextlib.ExitStack() as stack:
2079
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2080
+            stack.enter_context(
2081
+                tests.isolated_config(
1782 2082
                     monkeypatch=monkeypatch,
1783 2083
                     runner=runner,
1784
-        ):
2084
+                )
2085
+            )
1785 2086
             result_ = runner.invoke(
1786 2087
                 cli.derivepassphrase_vault, [], catch_exceptions=False
1787 2088
             )
... ...
@@ -1791,14 +2092,21 @@ contents go here
1791 2092
         ), 'expected error exit and known error message'
1792 2093
 
1793 2094
     def test_226a_no_passphrase_or_key(
1794
-        self, monkeypatch: pytest.MonkeyPatch
2095
+        self,
1795 2096
     ) -> None:
1796 2097
         """Deriving a passphrase without a passphrase or key fails."""
1797 2098
         runner = click.testing.CliRunner(mix_stderr=False)
1798
-        with tests.isolated_config(
2099
+        # TODO(the-13th-letter): Rewrite using parenthesized
2100
+        # with-statements.
2101
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2102
+        with contextlib.ExitStack() as stack:
2103
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2104
+            stack.enter_context(
2105
+                tests.isolated_config(
1799 2106
                     monkeypatch=monkeypatch,
1800 2107
                     runner=runner,
1801
-        ):
2108
+                )
2109
+            )
1802 2110
             result_ = runner.invoke(
1803 2111
                 cli.derivepassphrase_vault,
1804 2112
                 ['--', DUMMY_SERVICE],
... ...
@@ -1810,7 +2118,7 @@ contents go here
1810 2118
         )
1811 2119
 
1812 2120
     def test_230_config_directory_nonexistant(
1813
-        self, monkeypatch: pytest.MonkeyPatch
2121
+        self,
1814 2122
     ) -> None:
1815 2123
         """Running without an existing config directory works.
1816 2124
 
... ...
@@ -1820,10 +2128,17 @@ contents go here
1820 2128
 
1821 2129
         """
1822 2130
         runner = click.testing.CliRunner(mix_stderr=False)
1823
-        with tests.isolated_config(
2131
+        # TODO(the-13th-letter): Rewrite using parenthesized
2132
+        # with-statements.
2133
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2134
+        with contextlib.ExitStack() as stack:
2135
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2136
+            stack.enter_context(
2137
+                tests.isolated_config(
1824 2138
                     monkeypatch=monkeypatch,
1825 2139
                     runner=runner,
1826
-        ):
2140
+                )
2141
+            )
1827 2142
             with contextlib.suppress(FileNotFoundError):
1828 2143
                 shutil.rmtree(cli._config_filename(subsystem=None))
1829 2144
             result_ = runner.invoke(
... ...
@@ -1847,7 +2162,7 @@ contents go here
1847 2162
             }, 'config mismatch'
1848 2163
 
1849 2164
     def test_230a_config_directory_not_a_file(
1850
-        self, monkeypatch: pytest.MonkeyPatch
2165
+        self,
1851 2166
     ) -> None:
1852 2167
         """Erroring without an existing config directory errors normally.
1853 2168
 
... ...
@@ -1860,10 +2175,17 @@ contents go here
1860 2175
 
1861 2176
         """
1862 2177
         runner = click.testing.CliRunner(mix_stderr=False)
1863
-        with tests.isolated_config(
2178
+        # TODO(the-13th-letter): Rewrite using parenthesized
2179
+        # with-statements.
2180
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2181
+        with contextlib.ExitStack() as stack:
2182
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2183
+            stack.enter_context(
2184
+                tests.isolated_config(
1864 2185
                     monkeypatch=monkeypatch,
1865 2186
                     runner=runner,
1866
-        ):
2187
+                )
2188
+            )
1867 2189
             save_config_ = cli._save_config
1868 2190
 
1869 2191
             def obstruct_config_saving(*args: Any, **kwargs: Any) -> Any:
... ...
@@ -1887,14 +2209,21 @@ contents go here
1887 2209
             )
1888 2210
 
1889 2211
     def test_230b_store_config_custom_error(
1890
-        self, monkeypatch: pytest.MonkeyPatch
2212
+        self,
1891 2213
     ) -> None:
1892 2214
         """Storing the configuration reacts even to weird errors."""
1893 2215
         runner = click.testing.CliRunner(mix_stderr=False)
1894
-        with tests.isolated_config(
2216
+        # TODO(the-13th-letter): Rewrite using parenthesized
2217
+        # with-statements.
2218
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2219
+        with contextlib.ExitStack() as stack:
2220
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2221
+            stack.enter_context(
2222
+                tests.isolated_config(
1895 2223
                     monkeypatch=monkeypatch,
1896 2224
                     runner=runner,
1897
-        ):
2225
+                )
2226
+            )
1898 2227
             custom_error = 'custom error message'
1899 2228
 
1900 2229
             def raiser(config: Any) -> None:
... ...
@@ -2014,7 +2343,6 @@ contents go here
2014 2343
     )
2015 2344
     def test_300_unicode_normalization_form_warning(
2016 2345
         self,
2017
-        monkeypatch: pytest.MonkeyPatch,
2018 2346
         caplog: pytest.LogCaptureFixture,
2019 2347
         main_config: str,
2020 2348
         command_line: list[str],
... ...
@@ -2023,14 +2351,23 @@ contents go here
2023 2351
     ) -> None:
2024 2352
         """Using unnormalized Unicode passphrases warns."""
2025 2353
         runner = click.testing.CliRunner(mix_stderr=False)
2026
-        with tests.isolated_vault_config(
2354
+        # TODO(the-13th-letter): Rewrite using parenthesized
2355
+        # with-statements.
2356
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2357
+        with contextlib.ExitStack() as stack:
2358
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2359
+            stack.enter_context(
2360
+                tests.isolated_vault_config(
2027 2361
                     monkeypatch=monkeypatch,
2028 2362
                     runner=runner,
2029 2363
                     vault_config={
2030
-                'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()}
2364
+                        'services': {
2365
+                            DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()
2366
+                        }
2031 2367
                     },
2032 2368
                     main_config_str=main_config,
2033
-        ):
2369
+                )
2370
+            )
2034 2371
             result_ = runner.invoke(
2035 2372
                 cli.derivepassphrase_vault,
2036 2373
                 ['--debug', *command_line],
... ...
@@ -2086,7 +2423,6 @@ contents go here
2086 2423
     )
2087 2424
     def test_301_unicode_normalization_form_error(
2088 2425
         self,
2089
-        monkeypatch: pytest.MonkeyPatch,
2090 2426
         main_config: str,
2091 2427
         command_line: list[str],
2092 2428
         input: str | None,
... ...
@@ -2094,14 +2430,23 @@ contents go here
2094 2430
     ) -> None:
2095 2431
         """Using unknown Unicode normalization forms fails."""
2096 2432
         runner = click.testing.CliRunner(mix_stderr=False)
2097
-        with tests.isolated_vault_config(
2433
+        # TODO(the-13th-letter): Rewrite using parenthesized
2434
+        # with-statements.
2435
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2436
+        with contextlib.ExitStack() as stack:
2437
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2438
+            stack.enter_context(
2439
+                tests.isolated_vault_config(
2098 2440
                     monkeypatch=monkeypatch,
2099 2441
                     runner=runner,
2100 2442
                     vault_config={
2101
-                'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()}
2443
+                        'services': {
2444
+                            DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()
2445
+                        }
2102 2446
                     },
2103 2447
                     main_config_str=main_config,
2104
-        ):
2448
+                )
2449
+            )
2105 2450
             result_ = runner.invoke(
2106 2451
                 cli.derivepassphrase_vault,
2107 2452
                 command_line,
... ...
@@ -2131,22 +2476,29 @@ contents go here
2131 2476
     )
2132 2477
     def test_301a_unicode_normalization_form_error_from_stored_config(
2133 2478
         self,
2134
-        monkeypatch: pytest.MonkeyPatch,
2135 2479
         command_line: list[str],
2136 2480
     ) -> None:
2137 2481
         """Using unknown Unicode normalization forms in the config fails."""
2138 2482
         runner = click.testing.CliRunner(mix_stderr=False)
2139
-        with tests.isolated_vault_config(
2483
+        # TODO(the-13th-letter): Rewrite using parenthesized
2484
+        # with-statements.
2485
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2486
+        with contextlib.ExitStack() as stack:
2487
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2488
+            stack.enter_context(
2489
+                tests.isolated_vault_config(
2140 2490
                     monkeypatch=monkeypatch,
2141 2491
                     runner=runner,
2142 2492
                     vault_config={
2143
-                'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()}
2493
+                        'services': {
2494
+                            DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()
2495
+                        }
2144 2496
                     },
2145
-            main_config_str=textwrap.dedent("""
2146
-            [vault]
2147
-            default-unicode-normalization-form = 'XXX'
2148
-            """),
2149
-        ):
2497
+                    main_config_str=(
2498
+                        "[vault]\ndefault-unicode-normalization-form = 'XXX'\n"
2499
+                    ),
2500
+                )
2501
+            )
2150 2502
             result_ = runner.invoke(
2151 2503
                 cli.derivepassphrase_vault,
2152 2504
                 command_line,
... ...
@@ -2166,18 +2518,22 @@ contents go here
2166 2518
 
2167 2519
     def test_310_bad_user_config_file(
2168 2520
         self,
2169
-        monkeypatch: pytest.MonkeyPatch,
2170 2521
     ) -> None:
2171 2522
         """Loading a user configuration file in an invalid format fails."""
2172 2523
         runner = click.testing.CliRunner(mix_stderr=False)
2173
-        with tests.isolated_vault_config(
2524
+        # TODO(the-13th-letter): Rewrite using parenthesized
2525
+        # with-statements.
2526
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2527
+        with contextlib.ExitStack() as stack:
2528
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2529
+            stack.enter_context(
2530
+                tests.isolated_vault_config(
2174 2531
                     monkeypatch=monkeypatch,
2175 2532
                     runner=runner,
2176 2533
                     vault_config={'services': {}},
2177
-            main_config_str=textwrap.dedent("""
2178
-            This file is not valid TOML.
2179
-            """),
2180
-        ):
2534
+                    main_config_str='This file is not valid TOML.\n',
2535
+                )
2536
+            )
2181 2537
             result_ = runner.invoke(
2182 2538
                 cli.derivepassphrase_vault,
2183 2539
                 ['--phrase', '--', DUMMY_SERVICE],
... ...
@@ -2191,15 +2547,21 @@ contents go here
2191 2547
 
2192 2548
     def test_400_missing_af_unix_support(
2193 2549
         self,
2194
-        monkeypatch: pytest.MonkeyPatch,
2195 2550
     ) -> None:
2196 2551
         """Querying the SSH agent without `AF_UNIX` support fails."""
2197 2552
         runner = click.testing.CliRunner(mix_stderr=False)
2198
-        with tests.isolated_vault_config(
2553
+        # TODO(the-13th-letter): Rewrite using parenthesized
2554
+        # with-statements.
2555
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2556
+        with contextlib.ExitStack() as stack:
2557
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2558
+            stack.enter_context(
2559
+                tests.isolated_vault_config(
2199 2560
                     monkeypatch=monkeypatch,
2200 2561
                     runner=runner,
2201 2562
                     vault_config={'global': {'phrase': 'abc'}, 'services': {}},
2202
-        ):
2563
+                )
2564
+            )
2203 2565
             monkeypatch.setenv(
2204 2566
                 'SSH_AUTH_SOCK', "the value doesn't even matter"
2205 2567
             )
... ...
@@ -2238,30 +2600,43 @@ class TestCLIUtils:
2238 2600
         ],
2239 2601
     )
2240 2602
     def test_100_load_config(
2241
-        self, monkeypatch: pytest.MonkeyPatch, config: Any
2603
+        self,
2604
+        config: Any,
2242 2605
     ) -> None:
2243 2606
         """`cli._load_config` works for valid configurations."""
2244
-        runner = click.testing.CliRunner()
2245
-        with tests.isolated_vault_config(
2246
-            monkeypatch=monkeypatch, runner=runner, vault_config=config
2247
-        ):
2607
+        runner = click.testing.CliRunner(mix_stderr=False)
2608
+        # TODO(the-13th-letter): Rewrite using parenthesized
2609
+        # with-statements.
2610
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2611
+        with contextlib.ExitStack() as stack:
2612
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2613
+            stack.enter_context(
2614
+                tests.isolated_vault_config(
2615
+                    monkeypatch=monkeypatch,
2616
+                    runner=runner,
2617
+                    vault_config=config,
2618
+                )
2619
+            )
2248 2620
             config_filename = cli._config_filename(subsystem='vault')
2249 2621
             with config_filename.open(encoding='UTF-8') as fileobj:
2250 2622
                 assert json.load(fileobj) == config
2251 2623
             assert cli._load_config() == config
2252 2624
 
2253 2625
     def test_110_save_bad_config(
2254
-        self, monkeypatch: pytest.MonkeyPatch
2626
+        self,
2255 2627
     ) -> None:
2256 2628
         """`cli._save_config` fails for bad configurations."""
2257
-        runner = click.testing.CliRunner()
2629
+        runner = click.testing.CliRunner(mix_stderr=False)
2258 2630
         # TODO(the-13th-letter): Rewrite using parenthesized
2259 2631
         # with-statements.
2260 2632
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2261 2633
         with contextlib.ExitStack() as stack:
2634
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2262 2635
             stack.enter_context(
2263 2636
                 tests.isolated_vault_config(
2264
-                    monkeypatch=monkeypatch, runner=runner, vault_config={}
2637
+                    monkeypatch=monkeypatch,
2638
+                    runner=runner,
2639
+                    vault_config={},
2265 2640
                 )
2266 2641
             )
2267 2642
             stack.enter_context(
... ...
@@ -2392,9 +2769,10 @@ Boo.
2392 2769
         ), 'expected known output'
2393 2770
 
2394 2771
     def test_113_prompt_for_passphrase(
2395
-        self, monkeypatch: pytest.MonkeyPatch
2772
+        self,
2396 2773
     ) -> None:
2397 2774
         """`cli._prompt_for_passphrase` works."""
2775
+        with pytest.MonkeyPatch.context() as monkeypatch:
2398 2776
             monkeypatch.setattr(
2399 2777
                 click,
2400 2778
                 'prompt',
... ...
@@ -2553,12 +2931,18 @@ Boo.
2553 2931
             )
2554 2932
             script = outfile.getvalue()
2555 2933
         runner = click.testing.CliRunner(mix_stderr=False)
2556
-        monkeypatch = pytest.MonkeyPatch()
2557
-        with tests.isolated_vault_config(
2558
-            runner=runner,
2934
+        # TODO(the-13th-letter): Rewrite using parenthesized
2935
+        # with-statements.
2936
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
2937
+        with contextlib.ExitStack() as stack:
2938
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
2939
+            stack.enter_context(
2940
+                tests.isolated_vault_config(
2559 2941
                     monkeypatch=monkeypatch,
2942
+                    runner=runner,
2560 2943
                     vault_config={'services': {}},
2561
-        ):
2944
+                )
2945
+            )
2562 2946
             for result_ in vault_config_exporter_shell_interpreter(script):
2563 2947
                 result = tests.ReadableResult.parse(result_)
2564 2948
                 assert result.clean_exit()
... ...
@@ -2806,19 +3190,25 @@ Boo.
2806 3190
     )
2807 3191
     def test_203_repeated_config_deletion(
2808 3192
         self,
2809
-        monkeypatch: pytest.MonkeyPatch,
2810 3193
         command_line: list[str],
2811 3194
         config: _types.VaultConfig,
2812 3195
         result_config: _types.VaultConfig,
2813 3196
     ) -> None:
2814 3197
         """Repeatedly removing the same parts of a configuration works."""
2815
-        runner = click.testing.CliRunner(mix_stderr=False)
2816 3198
         for start_config in [config, result_config]:
2817
-            with tests.isolated_vault_config(
3199
+            runner = click.testing.CliRunner(mix_stderr=False)
3200
+            # TODO(the-13th-letter): Rewrite using parenthesized
3201
+            # with-statements.
3202
+            # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3203
+            with contextlib.ExitStack() as stack:
3204
+                monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3205
+                stack.enter_context(
3206
+                    tests.isolated_vault_config(
2818 3207
                         monkeypatch=monkeypatch,
2819 3208
                         runner=runner,
2820 3209
                         vault_config=start_config,
2821
-            ):
3210
+                    )
3211
+                )
2822 3212
                 result_ = runner.invoke(
2823 3213
                     cli.derivepassphrase_vault,
2824 3214
                     command_line,
... ...
@@ -2863,12 +3253,11 @@ Boo.
2863 3253
     @pytest.mark.parametrize('conn_hint', ['none', 'socket', 'client'])
2864 3254
     def test_227_get_suitable_ssh_keys(
2865 3255
         self,
2866
-        monkeypatch: pytest.MonkeyPatch,
2867 3256
         running_ssh_agent: tests.RunningSSHAgentInfo,
2868 3257
         conn_hint: str,
2869 3258
     ) -> None:
2870 3259
         """`cli._get_suitable_ssh_keys` works."""
2871
-        with monkeypatch.context():
3260
+        with pytest.MonkeyPatch.context() as monkeypatch:
2872 3261
             monkeypatch.setenv('SSH_AUTH_SOCK', running_ssh_agent.socket)
2873 3262
             monkeypatch.setattr(
2874 3263
                 ssh_agent.SSHAgentClient, 'list_keys', tests.list_keys
... ...
@@ -2899,7 +3288,6 @@ Boo.
2899 3288
 
2900 3289
     def test_400_key_to_phrase(
2901 3290
         self,
2902
-        monkeypatch: pytest.MonkeyPatch,
2903 3291
         skip_if_no_af_unix_support: None,
2904 3292
         ssh_agent_client_with_test_keys_loaded: ssh_agent.SSHAgentClient,
2905 3293
     ) -> None:
... ...
@@ -2924,8 +3312,11 @@ Boo.
2924 3312
             raise ssh_agent.TrailingDataError()
2925 3313
 
2926 3314
         del skip_if_no_af_unix_support
3315
+        with pytest.MonkeyPatch.context() as monkeypatch:
2927 3316
             monkeypatch.setattr(ssh_agent.SSHAgentClient, 'sign', fail)
2928
-        loaded_keys = list(ssh_agent_client_with_test_keys_loaded.list_keys())
3317
+            loaded_keys = list(
3318
+                ssh_agent_client_with_test_keys_loaded.list_keys()
3319
+            )
2929 3320
             loaded_key = base64.standard_b64encode(loaded_keys[0][0])
2930 3321
             with monkeypatch.context() as mp:
2931 3322
                 mp.setattr(
... ...
@@ -2933,7 +3324,9 @@ Boo.
2933 3324
                     'list_keys',
2934 3325
                     lambda *_a, **_kw: [],
2935 3326
                 )
2936
-            with pytest.raises(ErrCallback, match='not loaded into the agent'):
3327
+                with pytest.raises(
3328
+                    ErrCallback, match='not loaded into the agent'
3329
+                ):
2937 3330
                     cli._key_to_phrase(loaded_key, error_callback=err)
2938 3331
             with monkeypatch.context() as mp:
2939 3332
                 mp.setattr(ssh_agent.SSHAgentClient, 'list_keys', fail)
... ...
@@ -2988,13 +3381,20 @@ Boo.
2988 3381
 class TestCLITransition:
2989 3382
     """Transition tests for the command-line interface up to v1.0."""
2990 3383
 
2991
-    def test_100_help_output(self, monkeypatch: pytest.MonkeyPatch) -> None:
3384
+    def test_100_help_output(self) -> None:
2992 3385
         """The top-level help text mentions subcommands."""
2993 3386
         runner = click.testing.CliRunner(mix_stderr=False)
2994
-        with tests.isolated_config(
3387
+        # TODO(the-13th-letter): Rewrite using parenthesized
3388
+        # with-statements.
3389
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3390
+        with contextlib.ExitStack() as stack:
3391
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3392
+            stack.enter_context(
3393
+                tests.isolated_config(
2995 3394
                     monkeypatch=monkeypatch,
2996 3395
                     runner=runner,
2997
-        ):
3396
+                )
3397
+            )
2998 3398
             result_ = runner.invoke(
2999 3399
                 cli.derivepassphrase, ['--help'], catch_exceptions=False
3000 3400
             )
... ...
@@ -3004,14 +3404,21 @@ class TestCLITransition:
3004 3404
         ), 'expected clean exit, and known help text'
3005 3405
 
3006 3406
     def test_101_help_output_export(
3007
-        self, monkeypatch: pytest.MonkeyPatch
3407
+        self,
3008 3408
     ) -> None:
3009 3409
         """The "export" subcommand help text mentions subcommands."""
3010 3410
         runner = click.testing.CliRunner(mix_stderr=False)
3011
-        with tests.isolated_config(
3411
+        # TODO(the-13th-letter): Rewrite using parenthesized
3412
+        # with-statements.
3413
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3414
+        with contextlib.ExitStack() as stack:
3415
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3416
+            stack.enter_context(
3417
+                tests.isolated_config(
3012 3418
                     monkeypatch=monkeypatch,
3013 3419
                     runner=runner,
3014
-        ):
3420
+                )
3421
+            )
3015 3422
             result_ = runner.invoke(
3016 3423
                 cli.derivepassphrase,
3017 3424
                 ['export', '--help'],
... ...
@@ -3023,14 +3430,21 @@ class TestCLITransition:
3023 3430
         ), 'expected clean exit, and known help text'
3024 3431
 
3025 3432
     def test_102_help_output_export_vault(
3026
-        self, monkeypatch: pytest.MonkeyPatch
3433
+        self,
3027 3434
     ) -> None:
3028 3435
         """The "export vault" subcommand help text has known content."""
3029 3436
         runner = click.testing.CliRunner(mix_stderr=False)
3030
-        with tests.isolated_config(
3437
+        # TODO(the-13th-letter): Rewrite using parenthesized
3438
+        # with-statements.
3439
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3440
+        with contextlib.ExitStack() as stack:
3441
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3442
+            stack.enter_context(
3443
+                tests.isolated_config(
3031 3444
                     monkeypatch=monkeypatch,
3032 3445
                     runner=runner,
3033
-        ):
3446
+                )
3447
+            )
3034 3448
             result_ = runner.invoke(
3035 3449
                 cli.derivepassphrase,
3036 3450
                 ['export', 'vault', '--help'],
... ...
@@ -3042,14 +3456,21 @@ class TestCLITransition:
3042 3456
         ), 'expected clean exit, and known help text'
3043 3457
 
3044 3458
     def test_103_help_output_vault(
3045
-        self, monkeypatch: pytest.MonkeyPatch
3459
+        self,
3046 3460
     ) -> None:
3047 3461
         """The "vault" subcommand help text has known content."""
3048 3462
         runner = click.testing.CliRunner(mix_stderr=False)
3049
-        with tests.isolated_config(
3463
+        # TODO(the-13th-letter): Rewrite using parenthesized
3464
+        # with-statements.
3465
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3466
+        with contextlib.ExitStack() as stack:
3467
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3468
+            stack.enter_context(
3469
+                tests.isolated_config(
3050 3470
                     monkeypatch=monkeypatch,
3051 3471
                     runner=runner,
3052
-        ):
3472
+                )
3473
+            )
3053 3474
             result_ = runner.invoke(
3054 3475
                 cli.derivepassphrase,
3055 3476
                 ['vault', '--help'],
... ...
@@ -3083,11 +3504,22 @@ class TestCLITransition:
3083 3504
         ],
3084 3505
     )
3085 3506
     def test_110_load_config_backup(
3086
-        self, monkeypatch: pytest.MonkeyPatch, config: Any
3507
+        self,
3508
+        config: Any,
3087 3509
     ) -> None:
3088 3510
         """Loading the old settings file works."""
3089
-        runner = click.testing.CliRunner()
3090
-        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner):
3511
+        runner = click.testing.CliRunner(mix_stderr=False)
3512
+        # TODO(the-13th-letter): Rewrite using parenthesized
3513
+        # with-statements.
3514
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3515
+        with contextlib.ExitStack() as stack:
3516
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3517
+            stack.enter_context(
3518
+                tests.isolated_config(
3519
+                    monkeypatch=monkeypatch,
3520
+                    runner=runner,
3521
+                )
3522
+            )
3091 3523
             cli._config_filename(subsystem='old settings.json').write_text(
3092 3524
                 json.dumps(config, indent=2) + '\n', encoding='UTF-8'
3093 3525
             )
... ...
@@ -3113,11 +3545,22 @@ class TestCLITransition:
3113 3545
         ],
3114 3546
     )
3115 3547
     def test_111_migrate_config(
3116
-        self, monkeypatch: pytest.MonkeyPatch, config: Any
3548
+        self,
3549
+        config: Any,
3117 3550
     ) -> None:
3118 3551
         """Migrating the old settings file works."""
3119
-        runner = click.testing.CliRunner()
3120
-        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner):
3552
+        runner = click.testing.CliRunner(mix_stderr=False)
3553
+        # TODO(the-13th-letter): Rewrite using parenthesized
3554
+        # with-statements.
3555
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3556
+        with contextlib.ExitStack() as stack:
3557
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3558
+            stack.enter_context(
3559
+                tests.isolated_config(
3560
+                    monkeypatch=monkeypatch,
3561
+                    runner=runner,
3562
+                )
3563
+            )
3121 3564
             cli._config_filename(subsystem='old settings.json').write_text(
3122 3565
                 json.dumps(config, indent=2) + '\n', encoding='UTF-8'
3123 3566
             )
... ...
@@ -3143,11 +3586,22 @@ class TestCLITransition:
3143 3586
         ],
3144 3587
     )
3145 3588
     def test_112_migrate_config_error(
3146
-        self, monkeypatch: pytest.MonkeyPatch, config: Any
3589
+        self,
3590
+        config: Any,
3147 3591
     ) -> None:
3148 3592
         """Migrating the old settings file atop a directory fails."""
3149
-        runner = click.testing.CliRunner()
3150
-        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner):
3593
+        runner = click.testing.CliRunner(mix_stderr=False)
3594
+        # TODO(the-13th-letter): Rewrite using parenthesized
3595
+        # with-statements.
3596
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3597
+        with contextlib.ExitStack() as stack:
3598
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3599
+            stack.enter_context(
3600
+                tests.isolated_config(
3601
+                    monkeypatch=monkeypatch,
3602
+                    runner=runner,
3603
+                )
3604
+            )
3151 3605
             cli._config_filename(subsystem='old settings.json').write_text(
3152 3606
                 json.dumps(config, indent=2) + '\n', encoding='UTF-8'
3153 3607
             )
... ...
@@ -3179,11 +3633,22 @@ class TestCLITransition:
3179 3633
         ],
3180 3634
     )
3181 3635
     def test_113_migrate_config_error_bad_config_value(
3182
-        self, monkeypatch: pytest.MonkeyPatch, config: Any
3636
+        self,
3637
+        config: Any,
3183 3638
     ) -> None:
3184 3639
         """Migrating an invalid old settings file fails."""
3185
-        runner = click.testing.CliRunner()
3186
-        with tests.isolated_config(monkeypatch=monkeypatch, runner=runner):
3640
+        runner = click.testing.CliRunner(mix_stderr=False)
3641
+        # TODO(the-13th-letter): Rewrite using parenthesized
3642
+        # with-statements.
3643
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3644
+        with contextlib.ExitStack() as stack:
3645
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3646
+            stack.enter_context(
3647
+                tests.isolated_config(
3648
+                    monkeypatch=monkeypatch,
3649
+                    runner=runner,
3650
+                )
3651
+            )
3187 3652
             cli._config_filename(subsystem='old settings.json').write_text(
3188 3653
                 json.dumps(config, indent=2) + '\n', encoding='UTF-8'
3189 3654
             )
... ...
@@ -3192,18 +3657,24 @@ class TestCLITransition:
3192 3657
 
3193 3658
     def test_200_forward_export_vault_path_parameter(
3194 3659
         self,
3195
-        monkeypatch: pytest.MonkeyPatch,
3196 3660
         caplog: pytest.LogCaptureFixture,
3197 3661
     ) -> None:
3198 3662
         """Forwarding arguments from "export" to "export vault" works."""
3199 3663
         pytest.importorskip('cryptography', minversion='38.0')
3200 3664
         runner = click.testing.CliRunner(mix_stderr=False)
3201
-        with tests.isolated_vault_exporter_config(
3665
+        # TODO(the-13th-letter): Rewrite using parenthesized
3666
+        # with-statements.
3667
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3668
+        with contextlib.ExitStack() as stack:
3669
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3670
+            stack.enter_context(
3671
+                tests.isolated_vault_exporter_config(
3202 3672
                     monkeypatch=monkeypatch,
3203 3673
                     runner=runner,
3204 3674
                     vault_config=tests.VAULT_V03_CONFIG,
3205 3675
                     vault_key=tests.VAULT_MASTER_KEY,
3206
-        ):
3676
+                )
3677
+            )
3207 3678
             monkeypatch.setenv('VAULT_KEY', tests.VAULT_MASTER_KEY)
3208 3679
             result_ = runner.invoke(
3209 3680
                 cli.derivepassphrase,
... ...
@@ -3221,16 +3692,22 @@ class TestCLITransition:
3221 3692
 
3222 3693
     def test_201_forward_export_vault_empty_commandline(
3223 3694
         self,
3224
-        monkeypatch: pytest.MonkeyPatch,
3225 3695
         caplog: pytest.LogCaptureFixture,
3226 3696
     ) -> None:
3227 3697
         """Deferring from "export" to "export vault" works."""
3228 3698
         pytest.importorskip('cryptography', minversion='38.0')
3229 3699
         runner = click.testing.CliRunner(mix_stderr=False)
3230
-        with tests.isolated_config(
3700
+        # TODO(the-13th-letter): Rewrite using parenthesized
3701
+        # with-statements.
3702
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3703
+        with contextlib.ExitStack() as stack:
3704
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3705
+            stack.enter_context(
3706
+                tests.isolated_config(
3231 3707
                     monkeypatch=monkeypatch,
3232 3708
                     runner=runner,
3233
-        ):
3709
+                )
3710
+            )
3234 3711
             result_ = runner.invoke(
3235 3712
                 cli.derivepassphrase,
3236 3713
                 ['export'],
... ...
@@ -3251,19 +3728,27 @@ class TestCLITransition:
3251 3728
     )
3252 3729
     def test_210_forward_vault_disable_character_set(
3253 3730
         self,
3254
-        monkeypatch: pytest.MonkeyPatch,
3255 3731
         caplog: pytest.LogCaptureFixture,
3256 3732
         charset_name: str,
3257 3733
     ) -> None:
3258 3734
         """Forwarding arguments from top-level to "vault" works."""
3259
-        monkeypatch.setattr(cli, '_prompt_for_passphrase', tests.auto_prompt)
3260 3735
         option = f'--{charset_name}'
3261 3736
         charset = vault.Vault._CHARSETS[charset_name].decode('ascii')
3262 3737
         runner = click.testing.CliRunner(mix_stderr=False)
3263
-        with tests.isolated_config(
3738
+        # TODO(the-13th-letter): Rewrite using parenthesized
3739
+        # with-statements.
3740
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3741
+        with contextlib.ExitStack() as stack:
3742
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3743
+            stack.enter_context(
3744
+                tests.isolated_config(
3264 3745
                     monkeypatch=monkeypatch,
3265 3746
                     runner=runner,
3266
-        ):
3747
+                )
3748
+            )
3749
+            monkeypatch.setattr(
3750
+                cli, '_prompt_for_passphrase', tests.auto_prompt
3751
+            )
3267 3752
             result_ = runner.invoke(
3268 3753
                 cli.derivepassphrase,
3269 3754
                 [option, '0', '-p', '--', DUMMY_SERVICE],
... ...
@@ -3285,15 +3770,21 @@ class TestCLITransition:
3285 3770
 
3286 3771
     def test_211_forward_vault_empty_command_line(
3287 3772
         self,
3288
-        monkeypatch: pytest.MonkeyPatch,
3289 3773
         caplog: pytest.LogCaptureFixture,
3290 3774
     ) -> None:
3291 3775
         """Deferring from top-level to "vault" works."""
3292 3776
         runner = click.testing.CliRunner(mix_stderr=False)
3293
-        with tests.isolated_config(
3777
+        # TODO(the-13th-letter): Rewrite using parenthesized
3778
+        # with-statements.
3779
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3780
+        with contextlib.ExitStack() as stack:
3781
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3782
+            stack.enter_context(
3783
+                tests.isolated_config(
3294 3784
                     monkeypatch=monkeypatch,
3295 3785
                     runner=runner,
3296
-        ):
3786
+                )
3787
+            )
3297 3788
             result_ = runner.invoke(
3298 3789
                 cli.derivepassphrase,
3299 3790
                 [],
... ...
@@ -3313,16 +3804,22 @@ class TestCLITransition:
3313 3804
 
3314 3805
     def test_300_export_using_old_config_file(
3315 3806
         self,
3316
-        monkeypatch: pytest.MonkeyPatch,
3317 3807
         caplog: pytest.LogCaptureFixture,
3318 3808
     ) -> None:
3319 3809
         """Exporting from (and migrating) the old settings file works."""
3320 3810
         caplog.set_level(logging.INFO)
3321 3811
         runner = click.testing.CliRunner(mix_stderr=False)
3322
-        with tests.isolated_config(
3812
+        # TODO(the-13th-letter): Rewrite using parenthesized
3813
+        # with-statements.
3814
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3815
+        with contextlib.ExitStack() as stack:
3816
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3817
+            stack.enter_context(
3818
+                tests.isolated_config(
3323 3819
                     monkeypatch=monkeypatch,
3324 3820
                     runner=runner,
3325
-        ):
3821
+                )
3822
+            )
3326 3823
             cli._config_filename(subsystem='old settings.json').write_text(
3327 3824
                 json.dumps(
3328 3825
                     {'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}},
... ...
@@ -3347,15 +3844,21 @@ class TestCLITransition:
3347 3844
 
3348 3845
     def test_300a_export_using_old_config_file_migration_error(
3349 3846
         self,
3350
-        monkeypatch: pytest.MonkeyPatch,
3351 3847
         caplog: pytest.LogCaptureFixture,
3352 3848
     ) -> None:
3353 3849
         """Exporting from (and not migrating) the old settings file fails."""
3354 3850
         runner = click.testing.CliRunner(mix_stderr=False)
3355
-        with tests.isolated_config(
3851
+        # TODO(the-13th-letter): Rewrite using parenthesized
3852
+        # with-statements.
3853
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3854
+        with contextlib.ExitStack() as stack:
3855
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3856
+            stack.enter_context(
3857
+                tests.isolated_config(
3356 3858
                     monkeypatch=monkeypatch,
3357 3859
                     runner=runner,
3358
-        ):
3860
+                )
3861
+            )
3359 3862
             cli._config_filename(subsystem='old settings.json').write_text(
3360 3863
                 json.dumps(
3361 3864
                     {'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}},
... ...
@@ -3390,16 +3893,22 @@ class TestCLITransition:
3390 3893
 
3391 3894
     def test_400_completion_service_name_old_config_file(
3392 3895
         self,
3393
-        monkeypatch: pytest.MonkeyPatch,
3394 3896
     ) -> None:
3395 3897
         """Completing service names from the old settings file works."""
3396
-        runner = click.testing.CliRunner(mix_stderr=False)
3397 3898
         config = {'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS.copy()}}
3398
-        with tests.isolated_vault_config(
3899
+        runner = click.testing.CliRunner(mix_stderr=False)
3900
+        # TODO(the-13th-letter): Rewrite using parenthesized
3901
+        # with-statements.
3902
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
3903
+        with contextlib.ExitStack() as stack:
3904
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
3905
+            stack.enter_context(
3906
+                tests.isolated_vault_config(
3399 3907
                     monkeypatch=monkeypatch,
3400 3908
                     runner=runner,
3401 3909
                     vault_config=config,
3402
-        ):
3910
+                )
3911
+            )
3403 3912
             old_name = cli._config_filename(subsystem='old settings.json')
3404 3913
             new_name = cli._config_filename(subsystem='vault')
3405 3914
             old_name.unlink(missing_ok=True)
... ...
@@ -4241,18 +4750,24 @@ class TestShellCompletion:
4241 4750
     )
4242 4751
     def test_203_service_names(
4243 4752
         self,
4244
-        monkeypatch: pytest.MonkeyPatch,
4245 4753
         config: _types.VaultConfig,
4246 4754
         incomplete: str,
4247 4755
         completions: AbstractSet[str],
4248 4756
     ) -> None:
4249 4757
         """Our completion machinery works for vault service names."""
4250 4758
         runner = click.testing.CliRunner(mix_stderr=False)
4251
-        with tests.isolated_vault_config(
4759
+        # TODO(the-13th-letter): Rewrite using parenthesized
4760
+        # with-statements.
4761
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
4762
+        with contextlib.ExitStack() as stack:
4763
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
4764
+            stack.enter_context(
4765
+                tests.isolated_vault_config(
4252 4766
                     monkeypatch=monkeypatch,
4253 4767
                     runner=runner,
4254 4768
                     vault_config=config,
4255
-        ):
4769
+                )
4770
+            )
4256 4771
             comp = self.Completions(['vault'], incomplete)
4257 4772
             assert frozenset(comp.get_words()) == completions
4258 4773
 
... ...
@@ -4359,7 +4874,6 @@ class TestShellCompletion:
4359 4874
     )
4360 4875
     def test_300_shell_completion_formatting(
4361 4876
         self,
4362
-        monkeypatch: pytest.MonkeyPatch,
4363 4877
         shell: str,
4364 4878
         format_func: Callable[[click.shell_completion.CompletionItem], str],
4365 4879
         config: _types.VaultConfig,
... ...
@@ -4373,11 +4887,18 @@ class TestShellCompletion:
4373 4887
     ) -> None:
4374 4888
         """Custom completion functions work for all shells."""
4375 4889
         runner = click.testing.CliRunner(mix_stderr=False)
4376
-        with tests.isolated_vault_config(
4890
+        # TODO(the-13th-letter): Rewrite using parenthesized
4891
+        # with-statements.
4892
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
4893
+        with contextlib.ExitStack() as stack:
4894
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
4895
+            stack.enter_context(
4896
+                tests.isolated_vault_config(
4377 4897
                     monkeypatch=monkeypatch,
4378 4898
                     runner=runner,
4379 4899
                     vault_config=config,
4380
-        ):
4900
+                )
4901
+            )
4381 4902
             expected_items = [assertable_item(item) for item in results]
4382 4903
             expected_string = '\n'.join(
4383 4904
                 format_func(completion_item(item)) for item in results
... ...
@@ -4566,7 +5087,6 @@ class TestShellCompletion:
4566 5087
     )
4567 5088
     def test_400_incompletable_service_names(
4568 5089
         self,
4569
-        monkeypatch: pytest.MonkeyPatch,
4570 5090
         caplog: pytest.LogCaptureFixture,
4571 5091
         mode: Literal['config', 'import'],
4572 5092
         config: _types.VaultConfig,
... ...
@@ -4575,13 +5095,20 @@ class TestShellCompletion:
4575 5095
         completions: AbstractSet[str],
4576 5096
     ) -> None:
4577 5097
         """Completion skips incompletable items."""
4578
-        runner = click.testing.CliRunner(mix_stderr=False)
4579 5098
         vault_config = config if mode == 'config' else {'services': {}}
4580
-        with tests.isolated_vault_config(
5099
+        runner = click.testing.CliRunner(mix_stderr=False)
5100
+        # TODO(the-13th-letter): Rewrite using parenthesized
5101
+        # with-statements.
5102
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
5103
+        with contextlib.ExitStack() as stack:
5104
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
5105
+            stack.enter_context(
5106
+                tests.isolated_vault_config(
4581 5107
                     monkeypatch=monkeypatch,
4582 5108
                     runner=runner,
4583 5109
                     vault_config=vault_config,
4584
-        ):
5110
+                )
5111
+            )
4585 5112
             if mode == 'config':
4586 5113
                 result_ = runner.invoke(
4587 5114
                     cli.derivepassphrase_vault,
... ...
@@ -4609,15 +5136,23 @@ class TestShellCompletion:
4609 5136
 
4610 5137
     def test_410a_service_name_exceptions_not_found(
4611 5138
         self,
4612
-        monkeypatch: pytest.MonkeyPatch,
4613 5139
     ) -> None:
4614 5140
         """Service name completion quietly fails on missing configuration."""
4615 5141
         runner = click.testing.CliRunner(mix_stderr=False)
4616
-        with tests.isolated_vault_config(
5142
+        # TODO(the-13th-letter): Rewrite using parenthesized
5143
+        # with-statements.
5144
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
5145
+        with contextlib.ExitStack() as stack:
5146
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
5147
+            stack.enter_context(
5148
+                tests.isolated_vault_config(
4617 5149
                     monkeypatch=monkeypatch,
4618 5150
                     runner=runner,
4619
-            vault_config={'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}},
4620
-        ):
5151
+                    vault_config={
5152
+                        'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}
5153
+                    },
5154
+                )
5155
+            )
4621 5156
             cli._config_filename(subsystem='vault').unlink(missing_ok=True)
4622 5157
             assert not cli._shell_complete_service(
4623 5158
                 click.Context(cli.derivepassphrase),
... ...
@@ -4628,16 +5163,24 @@ class TestShellCompletion:
4628 5163
     @pytest.mark.parametrize('exc_type', [RuntimeError, KeyError, ValueError])
4629 5164
     def test_410b_service_name_exceptions_custom_error(
4630 5165
         self,
4631
-        monkeypatch: pytest.MonkeyPatch,
4632 5166
         exc_type: type[Exception],
4633 5167
     ) -> None:
4634 5168
         """Service name completion quietly fails on configuration errors."""
4635 5169
         runner = click.testing.CliRunner(mix_stderr=False)
4636
-        with tests.isolated_vault_config(
5170
+        # TODO(the-13th-letter): Rewrite using parenthesized
5171
+        # with-statements.
5172
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
5173
+        with contextlib.ExitStack() as stack:
5174
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
5175
+            stack.enter_context(
5176
+                tests.isolated_vault_config(
4637 5177
                     monkeypatch=monkeypatch,
4638 5178
                     runner=runner,
4639
-            vault_config={'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}},
4640
-        ):
5179
+                    vault_config={
5180
+                        'services': {DUMMY_SERVICE: DUMMY_CONFIG_SETTINGS}
5181
+                    },
5182
+                )
5183
+            )
4641 5184
 
4642 5185
             def raiser(*_a: Any, **_kw: Any) -> NoReturn:
4643 5186
                 raise exc_type('just being difficult')  # noqa: EM101,TRY003
... ...
@@ -42,7 +42,7 @@ if TYPE_CHECKING:
42 42
 class TestCLI:
43 43
     """Test the command-line interface for `derivepassphrase export vault`."""
44 44
 
45
-    def test_200_path_parameter(self, monkeypatch: pytest.MonkeyPatch) -> None:
45
+    def test_200_path_parameter(self) -> None:
46 46
         """The path `VAULT_PATH` is supported.
47 47
 
48 48
         Using `VAULT_PATH` as the path looks up the actual path in the
... ...
@@ -51,12 +51,19 @@ class TestCLI:
51 51
 
52 52
         """
53 53
         runner = click.testing.CliRunner(mix_stderr=False)
54
-        with tests.isolated_vault_exporter_config(
54
+        # TODO(the-13th-letter): Rewrite using parenthesized
55
+        # with-statements.
56
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
57
+        with contextlib.ExitStack() as stack:
58
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
59
+            stack.enter_context(
60
+                tests.isolated_vault_exporter_config(
55 61
                     monkeypatch=monkeypatch,
56 62
                     runner=runner,
57 63
                     vault_config=tests.VAULT_V03_CONFIG,
58 64
                     vault_key=tests.VAULT_MASTER_KEY,
59
-        ):
65
+                )
66
+            )
60 67
             monkeypatch.setenv('VAULT_KEY', tests.VAULT_MASTER_KEY)
61 68
             result_ = runner.invoke(
62 69
                 cli.derivepassphrase_export_vault,
... ...
@@ -66,14 +73,21 @@ class TestCLI:
66 73
         assert result.clean_exit(empty_stderr=True), 'expected clean exit'
67 74
         assert json.loads(result.output) == tests.VAULT_V03_CONFIG_DATA
68 75
 
69
-    def test_201_key_parameter(self, monkeypatch: pytest.MonkeyPatch) -> None:
76
+    def test_201_key_parameter(self) -> None:
70 77
         """The `--key` option is supported."""
71 78
         runner = click.testing.CliRunner(mix_stderr=False)
72
-        with tests.isolated_vault_exporter_config(
79
+        # TODO(the-13th-letter): Rewrite using parenthesized
80
+        # with-statements.
81
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
82
+        with contextlib.ExitStack() as stack:
83
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
84
+            stack.enter_context(
85
+                tests.isolated_vault_exporter_config(
73 86
                     monkeypatch=monkeypatch,
74 87
                     runner=runner,
75 88
                     vault_config=tests.VAULT_V03_CONFIG,
76
-        ):
89
+                )
90
+            )
77 91
             result_ = runner.invoke(
78 92
                 cli.derivepassphrase_export_vault,
79 93
                 ['-k', tests.VAULT_MASTER_KEY, '.vault'],
... ...
@@ -107,7 +121,6 @@ class TestCLI:
107 121
     )
108 122
     def test_210_load_vault_v02_v03_storeroom(
109 123
         self,
110
-        monkeypatch: pytest.MonkeyPatch,
111 124
         format: str,
112 125
         config: str | bytes,
113 126
         config_data: dict[str, Any],
... ...
@@ -119,11 +132,18 @@ class TestCLI:
119 132
 
120 133
         """
121 134
         runner = click.testing.CliRunner(mix_stderr=False)
122
-        with tests.isolated_vault_exporter_config(
135
+        # TODO(the-13th-letter): Rewrite using parenthesized
136
+        # with-statements.
137
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
138
+        with contextlib.ExitStack() as stack:
139
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
140
+            stack.enter_context(
141
+                tests.isolated_vault_exporter_config(
123 142
                     monkeypatch=monkeypatch,
124 143
                     runner=runner,
125 144
                     vault_config=config,
126
-        ):
145
+                )
146
+            )
127 147
             result_ = runner.invoke(
128 148
                 cli.derivepassphrase_export_vault,
129 149
                 ['-f', format, '-k', tests.VAULT_MASTER_KEY, 'VAULT_PATH'],
... ...
@@ -137,17 +157,23 @@ class TestCLI:
137 157
 
138 158
     def test_301_vault_config_not_found(
139 159
         self,
140
-        monkeypatch: pytest.MonkeyPatch,
141 160
         caplog: pytest.LogCaptureFixture,
142 161
     ) -> None:
143 162
         """Fail when trying to decode non-existant files/directories."""
144 163
         runner = click.testing.CliRunner(mix_stderr=False)
145
-        with tests.isolated_vault_exporter_config(
164
+        # TODO(the-13th-letter): Rewrite using parenthesized
165
+        # with-statements.
166
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
167
+        with contextlib.ExitStack() as stack:
168
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
169
+            stack.enter_context(
170
+                tests.isolated_vault_exporter_config(
146 171
                     monkeypatch=monkeypatch,
147 172
                     runner=runner,
148 173
                     vault_config=tests.VAULT_V03_CONFIG,
149 174
                     vault_key=tests.VAULT_MASTER_KEY,
150
-        ):
175
+                )
176
+            )
151 177
             result_ = runner.invoke(
152 178
                 cli.derivepassphrase_export_vault,
153 179
                 ['does-not-exist.txt'],
... ...
@@ -164,17 +190,23 @@ class TestCLI:
164 190
 
165 191
     def test_302_vault_config_invalid(
166 192
         self,
167
-        monkeypatch: pytest.MonkeyPatch,
168 193
         caplog: pytest.LogCaptureFixture,
169 194
     ) -> None:
170 195
         """Fail to parse invalid vault configurations (files)."""
171 196
         runner = click.testing.CliRunner(mix_stderr=False)
172
-        with tests.isolated_vault_exporter_config(
197
+        # TODO(the-13th-letter): Rewrite using parenthesized
198
+        # with-statements.
199
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
200
+        with contextlib.ExitStack() as stack:
201
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
202
+            stack.enter_context(
203
+                tests.isolated_vault_exporter_config(
173 204
                     monkeypatch=monkeypatch,
174 205
                     runner=runner,
175 206
                     vault_config='',
176 207
                     vault_key=tests.VAULT_MASTER_KEY,
177
-        ):
208
+                )
209
+            )
178 210
             result_ = runner.invoke(
179 211
                 cli.derivepassphrase_export_vault,
180 212
                 ['.vault'],
... ...
@@ -188,17 +220,23 @@ class TestCLI:
188 220
 
189 221
     def test_302a_vault_config_invalid_just_a_directory(
190 222
         self,
191
-        monkeypatch: pytest.MonkeyPatch,
192 223
         caplog: pytest.LogCaptureFixture,
193 224
     ) -> None:
194 225
         """Fail to parse invalid vault configurations (directories)."""
195 226
         runner = click.testing.CliRunner(mix_stderr=False)
196
-        with tests.isolated_vault_exporter_config(
227
+        # TODO(the-13th-letter): Rewrite using parenthesized
228
+        # with-statements.
229
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
230
+        with contextlib.ExitStack() as stack:
231
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
232
+            stack.enter_context(
233
+                tests.isolated_vault_exporter_config(
197 234
                     monkeypatch=monkeypatch,
198 235
                     runner=runner,
199 236
                     vault_config='',
200 237
                     vault_key=tests.VAULT_MASTER_KEY,
201
-        ):
238
+                )
239
+            )
202 240
             p = pathlib.Path('.vault')
203 241
             p.unlink()
204 242
             p.mkdir()
... ...
@@ -215,17 +253,23 @@ class TestCLI:
215 253
 
216 254
     def test_403_invalid_vault_config_bad_signature(
217 255
         self,
218
-        monkeypatch: pytest.MonkeyPatch,
219 256
         caplog: pytest.LogCaptureFixture,
220 257
     ) -> None:
221 258
         """Fail to parse vault configurations with invalid integrity checks."""
222 259
         runner = click.testing.CliRunner(mix_stderr=False)
223
-        with tests.isolated_vault_exporter_config(
260
+        # TODO(the-13th-letter): Rewrite using parenthesized
261
+        # with-statements.
262
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
263
+        with contextlib.ExitStack() as stack:
264
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
265
+            stack.enter_context(
266
+                tests.isolated_vault_exporter_config(
224 267
                     monkeypatch=monkeypatch,
225 268
                     runner=runner,
226 269
                     vault_config=tests.VAULT_V02_CONFIG,
227 270
                     vault_key=tests.VAULT_MASTER_KEY,
228
-        ):
271
+                )
272
+            )
229 273
             result_ = runner.invoke(
230 274
                 cli.derivepassphrase_export_vault,
231 275
                 ['-f', 'v0.3', '.vault'],
... ...
@@ -239,17 +283,23 @@ class TestCLI:
239 283
 
240 284
     def test_500_vault_config_invalid_internal(
241 285
         self,
242
-        monkeypatch: pytest.MonkeyPatch,
243 286
         caplog: pytest.LogCaptureFixture,
244 287
     ) -> None:
245 288
         """The decoded vault configuration data is valid."""
246 289
         runner = click.testing.CliRunner(mix_stderr=False)
247
-        with tests.isolated_vault_exporter_config(
290
+        # TODO(the-13th-letter): Rewrite using parenthesized
291
+        # with-statements.
292
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
293
+        with contextlib.ExitStack() as stack:
294
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
295
+            stack.enter_context(
296
+                tests.isolated_vault_exporter_config(
248 297
                     monkeypatch=monkeypatch,
249 298
                     runner=runner,
250 299
                     vault_config=tests.VAULT_V03_CONFIG,
251 300
                     vault_key=tests.VAULT_MASTER_KEY,
252
-        ):
301
+                )
302
+            )
253 303
 
254 304
             def export_vault_config_data(*_args: Any, **_kwargs: Any) -> None:
255 305
                 return None
... ...
@@ -300,7 +350,6 @@ class TestStoreroom:
300 350
     )
301 351
     def test_200_export_data_path_and_keys_type(
302 352
         self,
303
-        monkeypatch: pytest.MonkeyPatch,
304 353
         path: str | None,
305 354
         key: str | Buffer | None,
306 355
         handler: exporter.ExportVaultConfigDataFunction,
... ...
@@ -312,12 +361,19 @@ class TestStoreroom:
312 361
 
313 362
         """
314 363
         runner = click.testing.CliRunner(mix_stderr=False)
315
-        with tests.isolated_vault_exporter_config(
364
+        # TODO(the-13th-letter): Rewrite using parenthesized
365
+        # with-statements.
366
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
367
+        with contextlib.ExitStack() as stack:
368
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
369
+            stack.enter_context(
370
+                tests.isolated_vault_exporter_config(
316 371
                     monkeypatch=monkeypatch,
317 372
                     runner=runner,
318 373
                     vault_config=tests.VAULT_STOREROOM_CONFIG_ZIPPED,
319 374
                     vault_key=tests.VAULT_MASTER_KEY,
320
-        ):
375
+                )
376
+            )
321 377
             assert (
322 378
                 handler(path, key, format='storeroom')
323 379
                 == tests.VAULT_STOREROOM_CONFIG_DATA
... ...
@@ -339,7 +395,6 @@ class TestStoreroom:
339 395
     @pytest.mark.parametrize('config', ['xxx', 'null', '{"version": 255}'])
340 396
     def test_401_decrypt_bucket_file_bad_json_or_version(
341 397
         self,
342
-        monkeypatch: pytest.MonkeyPatch,
343 398
         config: str,
344 399
     ) -> None:
345 400
         """Fail on bad or unsupported bucket file contents.
... ...
@@ -354,11 +409,18 @@ class TestStoreroom:
354 409
             signing_key=bytes(storeroom.KEY_SIZE),
355 410
             hashing_key=bytes(storeroom.KEY_SIZE),
356 411
         )
357
-        with tests.isolated_vault_exporter_config(
412
+        # TODO(the-13th-letter): Rewrite using parenthesized
413
+        # with-statements.
414
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
415
+        with contextlib.ExitStack() as stack:
416
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
417
+            stack.enter_context(
418
+                tests.isolated_vault_exporter_config(
358 419
                     monkeypatch=monkeypatch,
359 420
                     runner=runner,
360 421
                     vault_config=tests.VAULT_STOREROOM_CONFIG_ZIPPED,
361
-        ):
422
+                )
423
+            )
362 424
             p = pathlib.Path('.vault', '20')
363 425
             with p.open('w', encoding='UTF-8') as outfile:
364 426
                 print(config, file=outfile)
... ...
@@ -394,7 +456,6 @@ class TestStoreroom:
394 456
     )
395 457
     def test_402_export_storeroom_data_bad_master_keys_file(
396 458
         self,
397
-        monkeypatch: pytest.MonkeyPatch,
398 459
         data: str,
399 460
         err_msg: str,
400 461
         handler: exporter.ExportVaultConfigDataFunction,
... ...
@@ -405,12 +466,19 @@ class TestStoreroom:
405 466
 
406 467
         """
407 468
         runner = click.testing.CliRunner(mix_stderr=False)
408
-        with tests.isolated_vault_exporter_config(
469
+        # TODO(the-13th-letter): Rewrite using parenthesized
470
+        # with-statements.
471
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
472
+        with contextlib.ExitStack() as stack:
473
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
474
+            stack.enter_context(
475
+                tests.isolated_vault_exporter_config(
409 476
                     monkeypatch=monkeypatch,
410 477
                     runner=runner,
411 478
                     vault_config=tests.VAULT_STOREROOM_CONFIG_ZIPPED,
412 479
                     vault_key=tests.VAULT_MASTER_KEY,
413
-        ):
480
+                )
481
+            )
414 482
             p = pathlib.Path('.vault', '.keys')
415 483
             with p.open('w', encoding='UTF-8') as outfile:
416 484
                 print(data, file=outfile)
... ...
@@ -451,7 +519,6 @@ class TestStoreroom:
451 519
     )
452 520
     def test_403_export_storeroom_data_bad_directory_listing(
453 521
         self,
454
-        monkeypatch: pytest.MonkeyPatch,
455 522
         zipped_config: bytes,
456 523
         error_text: str,
457 524
         handler: exporter.ExportVaultConfigDataFunction,
... ...
@@ -475,6 +542,7 @@ class TestStoreroom:
475 542
         # with-statements.
476 543
         # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
477 544
         with contextlib.ExitStack() as stack:
545
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
478 546
             stack.enter_context(
479 547
                 tests.isolated_vault_exporter_config(
480 548
                     monkeypatch=monkeypatch,
... ...
@@ -621,7 +689,6 @@ class TestVaultNativeConfig:
621 689
     )
622 690
     def test_201_export_vault_native_data_explicit_version(
623 691
         self,
624
-        monkeypatch: pytest.MonkeyPatch,
625 692
         config: str,
626 693
         format: Literal['v0.2', 'v0.3'],
627 694
         result: _types.VaultConfig | type[Exception],
... ...
@@ -638,12 +705,19 @@ class TestVaultNativeConfig:
638 705
 
639 706
         """
640 707
         runner = click.testing.CliRunner(mix_stderr=False)
641
-        with tests.isolated_vault_exporter_config(
708
+        # TODO(the-13th-letter): Rewrite using parenthesized
709
+        # with-statements.
710
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
711
+        with contextlib.ExitStack() as stack:
712
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
713
+            stack.enter_context(
714
+                tests.isolated_vault_exporter_config(
642 715
                     monkeypatch=monkeypatch,
643 716
                     runner=runner,
644 717
                     vault_config=config,
645 718
                     vault_key=tests.VAULT_MASTER_KEY,
646
-        ):
719
+                )
720
+            )
647 721
             if isinstance(result, type):
648 722
                 with pytest.raises(result):
649 723
                     handler(None, format=format)
... ...
@@ -677,7 +751,6 @@ class TestVaultNativeConfig:
677 751
     )
678 752
     def test_202_export_data_path_and_keys_type(
679 753
         self,
680
-        monkeypatch: pytest.MonkeyPatch,
681 754
         path: str | None,
682 755
         key: str | Buffer | None,
683 756
         handler: exporter.ExportVaultConfigDataFunction,
... ...
@@ -689,12 +762,19 @@ class TestVaultNativeConfig:
689 762
 
690 763
         """
691 764
         runner = click.testing.CliRunner(mix_stderr=False)
692
-        with tests.isolated_vault_exporter_config(
765
+        # TODO(the-13th-letter): Rewrite using parenthesized
766
+        # with-statements.
767
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
768
+        with contextlib.ExitStack() as stack:
769
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
770
+            stack.enter_context(
771
+                tests.isolated_vault_exporter_config(
693 772
                     monkeypatch=monkeypatch,
694 773
                     runner=runner,
695 774
                     vault_config=tests.VAULT_V03_CONFIG,
696 775
                     vault_key=tests.VAULT_MASTER_KEY,
697
-        ):
776
+                )
777
+            )
698 778
             assert (
699 779
                 handler(path, key, format='v0.3')
700 780
                 == tests.VAULT_V03_CONFIG_DATA
... ...
@@ -719,7 +799,6 @@ class TestVaultNativeConfig:
719 799
     )
720 800
     def test_300_result_caching(
721 801
         self,
722
-        monkeypatch: pytest.MonkeyPatch,
723 802
         parser_class: type[vault_native.VaultNativeConfigParser],
724 803
         config: str,
725 804
         result: dict[str, Any],
... ...
@@ -734,11 +813,18 @@ class TestVaultNativeConfig:
734 813
             return func
735 814
 
736 815
         runner = click.testing.CliRunner(mix_stderr=False)
737
-        with tests.isolated_vault_exporter_config(
816
+        # TODO(the-13th-letter): Rewrite using parenthesized
817
+        # with-statements.
818
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
819
+        with contextlib.ExitStack() as stack:
820
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
821
+            stack.enter_context(
822
+                tests.isolated_vault_exporter_config(
738 823
                     monkeypatch=monkeypatch,
739 824
                     runner=runner,
740 825
                     vault_config=config,
741
-        ):
826
+                )
827
+            )
742 828
             parser = parser_class(
743 829
                 base64.b64decode(config), tests.VAULT_MASTER_KEY
744 830
             )
... ...
@@ -43,7 +43,6 @@ class Test001ExporterUtils:
43 43
     )
44 44
     def test_200_get_vault_key(
45 45
         self,
46
-        monkeypatch: pytest.MonkeyPatch,
47 46
         expected: str,
48 47
         vault_key: str | None,
49 48
         logname: str | None,
... ...
@@ -63,9 +62,16 @@ class Test001ExporterUtils:
63 62
             ('USERNAME', username),
64 63
         ]
65 64
         runner = click.testing.CliRunner(mix_stderr=False)
66
-        with tests.isolated_vault_exporter_config(
65
+        # TODO(the-13th-letter): Rewrite using parenthesized
66
+        # with-statements.
67
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
68
+        with contextlib.ExitStack() as stack:
69
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
70
+            stack.enter_context(
71
+                tests.isolated_vault_exporter_config(
67 72
                     monkeypatch=monkeypatch, runner=runner
68
-        ):
73
+                )
74
+            )
69 75
             for key, value in priority_list:
70 76
                 if value is not None:
71 77
                     monkeypatch.setenv(key, value)
... ...
@@ -81,7 +87,6 @@ class Test001ExporterUtils:
81 87
     )
82 88
     def test_210_get_vault_path(
83 89
         self,
84
-        monkeypatch: pytest.MonkeyPatch,
85 90
         expected: pathlib.Path,
86 91
         path: str | os.PathLike[str] | None,
87 92
     ) -> None:
... ...
@@ -91,9 +96,16 @@ class Test001ExporterUtils:
91 96
 
92 97
         """
93 98
         runner = click.testing.CliRunner(mix_stderr=False)
94
-        with tests.isolated_vault_exporter_config(
99
+        # TODO(the-13th-letter): Rewrite using parenthesized
100
+        # with-statements.
101
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
102
+        with contextlib.ExitStack() as stack:
103
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
104
+            stack.enter_context(
105
+                tests.isolated_vault_exporter_config(
95 106
                     monkeypatch=monkeypatch, runner=runner
96
-        ):
107
+                )
108
+            )
97 109
             if path:
98 110
                 monkeypatch.setenv(
99 111
                     'VAULT_PATH', os.fspath(path) if path is not None else None
... ...
@@ -103,9 +115,7 @@ class Test001ExporterUtils:
103 115
                 == expected.expanduser().resolve()
104 116
             )
105 117
 
106
-    def test_220_register_export_vault_config_data_handler(
107
-        self, monkeypatch: pytest.MonkeyPatch
108
-    ) -> None:
118
+    def test_220_register_export_vault_config_data_handler(self) -> None:
109 119
         """Register vault config data export handlers."""
110 120
 
111 121
         def handler(  # pragma: no cover
... ...
@@ -117,6 +127,7 @@ class Test001ExporterUtils:
117 127
             del path, key
118 128
             raise ValueError(format)
119 129
 
130
+        with pytest.MonkeyPatch.context() as monkeypatch:
120 131
             registry = {'dummy': handler}
121 132
             monkeypatch.setattr(
122 133
                 exporter, '_export_vault_config_data_registry', registry
... ...
@@ -132,10 +143,9 @@ class Test001ExporterUtils:
132 143
                 'name2': handler,
133 144
             }
134 145
 
135
-    def test_300_get_vault_key_without_envs(
136
-        self, monkeypatch: pytest.MonkeyPatch
137
-    ) -> None:
146
+    def test_300_get_vault_key_without_envs(self) -> None:
138 147
         """Fail to look up the vault key in the empty environment."""
148
+        with pytest.MonkeyPatch.context() as monkeypatch:
139 149
             monkeypatch.delenv('VAULT_KEY', raising=False)
140 150
             monkeypatch.delenv('LOGNAME', raising=False)
141 151
             monkeypatch.delenv('USER', raising=False)
... ...
@@ -143,14 +153,13 @@ class Test001ExporterUtils:
143 153
             with pytest.raises(KeyError, match='VAULT_KEY'):
144 154
                 exporter.get_vault_key()
145 155
 
146
-    def test_310_get_vault_path_without_home(
147
-        self, monkeypatch: pytest.MonkeyPatch
148
-    ) -> None:
156
+    def test_310_get_vault_path_without_home(self) -> None:
149 157
         """Fail to look up the vault path without `HOME`."""
150 158
 
151 159
         def raiser(*_args: Any, **_kwargs: Any) -> Any:
152 160
             raise RuntimeError('Cannot determine home directory.')  # noqa: EM101,TRY003
153 161
 
162
+        with pytest.MonkeyPatch.context() as monkeypatch:
154 163
             monkeypatch.setattr(pathlib.Path, 'expanduser', raiser)
155 164
             monkeypatch.setattr(os.path, 'expanduser', raiser)
156 165
             with pytest.raises(
... ...
@@ -176,7 +185,6 @@ class Test001ExporterUtils:
176 185
     )
177 186
     def test_320_register_export_vault_config_data_handler_errors(
178 187
         self,
179
-        monkeypatch: pytest.MonkeyPatch,
180 188
         namelist: tuple[str, ...],
181 189
         err_pat: str,
182 190
     ) -> None:
... ...
@@ -196,6 +204,7 @@ class Test001ExporterUtils:
196 204
             del path, key
197 205
             raise ValueError(format)
198 206
 
207
+        with pytest.MonkeyPatch.context() as monkeypatch:
199 208
             registry = {'dummy': handler}
200 209
             monkeypatch.setattr(
201 210
                 exporter, '_export_vault_config_data_registry', registry
... ...
@@ -205,11 +214,12 @@ class Test001ExporterUtils:
205 214
                     handler
206 215
                 )
207 216
 
208
-    def test_321_export_vault_config_data_bad_handler(
209
-        self, monkeypatch: pytest.MonkeyPatch
210
-    ) -> None:
217
+    def test_321_export_vault_config_data_bad_handler(self) -> None:
211 218
         """Fail to export vault config data without known handlers."""
212
-        monkeypatch.setattr(exporter, '_export_vault_config_data_registry', {})
219
+        with pytest.MonkeyPatch.context() as monkeypatch:
220
+            monkeypatch.setattr(
221
+                exporter, '_export_vault_config_data_registry', {}
222
+            )
213 223
             monkeypatch.setattr(
214 224
                 exporter, 'find_vault_config_data_handlers', lambda: None
215 225
             )
... ...
@@ -223,18 +233,22 @@ class Test001ExporterUtils:
223 233
 class Test002CLI:
224 234
     """Test the command-line functionality of the `exporter` subpackage."""
225 235
 
226
-    def test_300_invalid_format(
227
-        self,
228
-        monkeypatch: pytest.MonkeyPatch,
229
-    ) -> None:
236
+    def test_300_invalid_format(self) -> None:
230 237
         """Reject invalid vault configuration format names."""
231 238
         runner = click.testing.CliRunner(mix_stderr=False)
232
-        with tests.isolated_vault_exporter_config(
239
+        # TODO(the-13th-letter): Rewrite using parenthesized
240
+        # with-statements.
241
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
242
+        with contextlib.ExitStack() as stack:
243
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
244
+            stack.enter_context(
245
+                tests.isolated_vault_exporter_config(
233 246
                     monkeypatch=monkeypatch,
234 247
                     runner=runner,
235 248
                     vault_config=tests.VAULT_V03_CONFIG,
236 249
                     vault_key=tests.VAULT_MASTER_KEY,
237
-        ):
250
+                )
251
+            )
238 252
             result_ = runner.invoke(
239 253
                 cli.derivepassphrase_export_vault,
240 254
                 ['-f', 'INVALID', 'VAULT_PATH'],
... ...
@@ -272,7 +286,6 @@ class Test002CLI:
272 286
     )
273 287
     def test_999_no_cryptography_error_message(
274 288
         self,
275
-        monkeypatch: pytest.MonkeyPatch,
276 289
         caplog: pytest.LogCaptureFixture,
277 290
         format: str,
278 291
         config: str | bytes,
... ...
@@ -280,12 +293,19 @@ class Test002CLI:
280 293
     ) -> None:
281 294
         """Abort export call if no cryptography is available."""
282 295
         runner = click.testing.CliRunner(mix_stderr=False)
283
-        with tests.isolated_vault_exporter_config(
296
+        # TODO(the-13th-letter): Rewrite using parenthesized
297
+        # with-statements.
298
+        # https://the13thletter.info/derivepassphrase/latest/pycompatibility/#after-eol-py3.9
299
+        with contextlib.ExitStack() as stack:
300
+            monkeypatch = stack.enter_context(pytest.MonkeyPatch.context())
301
+            stack.enter_context(
302
+                tests.isolated_vault_exporter_config(
284 303
                     monkeypatch=monkeypatch,
285 304
                     runner=runner,
286 305
                     vault_config=config,
287 306
                     vault_key=key,
288
-        ):
307
+                )
308
+            )
289 309
             result_ = runner.invoke(
290 310
                 cli.derivepassphrase_export_vault,
291 311
                 ['-f', format, 'VAULT_PATH'],
... ...
@@ -132,11 +132,11 @@ class TestStaticFunctionality:
132 132
 
133 133
     def test_200_constructor_no_running_agent(
134 134
         self,
135
-        monkeypatch: pytest.MonkeyPatch,
136 135
         skip_if_no_af_unix_support: None,
137 136
     ) -> None:
138 137
         """Abort if the running agent cannot be located."""
139 138
         del skip_if_no_af_unix_support
139
+        with pytest.MonkeyPatch.context() as monkeypatch:
140 140
             monkeypatch.delenv('SSH_AUTH_SOCK', raising=False)
141 141
             with pytest.raises(
142 142
                 KeyError, match='SSH_AUTH_SOCK environment variable'
... ...
@@ -467,26 +467,20 @@ class TestAgentInteraction:
467 467
 
468 468
     def test_300_constructor_bad_running_agent(
469 469
         self,
470
-        monkeypatch: pytest.MonkeyPatch,
471 470
         running_ssh_agent: tests.RunningSSHAgentInfo,
472 471
     ) -> None:
473 472
         """Fail if the agent address is invalid."""
474
-        with monkeypatch.context() as monkeypatch2:
475
-            monkeypatch2.setenv(
476
-                'SSH_AUTH_SOCK', running_ssh_agent.socket + '~'
477
-            )
473
+        with pytest.MonkeyPatch.context() as monkeypatch:
474
+            monkeypatch.setenv('SSH_AUTH_SOCK', running_ssh_agent.socket + '~')
478 475
             sock = socket.socket(family=socket.AF_UNIX)
479 476
             with pytest.raises(OSError):  # noqa: PT011
480 477
                 ssh_agent.SSHAgentClient(socket=sock)
481 478
 
482
-    def test_301_constructor_no_af_unix_support(
483
-        self,
484
-        monkeypatch: pytest.MonkeyPatch,
485
-    ) -> None:
479
+    def test_301_constructor_no_af_unix_support(self) -> None:
486 480
         """Fail without [`socket.AF_UNIX`][] support."""
487
-        with monkeypatch.context() as monkeypatch2:
488
-            monkeypatch2.setenv('SSH_AUTH_SOCK', "the value doesn't matter")
489
-            monkeypatch2.delattr(socket, 'AF_UNIX', raising=False)
481
+        with pytest.MonkeyPatch.context() as monkeypatch:
482
+            monkeypatch.setenv('SSH_AUTH_SOCK', "the value doesn't matter")
483
+            monkeypatch.delattr(socket, 'AF_UNIX', raising=False)
490 484
             with pytest.raises(
491 485
                 NotImplementedError,
492 486
                 match='UNIX domain sockets',
... ...
@@ -503,7 +497,6 @@ class TestAgentInteraction:
503 497
     )
504 498
     def test_310_truncated_server_response(
505 499
         self,
506
-        monkeypatch: pytest.MonkeyPatch,
507 500
         running_ssh_agent: tests.RunningSSHAgentInfo,
508 501
         response: bytes,
509 502
     ) -> None:
... ...
@@ -520,6 +513,7 @@ class TestAgentInteraction:
520 513
                 return response_stream.read(*args, **kwargs)
521 514
 
522 515
         pseudo_socket = PseudoSocket()
516
+        with pytest.MonkeyPatch.context() as monkeypatch:
523 517
             monkeypatch.setattr(client, '_connection', pseudo_socket)
524 518
             with pytest.raises(EOFError):
525 519
                 client.request(255, b'')
... ...
@@ -552,7 +546,6 @@ class TestAgentInteraction:
552 546
     )
553 547
     def test_320_list_keys_error_responses(
554 548
         self,
555
-        monkeypatch: pytest.MonkeyPatch,
556 549
         running_ssh_agent: tests.RunningSSHAgentInfo,
557 550
         response_code: _types.SSH_AGENT,
558 551
         response: bytes | bytearray,
... ...
@@ -602,9 +595,9 @@ class TestAgentInteraction:
602 595
                 )
603 596
             return response
604 597
 
605
-        with monkeypatch.context() as monkeypatch2:
598
+        with pytest.MonkeyPatch.context() as monkeypatch:
606 599
             client = ssh_agent.SSHAgentClient()
607
-            monkeypatch2.setattr(client, 'request', request)
600
+            monkeypatch.setattr(client, 'request', request)
608 601
             with pytest.raises(exc_type, match=exc_pattern):
609 602
                 client.list_keys()
610 603
 
... ...
@@ -640,7 +633,6 @@ class TestAgentInteraction:
640 633
     )
641 634
     def test_330_sign_error_responses(
642 635
         self,
643
-        monkeypatch: pytest.MonkeyPatch,
644 636
         running_ssh_agent: tests.RunningSSHAgentInfo,
645 637
         key: bytes | bytearray,
646 638
         check: bool,
... ...
@@ -692,16 +684,16 @@ class TestAgentInteraction:
692 684
                 )
693 685
             return response  # pragma: no cover
694 686
 
695
-        with monkeypatch.context() as monkeypatch2:
687
+        with pytest.MonkeyPatch.context() as monkeypatch:
696 688
             client = ssh_agent.SSHAgentClient()
697
-            monkeypatch2.setattr(client, 'request', request)
689
+            monkeypatch.setattr(client, 'request', request)
698 690
             Pair = _types.SSHKeyCommentPair  # noqa: N806
699 691
             com = b'no comment'
700 692
             loaded_keys = [
701 693
                 Pair(v.public_key_data, com).toreadonly()
702 694
                 for v in tests.SUPPORTED_KEYS.values()
703 695
             ]
704
-            monkeypatch2.setattr(client, 'list_keys', lambda: loaded_keys)
696
+            monkeypatch.setattr(client, 'list_keys', lambda: loaded_keys)
705 697
             with pytest.raises(exc_type, match=exc_pattern):
706 698
                 client.sign(key, b'abc', check_if_key_loaded=check)
707 699
 
708 700