1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
|
From: Kurt McKee <contactme@kurtmckee.org>
Date: Wed, 20 Aug 2025 09:32:27 -0500
Subject: Fix a test failure caused by an internal docutils `open()` call
docutils 0.22 introduced an RST stylesheet feature
that must be able to open files during RST-to-HTML rendering.
The original design of the `test_cli_explicit_format` test
patched `pathlib.Path.open()` so it would always return the
same open file instance...which was always closed on first use.
This caused a failure when docutils tried to open `minimal.css`:
```
ValueError: I/O operation on closed file.
```
This change introduces a more complex mocking strategy
that documents and meets the current technical requirements.
Origin: https://github.com/pypa/readme_renderer/pull/332
---
tests/test_cli.py | 47 +++++++++++++++++++++++++++++++++++++++++------
1 file changed, 41 insertions(+), 6 deletions(-)
diff --git a/tests/test_cli.py b/tests/test_cli.py
index dddf5c3..3e8c928 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -46,12 +46,47 @@ def test_cli_invalid_format():
def test_cli_explicit_format(input_file):
fmt = input_file.suffix.lstrip(".")
- with input_file.open() as fp, \
- mock.patch("pathlib.Path.open", return_value=fp), \
- mock.patch("builtins.print") as print_:
- main(["-f", fmt, "no-file.invalid"])
- print_.assert_called_once()
- (result,), _ = print_.call_args
+
+ # The explicit-format tests present a number of technical challenges.
+ #
+ # 1. The filename must not have a recognized extension
+ # to ensure that the renderer is honoring the `-f` argument.
+ # Therefore, patching is used so the input filename can be faked.
+ #
+ # 2. docutils must be able to open stylesheet files during RST-to-HTML rendering.
+ # Therefore, patching must be limited to readme-renderer's usage.
+ #
+ # The strategy used here is to patch `pathlib.Path.open()` until it is first called,
+ # since the first call occurs when readme-renderer reads the input file's content.
+ # After that, the patch is disabled. However, this presents additional challenges:
+ #
+ # 3. On Python <=3.11, `patch.__exit__()` cannot be called more than once.
+ # 4. `patch.stop()` cannot sufficiently undo a patch if `.__enter__()` was called.
+ #
+ # Therefore, the patch cannot be used as a context manager.
+ # It must be manually started and stopped.
+
+ fp = input_file.open()
+
+ def stop_open_patch_after_one_call():
+ open_patch.stop()
+ return fp
+
+ open_patch = mock.patch(
+ "pathlib.Path.open",
+ side_effect=stop_open_patch_after_one_call,
+ )
+
+ open_patch.start()
+
+ try:
+ with fp, mock.patch("builtins.print") as print_:
+ main(["-f", fmt, "no-file.invalid"])
+ print_.assert_called_once()
+ (result,), _ = print_.call_args
+ finally:
+ # Stop the patch regardless of the test result.
+ open_patch.stop()
with input_file.with_suffix(".html").open() as fp:
assert result.strip() == fp.read().strip()
|