Skip to content

Commit

Permalink
Adopt UTF-8 for .pth files on Python 3.13 (#4306)
Browse files Browse the repository at this point in the history
  • Loading branch information
abravalheri authored Aug 28, 2024
2 parents 738dd48 + 353d0aa commit 2fca902
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 20 deletions.
48 changes: 34 additions & 14 deletions setuptools/command/easy_install.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
from setuptools.wheel import Wheel

from .._path import ensure_directory
from ..compat import py39, py311
from ..compat import py39, py311, py312

from distutils import dir_util, log
from distutils.command import install
Expand Down Expand Up @@ -590,8 +590,9 @@ def check_pth_processing(self): # noqa: C901
os.unlink(ok_file)
dirname = os.path.dirname(ok_file)
os.makedirs(dirname, exist_ok=True)
f = open(pth_file, 'w', encoding=py39.LOCALE_ENCODING)
# ^-- Requires encoding="locale" instead of "utf-8" (python/cpython#77102).
f = open(pth_file, 'w', encoding=py312.PTH_ENCODING)
# ^-- Python<3.13 require encoding="locale" instead of "utf-8",
# see python/cpython#77102.
except OSError:
self.cant_write_to_target()
else:
Expand Down Expand Up @@ -1282,8 +1283,9 @@ def update_pth(self, dist): # noqa: C901 # is too complex (11) # FIXME
if os.path.islink(filename):
os.unlink(filename)

with open(filename, 'wt', encoding=py39.LOCALE_ENCODING) as f:
# Requires encoding="locale" instead of "utf-8" (python/cpython#77102).
with open(filename, 'wt', encoding=py312.PTH_ENCODING) as f:
# ^-- Python<3.13 require encoding="locale" instead of "utf-8",
# see python/cpython#77102.
f.write(self.pth_file.make_relative(dist.location) + '\n')

def unpack_progress(self, src, dst):
Expand Down Expand Up @@ -1509,9 +1511,8 @@ def expand_paths(inputs): # noqa: C901 # is too complex (11) # FIXME
continue

# Read the .pth file
with open(os.path.join(dirname, name), encoding=py39.LOCALE_ENCODING) as f:
# Requires encoding="locale" instead of "utf-8" (python/cpython#77102).
lines = list(yield_lines(f))
content = _read_pth(os.path.join(dirname, name))
lines = list(yield_lines(content))

# Yield existing non-dupe, non-import directory lines from it
for line in lines:
Expand Down Expand Up @@ -1625,9 +1626,8 @@ def _load_raw(self):
paths = []
dirty = saw_import = False
seen = set(self.sitedirs)
f = open(self.filename, 'rt', encoding=py39.LOCALE_ENCODING)
# ^-- Requires encoding="locale" instead of "utf-8" (python/cpython#77102).
for line in f:
content = _read_pth(self.filename)
for line in content.splitlines():
path = line.rstrip()
# still keep imports and empty/commented lines for formatting
paths.append(path)
Expand All @@ -1646,7 +1646,6 @@ def _load_raw(self):
paths.pop()
continue
seen.add(normalized_path)
f.close()
# remove any trailing empty/blank line
while paths and not paths[-1].strip():
paths.pop()
Expand Down Expand Up @@ -1697,8 +1696,9 @@ def save(self):
data = '\n'.join(lines) + '\n'
if os.path.islink(self.filename):
os.unlink(self.filename)
with open(self.filename, 'wt', encoding=py39.LOCALE_ENCODING) as f:
# Requires encoding="locale" instead of "utf-8" (python/cpython#77102).
with open(self.filename, 'wt', encoding=py312.PTH_ENCODING) as f:
# ^-- Python<3.13 require encoding="locale" instead of "utf-8",
# see python/cpython#77102.
f.write(data)
elif os.path.exists(self.filename):
log.debug("Deleting empty %s", self.filename)
Expand Down Expand Up @@ -2357,6 +2357,26 @@ def only_strs(values):
return filter(lambda val: isinstance(val, str), values)


def _read_pth(fullname: str) -> str:
# Python<3.13 require encoding="locale" instead of "utf-8", see python/cpython#77102
# In the case old versions of setuptools are producing `pth` files with
# different encodings that might be problematic... So we fallback to "locale".

try:
with open(fullname, encoding=py312.PTH_ENCODING) as f:
return f.read()
except UnicodeDecodeError: # pragma: no cover
# This error may only happen for Python >= 3.13
# TODO: Possible deprecation warnings to be added in the future:
# ``.pth file {fullname!r} is not UTF-8.``
# Your environment contain {fullname!r} that cannot be read as UTF-8.
# This is likely to have been produced with an old version of setuptools.
# Please be mindful that this is deprecated and in the future, non-utf8
# .pth files may cause setuptools to fail.
with open(fullname, encoding=py39.LOCALE_ENCODING) as f:
return f.read()


class EasyInstallDeprecationWarning(SetuptoolsDeprecationWarning):
_SUMMARY = "easy_install command is deprecated."
_DETAILS = """
Expand Down
9 changes: 6 additions & 3 deletions setuptools/command/editable_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@

from .. import Command, _normalization, _path, errors, namespaces
from .._path import StrPath
from ..compat import py39
from ..compat import py312
from ..discovery import find_package_path
from ..dist import Distribution
from ..warnings import InformationOnly, SetuptoolsDeprecationWarning, SetuptoolsWarning
Expand Down Expand Up @@ -561,7 +561,9 @@ def __exit__(


def _encode_pth(content: str) -> bytes:
""".pth files are always read with 'locale' encoding, the recommendation
"""
Prior to Python 3.13 (see https://github.com/python/cpython/issues/77102),
.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
Expand All @@ -571,7 +573,8 @@ def _encode_pth(content: str) -> bytes:
or ``locale.getencoding()``).
"""
with io.BytesIO() as buffer:
wrapper = io.TextIOWrapper(buffer, encoding=py39.LOCALE_ENCODING)
wrapper = io.TextIOWrapper(buffer, encoding=py312.PTH_ENCODING)
# TODO: Python 3.13 replace the whole function with `bytes(content, "utf-8")`
wrapper.write(content)
wrapper.flush()
buffer.seek(0)
Expand Down
13 changes: 13 additions & 0 deletions setuptools/compat/py312.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from __future__ import annotations

import sys

if sys.version_info >= (3, 12, 4):
# Python 3.13 should support `.pth` files encoded in UTF-8
# See discussion in https://github.com/python/cpython/issues/77102
PTH_ENCODING: str | None = "utf-8"
else:
from .py39 import LOCALE_ENCODING

# PTH_ENCODING = "locale"
PTH_ENCODING = LOCALE_ENCODING
7 changes: 4 additions & 3 deletions setuptools/namespaces.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import itertools
import os

from .compat import py39
from .compat import py312

from distutils import log

Expand All @@ -25,8 +25,9 @@ def install_namespaces(self):
list(lines)
return

with open(filename, 'wt', encoding=py39.LOCALE_ENCODING) as f:
# Requires encoding="locale" instead of "utf-8" (python/cpython#77102).
with open(filename, 'wt', encoding=py312.PTH_ENCODING) as f:
# Python<3.13 requires encoding="locale" instead of "utf-8"
# See: python/cpython#77102
f.writelines(lines)

def uninstall_namespaces(self):
Expand Down

0 comments on commit 2fca902

Please sign in to comment.