Always test the "no cryptography support" scenario, shrinking the test matrix
Marco Ricci

Marco Ricci commited on 2026-08-23 12:44:07
Zeige 3 geänderte Dateien mit 19 Einfügungen und 17 Löschungen.


Even when the `cryptography` library is available, we test the fallback
behavior of `derivepassphrase export vault` by inhibiting the loading of
`cryptography`.  As a result, we no longer need a separate testing
environment for the "no cryptography support" case, and can therefore
halve the number of configurations in our testing matrix.  We also no
longer need some associated configuration and helper objects.

In hindsight, I cannot believe I never tried to do a web search on how
to inhibit loading of a module in Python, and rather assumed that just
because pytest does not prominently offer a dedicated top-level function
or a dedicated `monkeypatch` method, that this is not supported.  But
seeing the underlying documentation for `importlib.import_module` and
the documentation of how `sys.modules[...] = None` affects the module
resolution, it becomes immediately obvious that this can be handled with
monkeypatching, and does not need a separate virtualenv.
... ...
@@ -391,23 +391,18 @@ dependencies = [
391 391
     'pytest < 8.4 ; python_version < "3.10"',
392 392
     'hypothesis != 6.130.13, != 6.131.*, != 6.132.*, != 6.133.*, != 6.134.*, != 6.135.0, != 6.135.1 ; python_version < "3.10"',
393 393
 ]
394
+features = ["export", "export-dev-wheels"]
394 395
 matrix-name-format = '{variable}_{value}'
395 396
 
396 397
 [[tool.hatch.envs.hatch-test.matrix]]
397 398
 python = ["3.14", "3.13", "3.12", "3.11", "3.10", "3.9", "pypy3.11", "pypy3.10", "pypy3.9"]
398
-cryptography = ["no", "yes"]
399 399
 parser-version = ["PEG"]
400 400
 
401 401
 [[tool.hatch.envs.hatch-test.matrix]]
402 402
 python = ["3.9", "pypy3.9"]
403
-cryptography = ["no", "yes"]
404 403
 parser-version = ["LL1"]
405 404
 
406 405
 [tool.hatch.envs.hatch-test.overrides]
407
-matrix.cryptography.features = [
408
-    { value = "export", if = ["yes"] },
409
-    { value = "export-dev-wheels", if = ["yes"] },
410
-]
411 406
 matrix.parser-version.env-vars = [
412 407
     { key = "PYTHONOLDPARSER", value = "1", if = ["LL1"] },
413 408
 ]
... ...
@@ -56,16 +56,6 @@ if TYPE_CHECKING:
56 56
 # =====
57 57
 
58 58
 
59
-skip_if_cryptography_support = pytest.mark.skipif(
60
-    importlib.util.find_spec("cryptography") is not None,
61
-    reason='cryptography support available; cannot test "no support" scenario',
62
-)
63
-"""
64
-A cached pytest mark to skip this test if cryptography support is
65
-available.  Usually this means that the test targets
66
-`derivepassphrase`'s fallback functionality, which is not available
67
-whenever the primary functionality is.
68
-"""
69 59
 skip_if_no_cryptography_support = pytest.mark.skipif(
70 60
     importlib.util.find_spec("cryptography") is None,
71 61
     reason='no "cryptography" support',
... ...
@@ -11,6 +11,7 @@ import operator
11 11
 import os
12 12
 import pathlib
13 13
 import string
14
+import sys
14 15
 import types
15 16
 from typing import TYPE_CHECKING, NamedTuple, TypeVar
16 17
 
... ...
@@ -473,7 +474,6 @@ class TestGenericVaultCLIErrors:
473 474
                 "expected error exit and known error message"
474 475
             )
475 476
 
476
-    @pytest_machinery.skip_if_cryptography_support
477 477
     @pytest_machinery.Parametrize.VAULT_CONFIG_FORMATS_DATA
478 478
     def test_no_cryptography_error_message(
479 479
         self,
... ...
@@ -484,6 +484,23 @@ class TestGenericVaultCLIErrors:
484 484
     ) -> None:
485 485
         """Abort export call if no cryptography is available."""
486 486
         del config_data
487
+        with pytest.MonkeyPatch.context() as monkeypatch:
488
+            monkeypatch.setitem(sys.modules, "cryptography", None)
489
+            monkeypatch.delitem(
490
+                sys.modules,
491
+                "derivepassphrase.exporter.vault_native",
492
+                raising=False,
493
+            )
494
+            monkeypatch.delitem(
495
+                sys.modules,
496
+                "derivepassphrase.exporter.storeroom",
497
+                raising=False,
498
+            )
499
+            monkeypatch.delitem(  # also reset export_data handler registry
500
+                sys.modules,
501
+                "derivepassphrase.exporter",
502
+                raising=False,
503
+            )
487 504
             result = self._call_cli(
488 505
                 ["-f", format, "VAULT_PATH"], vault_config=config
489 506
             )
490 507