diff --git a/newsfragments/4009.misc.rst b/newsfragments/4009.misc.rst new file mode 100644 index 0000000000..f127889cbc --- /dev/null +++ b/newsfragments/4009.misc.rst @@ -0,0 +1 @@ +Use default encoding to create ``.pth`` files with ``editable_wheel``. diff --git a/setuptools/command/editable_wheel.py b/setuptools/command/editable_wheel.py index d877276fec..91c215f2aa 100644 --- a/setuptools/command/editable_wheel.py +++ b/setuptools/command/editable_wheel.py @@ -11,6 +11,7 @@ """ import logging +import io import os import shutil import sys @@ -401,7 +402,7 @@ def __init__(self, dist: Distribution, name: str, path_entries: List[Path]): def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]): entries = "\n".join((str(p.resolve()) for p in self.path_entries)) - contents = bytes(f"{entries}\n", "utf-8") + contents = _encode_pth(f"{entries}\n") wheel.writestr(f"__editable__.{self.name}.pth", contents) def __enter__(self): @@ -509,7 +510,7 @@ def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str] content = bytes(_finder_template(name, roots, namespaces_), "utf-8") wheel.writestr(f"{finder}.py", content) - content = bytes(f"import {finder}; {finder}.install()", "utf-8") + content = _encode_pth(f"import {finder}; {finder}.install()") wheel.writestr(f"__editable__.{self.name}.pth", content) def __enter__(self): @@ -525,6 +526,24 @@ def __exit__(self, _exc_type, _exc_value, _traceback): InformationOnly.emit("Editable installation.", msg) +def _encode_pth(content: str) -> bytes: + """.pth files are always read with 'locale' encoding, the recommendation + from the cpython core developers is to write them as ``open(path, "w")`` + and ignore warnings (see python/cpython#77102, pypa/setuptools#3937). + This function tries to simulate this behaviour without having to create an + actual file, in a way that supports a range of active Python versions. + (There seems to be some variety in the way different version of Python handle + ``encoding=None``, not all of them use ``locale.getpreferredencoding(False)``). + """ + encoding = "locale" if sys.version_info >= (3, 10) else None + with io.BytesIO() as buffer: + wrapper = io.TextIOWrapper(buffer, encoding) + wrapper.write(content) + wrapper.flush() + buffer.seek(0) + return buffer.read() + + def _can_symlink_files(base_dir: Path) -> bool: with TemporaryDirectory(dir=str(base_dir.resolve())) as tmp: path1, path2 = Path(tmp, "file1.txt"), Path(tmp, "file2.txt") diff --git a/setuptools/tests/test_editable_install.py b/setuptools/tests/test_editable_install.py index 2abcaee8fd..0265611945 100644 --- a/setuptools/tests/test_editable_install.py +++ b/setuptools/tests/test_editable_install.py @@ -22,6 +22,7 @@ from setuptools.command.editable_wheel import ( _DebuggingTips, _LinkTree, + _encode_pth, _find_virtual_namespaces, _find_namespaces, _find_package_roots, @@ -1107,6 +1108,13 @@ def test_debugging_tips(tmpdir_cwd, monkeypatch): cmd.run() +@pytest.mark.filterwarnings("error") +def test_encode_pth(): + """Ensure _encode_pth function does not produce encoding warnings""" + content = _encode_pth("tkmilan_รง_utf8") # no warnings (would be turned into errors) + assert isinstance(content, bytes) + + def install_project(name, venv, tmp_path, files, *opts): project = tmp_path / name project.mkdir()