Skip to content

Commit 9021ee3

Browse files
authored
Merge pull request #284 from d-chris/d-chris/issue283
introduced option to specify file encoding
2 parents 5227a45 + f0ec39e commit 9021ee3

File tree

3 files changed

+194
-7
lines changed

3 files changed

+194
-7
lines changed

pytest_doctestplus/plugin.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ def parse(self, s, name=None):
444444
continue
445445

446446
if config.getoption('remote_data', 'none') != 'any':
447-
if any(re.match(fr'{comment_char}\s+doctest-remote-data-all\s*::', x.strip())
447+
if any(re.match(fr'{comment_char}\s+doctest-remote-data-all\s*::', x.strip()) # noqa: E501
448448
for x in lines):
449449
skip_all = True
450450
continue
@@ -912,13 +912,13 @@ def test_filter(test):
912912
return tests
913913

914914

915-
def write_modified_file(fname, new_fname, changes):
915+
def write_modified_file(fname, new_fname, changes, encoding=None):
916916
# Sort in reversed order to edit the lines:
917917
bad_tests = []
918918
changes.sort(key=lambda x: (x["test_lineno"], x["example_lineno"]),
919919
reverse=True)
920920

921-
with open(fname) as f:
921+
with open(fname, encoding=encoding) as f:
922922
text = f.readlines()
923923

924924
for change in changes:
@@ -939,7 +939,7 @@ def write_modified_file(fname, new_fname, changes):
939939

940940
text[lineno:lineno+want.count("\n")] = [got]
941941

942-
with open(new_fname, "w") as f:
942+
with open(new_fname, "w", encoding=encoding) as f:
943943
f.write("".join(text))
944944

945945
return bad_tests
@@ -954,6 +954,8 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config):
954954
if not diff_mode:
955955
return # we do not report or apply diffs
956956

957+
encoding = config.getini("doctest_encoding")
958+
957959
if diff_mode != "overwrite":
958960
# In this mode, we write a corrected file to a temporary folder in
959961
# order to compare them (rather than modifying the file).
@@ -974,14 +976,14 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config):
974976
new_fname = fname.replace(common_path, tmpdirname)
975977
os.makedirs(os.path.split(new_fname)[0], exist_ok=True)
976978

977-
bad_tests = write_modified_file(fname, new_fname, changes)
979+
bad_tests = write_modified_file(fname, new_fname, changes, encoding)
978980
all_bad_tests.extend(bad_tests)
979981

980982
# git diff returns 1 to signal changes, so just ignore the
981983
# exit status:
982984
with subprocess.Popen(
983985
["git", "diff", "-p", "--no-index", fname, new_fname],
984-
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) as p:
986+
stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding=encoding) as p:
985987
p.wait()
986988
# Diff should be fine, but write error if not:
987989
diff = p.stderr.read()
@@ -1013,7 +1015,7 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config):
10131015
return
10141016
terminalreporter.write_line("Applied fix to the following files:")
10151017
for fname, changes in changesets.items():
1016-
bad_tests = write_modified_file(fname, fname, changes)
1018+
bad_tests = write_modified_file(fname, fname, changes, encoding)
10171019
all_bad_tests.extend(bad_tests)
10181020
terminalreporter.write_line(f" {fname}")
10191021

pytest_doctestplus/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from importlib.metadata import distribution
44
from packaging.requirements import Requirement
55

6+
67
class ModuleChecker:
78

89
def find_module(self, module):

tests/test_encoding.py

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import locale
2+
import os
3+
from pathlib import Path
4+
from textwrap import dedent
5+
from typing import Callable, Optional
6+
7+
import pytest
8+
9+
pytest_plugins = ["pytester"]
10+
11+
12+
@pytest.fixture(
13+
params=[
14+
("A", "a", "utf-8"),
15+
("☆", "★", "utf-8"),
16+
("b", "B", "cp1252"),
17+
("☁", "☀", "utf-8"),
18+
],
19+
ids=[
20+
"Aa-utf8",
21+
"star-utf8",
22+
"bB-cp1252",
23+
"cloud-utf8",
24+
],
25+
)
26+
def charset(request):
27+
return request.param
28+
29+
30+
@pytest.fixture()
31+
def basic_file(tmp_path: Path) -> Callable[[str, str, str], tuple[str, str, str]]:
32+
33+
def makebasicfile(a, b, encoding: str) -> tuple[str, str, str]:
34+
"""alternative implementation without the use of `testdir.makepyfile`."""
35+
36+
content = """
37+
def f():
38+
'''
39+
>>> print('{}')
40+
{}
41+
'''
42+
pass
43+
"""
44+
45+
original = dedent(content.format(a, b))
46+
expected_result = dedent(content.format(a, a))
47+
48+
original_file = tmp_path.joinpath("test_basic.py")
49+
original_file.write_text(original, encoding=encoding)
50+
51+
expected_diff = dedent(
52+
f"""
53+
>>> print('{a}')
54+
- {b}
55+
+ {a}
56+
"""
57+
).strip("\n")
58+
59+
return str(original_file), expected_diff, expected_result
60+
61+
return makebasicfile
62+
63+
64+
@pytest.fixture()
65+
def ini_file(testdir) -> Callable[..., Path]:
66+
67+
def makeini(
68+
encoding: Optional[str] = None,
69+
) -> Path:
70+
"""Create a pytest.ini file with the specified encoding."""
71+
72+
ini = ["[pytest]"]
73+
74+
if encoding is not None:
75+
ini.append(f"doctest_encoding = {encoding}")
76+
77+
ini.append("")
78+
79+
p = testdir.makefile(".ini", pytest="\n".join(ini))
80+
81+
return Path(p)
82+
83+
return makeini
84+
85+
86+
def test_basic_file_encoding_diff(testdir, capsys, basic_file, charset, ini_file):
87+
"""
88+
Test the diff from console output is as expected.
89+
"""
90+
a, b, encoding = charset
91+
92+
# create python file to test
93+
file, diff, _ = basic_file(a, b, encoding)
94+
95+
# create pytest.ini file
96+
ini = ini_file(encoding=encoding)
97+
assert ini.is_file(), "setup pytest.ini not created/found"
98+
99+
testdir.inline_run(
100+
file,
101+
"--doctest-plus-generate-diff",
102+
"-c",
103+
str(ini),
104+
)
105+
106+
stdout, _ = capsys.readouterr()
107+
assert diff in stdout
108+
109+
110+
def test_basic_file_encoding_overwrite(testdir, basic_file, charset, ini_file):
111+
"""
112+
Test that the file is overwritten with the expected content.
113+
"""
114+
115+
a, b, encoding = charset
116+
117+
# create python file to test
118+
file, _, expected = basic_file(a, b, encoding)
119+
120+
# create pytest.ini file
121+
ini = ini_file(encoding=encoding)
122+
assert ini.is_file(), "setup pytest.ini not created/found"
123+
124+
testdir.inline_run(
125+
file,
126+
"--doctest-plus-generate-diff",
127+
"overwrite",
128+
"-c",
129+
str(ini),
130+
)
131+
132+
assert expected in Path(file).read_text(encoding)
133+
134+
135+
@pytest.mark.skipif(os.getenv("CI", False), reason="skip on CI")
136+
def test_legacy_diff(testdir, capsys, basic_file, charset):
137+
"""
138+
Legacy test are supported to fail on Windows, when no encoding is provided.
139+
140+
On Windows this is cp1252, so "utf-8" are expected to fail while writing test files.
141+
"""
142+
a, b, _ = charset
143+
144+
try:
145+
file, diff, _ = basic_file(a, b, None)
146+
except UnicodeEncodeError:
147+
encoding = locale.getpreferredencoding(False)
148+
reason = f"could not encode {repr(charset)} with {encoding=}"
149+
pytest.xfail(reason=reason)
150+
151+
testdir.inline_run(
152+
file,
153+
"--doctest-plus-generate-diff",
154+
)
155+
156+
stdout, _ = capsys.readouterr()
157+
158+
assert diff in stdout
159+
160+
161+
@pytest.mark.skipif(os.getenv("CI", False), reason="skip on CI")
162+
def test_legacy_overwrite(testdir, basic_file, charset):
163+
"""
164+
Legacy test are supported to fail on Windows, when no encoding is provided.
165+
166+
On Windows this is cp1252, so "utf-8" are expected to fail while writing test files.
167+
"""
168+
169+
a, b, _encoding = charset
170+
171+
try:
172+
file, _, expected = basic_file(a, b, None)
173+
except UnicodeEncodeError:
174+
encoding = locale.getpreferredencoding(False)
175+
reason = f"could not encode {repr(charset)} with {encoding=}"
176+
pytest.xfail(reason=reason)
177+
178+
testdir.inline_run(
179+
file,
180+
"--doctest-plus-generate-diff",
181+
"overwrite",
182+
)
183+
184+
assert expected in Path(file).read_text(_encoding)

0 commit comments

Comments
 (0)