Add support for Python 3.9
Marco Ricci

Marco Ricci commited on 2024-10-01 11:51:05
Zeige 12 geänderte Dateien mit 102 Einfügungen und 103 Löschungen.


Support Python 3.9 by not using the following features:

  - `match`/`case` statements; replace them with `if`-statements
  - annotation-like class unions in `isinstance`; use a tuple instead
  - `TypeAlias` objects; write these as strings
  - the `root_dir` parameter of `glob.glob`; build the file list via
    `fnmatch.fnmatch` ourselves
... ...
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6 6
 name = "derivepassphrase"
7 7
 description = "An almost faithful Python reimplementation of James Coglan's vault."
8 8
 readme = "README.md"
9
-requires-python = ">= 3.10"
9
+requires-python = ">= 3.9"
10 10
 license = "MIT"
11 11
 keywords = []
12 12
 authors = [
... ...
@@ -133,7 +133,7 @@ extra-dependencies = [
133 133
 matrix-name-format = '{variable}_{value}'
134 134
 
135 135
 [[tool.hatch.envs.hatch-test.matrix]]
136
-python = ["3.12", "3.11", "3.10", "pypy3.10"]
136
+python = ["3.12", "3.11", "3.10", "3.9", "pypy3.10", "pypy3.9"]
137 137
 cryptography = ["no", "yes"]
138 138
 hypothesis-profile = ["user-default"]
139 139
 
... ...
@@ -221,18 +221,18 @@ def validate_vault_config(  # noqa: C901,PLR0912,PLR0915
221 221
         if not isinstance(o_global, dict):
222 222
             raise TypeError(err_not_a_dict(['global']))
223 223
         for key, value in o_global.items():
224
-            match key:
225
-                case 'key' | 'phrase':
224
+            # Use match/case here once Python 3.9 becomes unsupported.
225
+            if key in {'key', 'phrase'}:
226 226
                 if not isinstance(value, str):
227 227
                     raise TypeError(err_not_a_dict(['global', key]))
228
-                case 'unicode_normalization_form':
228
+            elif key == 'unicode_normalization_form':
229 229
                 if not isinstance(value, str):
230 230
                     raise TypeError(err_not_a_dict(['global', key]))
231 231
                 if not allow_derivepassphrase_extensions:
232 232
                     raise ValueError(
233 233
                         err_derivepassphrase_extension(key, ('global',))
234 234
                     )
235
-                case _ if not allow_unknown_settings:
235
+            elif not allow_unknown_settings:
236 236
                 raise ValueError(err_unknown_setting(key, ('global',)))
237 237
         if 'key' in o_global and 'phrase' in o_global:
238 238
             raise ValueError(err_key_and_phrase)
... ...
@@ -244,17 +244,15 @@ def validate_vault_config(  # noqa: C901,PLR0912,PLR0915
244 244
         if not isinstance(service, dict):
245 245
             raise TypeError(err_not_a_dict(['services', sv_name]))
246 246
         for key, value in service.items():
247
-            match key:
248
-                case 'notes' | 'phrase' | 'key':
247
+            # Use match/case here once Python 3.9 becomes unsupported.
248
+            if key in {'notes', 'phrase', 'key'}:
249 249
                 if not isinstance(value, str):
250 250
                     raise TypeError(
251 251
                         err_not_a_string(['services', sv_name, key])
252 252
                     )
253
-                case 'length':
253
+            elif key == 'length':
254 254
                 if not isinstance(value, int):
255
-                        raise TypeError(
256
-                            err_not_an_int(['services', sv_name, key])
257
-                        )
255
+                    raise TypeError(err_not_an_int(['services', sv_name, key]))
258 256
                 if value < 1:
259 257
                     raise ValueError(
260 258
                         err_bad_number(
... ...
@@ -263,19 +261,17 @@ def validate_vault_config(  # noqa: C901,PLR0912,PLR0915
263 261
                             strictly_positive=True,
264 262
                         )
265 263
                     )
266
-                case (
267
-                    'repeat'
268
-                    | 'lower'
269
-                    | 'upper'
270
-                    | 'number'
271
-                    | 'space'
272
-                    | 'dash'
273
-                    | 'symbol'
274
-                ):
264
+            elif key in {
265
+                'repeat',
266
+                'lower',
267
+                'upper',
268
+                'number',
269
+                'space',
270
+                'dash',
271
+                'symbol',
272
+            }:
275 273
                 if not isinstance(value, int):
276
-                        raise TypeError(
277
-                            err_not_an_int(['services', sv_name, key])
278
-                        )
274
+                    raise TypeError(err_not_an_int(['services', sv_name, key]))
279 275
                 if value < 0:
280 276
                     raise ValueError(
281 277
                         err_bad_number(
... ...
@@ -284,7 +280,7 @@ def validate_vault_config(  # noqa: C901,PLR0912,PLR0915
284 280
                             strictly_positive=False,
285 281
                         )
286 282
                     )
287
-                case _ if not allow_unknown_settings:
283
+            elif not allow_unknown_settings:
288 284
                 raise ValueError(
289 285
                     err_unknown_setting(key, ['services', sv_name])
290 286
                 )
... ...
@@ -202,8 +202,8 @@ def _load_data(
202 202
 ) -> Any:  # noqa: ANN401
203 203
     contents: bytes
204 204
     module: types.ModuleType
205
-    match fmt:
206
-        case 'v0.2':
205
+    # Use match/case here once Python 3.9 becomes unsupported.
206
+    if fmt == 'v0.2':
207 207
         module = importlib.import_module(
208 208
             'derivepassphrase.exporter.vault_native'
209 209
         )
... ...
@@ -214,7 +214,7 @@ def _load_data(
214 214
         return module.export_vault_native_data(
215 215
             contents, key, try_formats=['v0.2']
216 216
         )
217
-        case 'v0.3':
217
+    elif fmt == 'v0.3':  # noqa: RET505
218 218
         module = importlib.import_module(
219 219
             'derivepassphrase.exporter.vault_native'
220 220
         )
... ...
@@ -225,14 +225,12 @@ def _load_data(
225 225
         return module.export_vault_native_data(
226 226
             contents, key, try_formats=['v0.3']
227 227
         )
228
-        case 'storeroom':
229
-            module = importlib.import_module(
230
-                'derivepassphrase.exporter.storeroom'
231
-            )
228
+    elif fmt == 'storeroom':
229
+        module = importlib.import_module('derivepassphrase.exporter.storeroom')
232 230
         if module.STUBBED:
233 231
             raise ModuleNotFoundError
234 232
         return module.export_storeroom_data(path, key)
235
-        case _:  # pragma: no cover
233
+    else:  # pragma: no cover
236 234
         assert_never(fmt)
237 235
 
238 236
 
... ...
@@ -373,12 +371,12 @@ def _config_filename(
373 371
     path = os.getenv(PROG_NAME.upper() + '_PATH') or click.get_app_dir(
374 372
         PROG_NAME, force_posix=True
375 373
     )
376
-    match subsystem:
377
-        case None:
374
+    # Use match/case here once Python 3.9 becomes unsupported.
375
+    if subsystem is None:
378 376
         return path
379
-        case 'vault' | 'settings':
377
+    elif subsystem in {'vault', 'settings'}:  # noqa: RET505
380 378
         filename = f'{subsystem}.json'
381
-        case _:  # pragma: no cover
379
+    else:  # pragma: no cover
382 380
         msg = f'Unknown configuration subsystem: {subsystem!r}'
383 381
         raise AssertionError(msg)
384 382
     return os.path.join(path, filename)
... ...
@@ -522,14 +520,14 @@ def _get_suitable_ssh_keys(
522 520
     """
523 521
     client: ssh_agent.SSHAgentClient
524 522
     client_context: contextlib.AbstractContextManager[Any]
525
-    match conn:
526
-        case ssh_agent.SSHAgentClient():
523
+    # Use match/case here once Python 3.9 becomes unsupported.
524
+    if isinstance(conn, ssh_agent.SSHAgentClient):
527 525
         client = conn
528 526
         client_context = contextlib.nullcontext()
529
-        case socket.socket() | None:
527
+    elif isinstance(conn, socket.socket) or conn is None:
530 528
         client = ssh_agent.SSHAgentClient(socket=conn)
531 529
         client_context = client
532
-        case _:  # pragma: no cover
530
+    else:  # pragma: no cover
533 531
         assert_never(conn)
534 532
         msg = f'invalid connection hint: {conn!r}'
535 533
         raise TypeError(msg)  # noqa: DOC501
... ...
@@ -1216,18 +1214,18 @@ def derivepassphrase_vault(  # noqa: C901,PLR0912,PLR0913,PLR0914,PLR0915
1216 1214
     for param in ctx.command.params:
1217 1215
         if isinstance(param, click.Option):
1218 1216
             group: type[click.Option]
1219
-            match param:
1220
-                case PasswordGenerationOption():
1217
+            # Use match/case here once Python 3.9 becomes unsupported.
1218
+            if isinstance(param, PasswordGenerationOption):
1221 1219
                 group = PasswordGenerationOption
1222
-                case ConfigurationOption():
1220
+            elif isinstance(param, ConfigurationOption):
1223 1221
                 group = ConfigurationOption
1224
-                case StorageManagementOption():
1222
+            elif isinstance(param, StorageManagementOption):
1225 1223
                 group = StorageManagementOption
1226
-                case OptionGroupOption():
1227
-                    raise AssertionError(  # noqa: DOC501,TRY003
1224
+            elif isinstance(param, OptionGroupOption):
1225
+                raise AssertionError(  # noqa: DOC501,TRY003,TRY004
1228 1226
                     f'Unknown option group for {param!r}'  # noqa: EM102
1229 1227
                 )
1230
-                case _:
1228
+            else:
1231 1229
                 group = click.Option
1232 1230
             options_in_group.setdefault(group, []).append(param)
1233 1231
         params_by_str[param.human_readable_name] = param
... ...
@@ -23,7 +23,7 @@ should *not* be used or relied on.
23 23
 from __future__ import annotations
24 24
 
25 25
 import base64
26
-import glob
26
+import fnmatch
27 27
 import json
28 28
 import logging
29 29
 import os
... ...
@@ -678,9 +678,15 @@ def export_storeroom_data(  # noqa: C901,PLR0912,PLR0914,PLR0915
678 678
 
679 679
     config_structure: dict[str, Any] = {}
680 680
     json_contents: dict[str, bytes] = {}
681
-    for file in glob.glob(
682
-        '[01][0-9a-f]', root_dir=os.fsdecode(storeroom_path)
683
-    ):
681
+    # Use glob.glob(..., root_dir=...) here once Python 3.9 becomes
682
+    # unsupported.
683
+    storeroom_path_str = os.fsdecode(storeroom_path)
684
+    valid_hashdirs = [
685
+        hashdir_name
686
+        for hashdir_name in os.listdir(storeroom_path_str)
687
+        if fnmatch.fnmatch(hashdir_name, '[01][0-9a-f]')
688
+    ]
689
+    for file in valid_hashdirs:
684 690
         bucket_contents = list(
685 691
             decrypt_bucket_file(file, master_keys, root_dir=storeroom_path)
686 692
         )
... ...
@@ -441,20 +441,20 @@ def export_vault_native_data(
441 441
         key = exporter.get_vault_key()
442 442
     stored_exception: Exception | None = None
443 443
     for config_format in try_formats:
444
-        match config_format:
445
-            case 'v0.2':
444
+        # Use match/case here once Python 3.9 becomes unsupported.
445
+        if config_format == 'v0.2':
446 446
             try:
447 447
                 return VaultNativeV02ConfigParser(contents, key)()
448 448
             except ValueError as exc:
449 449
                 exc.__context__ = stored_exception
450 450
                 stored_exception = exc
451
-            case 'v0.3':
451
+        elif config_format == 'v0.3':
452 452
             try:
453 453
                 return VaultNativeV03ConfigParser(contents, key)()
454 454
             except ValueError as exc:
455 455
                 exc.__context__ = stored_exception
456 456
                 stored_exception = exc
457
-            case _:  # pragma: no cover
457
+        else:  # pragma: no cover
458 458
             msg = (
459 459
                 f'Invalid vault native configuration format: '
460 460
                 f'{config_format!r}'
... ...
@@ -43,14 +43,17 @@ class SSHAgentFailedError(RuntimeError):
43 43
     """The SSH agent failed to complete the requested operation."""
44 44
 
45 45
     def __str__(self) -> str:
46
-        match self.args:
47
-            case (_types.SSH_AGENT.FAILURE.value, b''):  # pragma: no branch
46
+        # Use match/case here once Python 3.9 becomes unsupported.
47
+        if self.args == (  # pragma: no branch
48
+            _types.SSH_AGENT.FAILURE.value,
49
+            b'',
50
+        ):
48 51
             return 'The SSH agent failed to complete the request'
49
-            case (_, _msg) if _msg:  # pragma: no cover
52
+        elif self.args[1]:  # noqa: RET505  # pragma: no cover
50 53
             code = self.args[0]
51 54
             msg = self.args[1].decode('utf-8', 'surrogateescape')
52 55
             return f'[Code {code:d}] {msg:s}'
53
-            case _:  # pragma: no cover
56
+        else:  # pragma: no cover
54 57
             return repr(self)
55 58
 
56 59
     def __repr__(self) -> str:  # pragma: no cover
... ...
@@ -348,7 +351,7 @@ class SSHAgentClient:
348 351
 
349 352
         """
350 353
         if isinstance(  # pragma: no branch
351
-            response_code, int | _types.SSH_AGENT
354
+            response_code, (int, _types.SSH_AGENT)
352 355
         ):
353 356
             response_code = frozenset({response_code})
354 357
         if response_code is not None:  # pragma: no branch
... ...
@@ -11,13 +11,15 @@ import collections
11 11
 import hashlib
12 12
 import math
13 13
 import types
14
-from collections.abc import Callable
15
-from typing import TypeAlias
14
+from typing import TYPE_CHECKING
16 15
 
17
-from typing_extensions import assert_type
16
+from typing_extensions import TypeAlias, assert_type
18 17
 
19 18
 from derivepassphrase import sequin, ssh_agent
20 19
 
20
+if TYPE_CHECKING:
21
+    from collections.abc import Callable
22
+
21 23
 __author__ = 'Marco Ricci <software@the13thletter.info>'
22 24
 
23 25
 
... ...
@@ -458,7 +460,7 @@ class Vault:
458 460
             a passphrase deterministically.
459 461
 
460 462
         """
461
-        TestFunc: TypeAlias = Callable[[bytes | bytearray], bool]
463
+        TestFunc: TypeAlias = 'Callable[[bytes | bytearray], bool]'
462 464
         deterministic_signature_types: dict[str, TestFunc]
463 465
         deterministic_signature_types = {
464 466
             'ssh-ed25519': lambda k: k.startswith(
... ...
@@ -806,11 +806,11 @@ def isolated_vault_exporter_config(
806 806
         monkeypatch.delenv('USERNAME', raising=False)
807 807
         if vault_key is not None:
808 808
             monkeypatch.setenv('VAULT_KEY', vault_key)
809
-        match vault_config:
810
-            case str():
809
+        # Use match/case here once Python 3.9 becomes unsupported.
810
+        if isinstance(vault_config, str):
811 811
             with open('.vault', 'w', encoding='UTF-8') as outfile:
812 812
                 print(vault_config, file=outfile)
813
-            case bytes():
813
+        elif isinstance(vault_config, bytes):
814 814
             os.makedirs('.vault', mode=0o700, exist_ok=True)
815 815
             with (
816 816
                 chdir('.vault'),
... ...
@@ -822,9 +822,9 @@ def isolated_vault_exporter_config(
822 822
                 tmpzipfile.seek(0, 0)
823 823
                 with zipfile.ZipFile(tmpzipfile.file) as zipfileobj:
824 824
                     zipfileobj.extractall()
825
-            case None:
825
+        elif vault_config is None:
826 826
             pass
827
-            case _:  # pragma: no cover
827
+        else:  # pragma: no cover
828 828
             assert_never(vault_config)
829 829
         yield
830 830
 
... ...
@@ -935,14 +935,14 @@ class ReadableResult(NamedTuple):
935 935
                 code, or an expected exception type.
936 936
 
937 937
         """
938
-        match error:
939
-            case str():
938
+        # Use match/case here once Python 3.9 becomes unsupported.
939
+        if isinstance(error, str):
940 940
             return (
941 941
                 isinstance(self.exception, SystemExit)
942 942
                 and self.exit_code > 0
943 943
                 and (not error or error in self.stderr)
944 944
             )
945
-            case _:
945
+        else:  # noqa: RET505
946 946
             return isinstance(self.exception, error)
947 947
 
948 948
 
... ...
@@ -262,8 +262,8 @@ def running_ssh_agent() -> Iterator[str]:  # pragma: no cover
262 262
         else:  # pragma: no cover
263 263
             monkeypatch.delenv('SSH_AUTH_SOCK', raising=False)
264 264
         for exec_name, spawn_func, _ in _spawn_handlers:
265
-            match exec_name:
266
-                case '(system)':
265
+            # Use match/case here once Python 3.9 becomes unsupported.
266
+            if exec_name == '(system)':
267 267
                 assert (
268 268
                     os.environ.get('SSH_AUTH_SOCK', None)
269 269
                     == startup_ssh_auth_sock
... ...
@@ -277,8 +277,8 @@ def running_ssh_agent() -> Iterator[str]:  # pragma: no cover
277 277
                 assert (
278 278
                     os.environ.get('SSH_AUTH_SOCK', None)
279 279
                     == startup_ssh_auth_sock
280
-                    ), 'SSH_AUTH_SOCK mismatch after returning from running agent'  # noqa: E501
281
-                case _:
280
+                ), 'SSH_AUTH_SOCK mismatch after returning from running agent'
281
+            else:
282 282
                 assert (
283 283
                     os.environ.get('SSH_AUTH_SOCK', None)
284 284
                     == startup_ssh_auth_sock
... ...
@@ -310,9 +310,7 @@ def running_ssh_agent() -> Iterator[str]:  # pragma: no cover
310 310
                         'pid' not in pid_line.lower()
311 311
                         and '_pid' not in pid_line.lower()
312 312
                     ):  # pragma: no cover
313
-                            pytest.skip(
314
-                                f'Cannot parse agent output: {pid_line!r}'
315
-                            )
313
+                        pytest.skip(f'Cannot parse agent output: {pid_line!r}')
316 314
                     proc2 = _spawn_data_sink(
317 315
                         emits_debug_output=emits_debug_output, proc=proc
318 316
                     )
... ...
@@ -419,11 +417,10 @@ def spawn_ssh_agent(  # noqa: C901
419 417
         else:  # pragma: no cover
420 418
             monkeypatch.delenv('SSH_AUTH_SOCK', raising=False)
421 419
         exec_name, spawn_func, agent_type = request.param
422
-        match exec_name:
423
-            case '(system)':
420
+        # Use match/case here once Python 3.9 becomes unsupported.
421
+        if exec_name == '(system)':
424 422
             assert (
425
-                    os.environ.get('SSH_AUTH_SOCK', None)
426
-                    == startup_ssh_auth_sock
423
+                os.environ.get('SSH_AUTH_SOCK', None) == startup_ssh_auth_sock
427 424
             ), 'SSH_AUTH_SOCK mismatch when checking for running agent'
428 425
             try:
429 426
                 client = ssh_agent.SSHAgentClient()
... ...
@@ -439,25 +436,22 @@ def spawn_ssh_agent(  # noqa: C901
439 436
                 assert (
440 437
                     os.environ.get('SSH_AUTH_SOCK', None)
441 438
                     == startup_ssh_auth_sock
442
-                    ), 'SSH_AUTH_SOCK mismatch before setting up for running agent'  # noqa: E501
439
+                ), 'SSH_AUTH_SOCK mismatch before setting up for running agent'
443 440
                 yield tests.SpawnedSSHAgentInfo(agent_type, client, False)
444 441
             assert (
445
-                    os.environ.get('SSH_AUTH_SOCK', None)
446
-                    == startup_ssh_auth_sock
442
+                os.environ.get('SSH_AUTH_SOCK', None) == startup_ssh_auth_sock
447 443
             ), 'SSH_AUTH_SOCK mismatch after returning from running agent'
448 444
             return
449 445
 
450
-            case _:
446
+        else:
451 447
             assert (
452
-                    os.environ.get('SSH_AUTH_SOCK', None)
453
-                    == startup_ssh_auth_sock
448
+                os.environ.get('SSH_AUTH_SOCK', None) == startup_ssh_auth_sock
454 449
             ), f'SSH_AUTH_SOCK mismatch when checking for spawnable {exec_name}'  # noqa: E501
455 450
             spawn_data = spawn_func(
456 451
                 executable=shutil.which(exec_name), env=agent_env
457 452
             )
458 453
             assert (
459
-                    os.environ.get('SSH_AUTH_SOCK', None)
460
-                    == startup_ssh_auth_sock
454
+                os.environ.get('SSH_AUTH_SOCK', None) == startup_ssh_auth_sock
461 455
             ), f'SSH_AUTH_SOCK mismatch after spawning {exec_name}'
462 456
             if spawn_data is None:  # pragma: no cover
463 457
                 pytest.skip(f'Cannot spawn usable {exec_name}')
... ...
@@ -483,7 +477,7 @@ def spawn_ssh_agent(  # noqa: C901
483 477
                 assert (
484 478
                     os.environ.get('SSH_AUTH_SOCK', None)
485 479
                     == startup_ssh_auth_sock
486
-                    ), f'SSH_AUTH_SOCK mismatch before spawning {exec_name} helper'  # noqa: E501
480
+                ), f'SSH_AUTH_SOCK mismatch before spawning {exec_name} helper'
487 481
                 proc2 = _spawn_data_sink(
488 482
                     emits_debug_output=emits_debug_output, proc=proc
489 483
                 )
... ...
@@ -492,7 +486,7 @@ def spawn_ssh_agent(  # noqa: C901
492 486
                 assert (
493 487
                     os.environ.get('SSH_AUTH_SOCK', None)
494 488
                     == startup_ssh_auth_sock
495
-                    ), f'SSH_AUTH_SOCK mismatch after spawning {exec_name} helper'  # noqa: E501
489
+                ), f'SSH_AUTH_SOCK mismatch after spawning {exec_name} helper'
496 490
                 monkeypatch2 = exit_stack.enter_context(
497 491
                     pytest.MonkeyPatch.context()
498 492
                 )
... ...
@@ -507,8 +501,7 @@ def spawn_ssh_agent(  # noqa: C901
507 501
                 exit_stack.enter_context(client)
508 502
                 yield tests.SpawnedSSHAgentInfo(agent_type, client, True)
509 503
             assert (
510
-                    os.environ.get('SSH_AUTH_SOCK', None)
511
-                    == startup_ssh_auth_sock
504
+                os.environ.get('SSH_AUTH_SOCK', None) == startup_ssh_auth_sock
512 505
             ), f'SSH_AUTH_SOCK mismatch after tearing down {exec_name}'
513 506
             return
514 507
 
... ...
@@ -1568,13 +1568,13 @@ Boo.
1568 1568
                 ssh_agent.SSHAgentClient, 'list_keys', tests.list_keys
1569 1569
             )
1570 1570
             hint: ssh_agent.SSHAgentClient | socket.socket | None
1571
-            match conn_hint:
1572
-                case 'client':
1571
+            # Use match/case here once Python 3.9 becomes unsupported.
1572
+            if conn_hint == 'client':
1573 1573
                 hint = ssh_agent.SSHAgentClient()
1574
-                case 'socket':
1574
+            elif conn_hint == 'socket':
1575 1575
                 hint = socket.socket(family=socket.AF_UNIX)
1576 1576
                 hint.connect(running_ssh_agent)
1577
-                case _:
1577
+            else:
1578 1578
                 assert conn_hint == 'none'
1579 1579
                 hint = None
1580 1580
             exception: Exception | None = None
... ...
@@ -430,7 +430,7 @@ class TestAgentInteraction:
430 430
             del request_code
431 431
             del payload
432 432
             if isinstance(  # pragma: no branch
433
-                response_code, int | _types.SSH_AGENT
433
+                response_code, (int, _types.SSH_AGENT)
434 434
             ):
435 435
                 response_code = frozenset({response_code})
436 436
             if response_code is not None:  # pragma: no branch
... ...
@@ -507,7 +507,7 @@ class TestAgentInteraction:
507 507
             del request_code
508 508
             del payload
509 509
             if isinstance(  # pragma: no branch
510
-                response_code, int | _types.SSH_AGENT
510
+                response_code, (int, _types.SSH_AGENT)
511 511
             ):
512 512
                 response_code = frozenset({response_code})
513 513
             if response_code is not None:  # pragma: no branch
... ...
@@ -7,11 +7,12 @@
7 7
 from __future__ import annotations
8 8
 
9 9
 import math
10
-from typing import TYPE_CHECKING, TypeAlias, TypeVar
10
+from typing import TYPE_CHECKING
11 11
 
12 12
 import hypothesis
13 13
 import pytest
14 14
 from hypothesis import strategies
15
+from typing_extensions import TypeAlias, TypeVar
15 16
 
16 17
 import derivepassphrase
17 18
 
18 19