Skip to content

Commit 1ed7591

Browse files
authored
Adopt more UTF-8 (pypa#4309)
2 parents d756377 + 1c91ac8 commit 1ed7591

18 files changed

+199
-92
lines changed

newsfragments/4309.removal.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Further adoption of UTF-8 in ``setuptools``.
2+
This change regards mostly files produced and consumed during the build process
3+
(e.g. metadata files, script wrappers, automatically updated config files, etc..)
4+
Although precautions were taken to minimize disruptions, some edge cases might
5+
be subject to backwards incompatibility.
6+
7+
Support for ``"locale"`` encoding is now **deprecated**.

pkg_resources/__init__.py

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1524,8 +1524,7 @@ def run_script(self, script_name, namespace):
15241524
script_filename = self._fn(self.egg_info, script)
15251525
namespace['__file__'] = script_filename
15261526
if os.path.exists(script_filename):
1527-
with open(script_filename) as fid:
1528-
source = fid.read()
1527+
source = _read_utf8_with_fallback(script_filename)
15291528
code = compile(source, script_filename, 'exec')
15301529
exec(code, namespace, namespace)
15311530
else:
@@ -2175,11 +2174,10 @@ def non_empty_lines(path):
21752174
"""
21762175
Yield non-empty lines from file at path
21772176
"""
2178-
with open(path) as f:
2179-
for line in f:
2180-
line = line.strip()
2181-
if line:
2182-
yield line
2177+
for line in _read_utf8_with_fallback(path).splitlines():
2178+
line = line.strip()
2179+
if line:
2180+
yield line
21832181

21842182

21852183
def resolve_egg_link(path):
@@ -3323,3 +3321,35 @@ def _initialize_master_working_set():
33233321
# match order
33243322
list(map(working_set.add_entry, sys.path))
33253323
globals().update(locals())
3324+
3325+
3326+
# ---- Ported from ``setuptools`` to avoid introducing an import inter-dependency ----
3327+
LOCALE_ENCODING = "locale" if sys.version_info >= (3, 10) else None
3328+
3329+
3330+
def _read_utf8_with_fallback(file: str, fallback_encoding=LOCALE_ENCODING) -> str:
3331+
"""See setuptools.unicode_utils._read_utf8_with_fallback"""
3332+
try:
3333+
with open(file, "r", encoding="utf-8") as f:
3334+
return f.read()
3335+
except UnicodeDecodeError: # pragma: no cover
3336+
msg = f"""\
3337+
********************************************************************************
3338+
`encoding="utf-8"` fails with {file!r}, trying `encoding={fallback_encoding!r}`.
3339+
3340+
This fallback behaviour is considered **deprecated** and future versions of
3341+
`setuptools/pkg_resources` may not implement it.
3342+
3343+
Please encode {file!r} with "utf-8" to ensure future builds will succeed.
3344+
3345+
If this file was produced by `setuptools` itself, cleaning up the cached files
3346+
and re-building/re-installing the package with a newer version of `setuptools`
3347+
(e.g. by updating `build-system.requires` in its `pyproject.toml`)
3348+
might solve the problem.
3349+
********************************************************************************
3350+
"""
3351+
# TODO: Add a deadline?
3352+
# See comment in setuptools.unicode_utils._Utf8EncodingNeeded
3353+
warnings.warns(msg, PkgResourcesDeprecationWarning, stacklevel=2)
3354+
with open(file, "r", encoding=fallback_encoding) as f:
3355+
return f.read()

setuptools/_imp.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import os
77
import importlib.util
88
import importlib.machinery
9+
import tokenize
910

1011
from importlib.util import module_from_spec
1112

@@ -60,13 +61,13 @@ def find_module(module, paths=None):
6061

6162
if suffix in importlib.machinery.SOURCE_SUFFIXES:
6263
kind = PY_SOURCE
64+
file = tokenize.open(path)
6365
elif suffix in importlib.machinery.BYTECODE_SUFFIXES:
6466
kind = PY_COMPILED
67+
file = open(path, 'rb')
6568
elif suffix in importlib.machinery.EXTENSION_SUFFIXES:
6669
kind = C_EXTENSION
6770

68-
if kind in {PY_SOURCE, PY_COMPILED}:
69-
file = open(path, mode)
7071
else:
7172
path = None
7273
suffix = mode = ''

setuptools/command/bdist_egg.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def __bootstrap__():
5454
__bootstrap__()
5555
"""
5656
).lstrip()
57-
with open(pyfile, 'w') as f:
57+
with open(pyfile, 'w', encoding="utf-8") as f:
5858
f.write(_stub_template % resource)
5959

6060

@@ -200,10 +200,9 @@ def run(self): # noqa: C901 # is too complex (14) # FIXME
200200
log.info("writing %s", native_libs)
201201
if not self.dry_run:
202202
ensure_directory(native_libs)
203-
libs_file = open(native_libs, 'wt')
204-
libs_file.write('\n'.join(all_outputs))
205-
libs_file.write('\n')
206-
libs_file.close()
203+
with open(native_libs, 'wt', encoding="utf-8") as libs_file:
204+
libs_file.write('\n'.join(all_outputs))
205+
libs_file.write('\n')
207206
elif os.path.isfile(native_libs):
208207
log.info("removing %s", native_libs)
209208
if not self.dry_run:
@@ -350,9 +349,8 @@ def write_safety_flag(egg_dir, safe):
350349
if safe is None or bool(safe) != flag:
351350
os.unlink(fn)
352351
elif safe is not None and bool(safe) == flag:
353-
f = open(fn, 'wt')
354-
f.write('\n')
355-
f.close()
352+
with open(fn, 'wt', encoding="utf-8") as f:
353+
f.write('\n')
356354

357355

358356
safety_flags = {

setuptools/command/build_ext.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -342,9 +342,8 @@ def _write_stub_file(self, stub_file: str, ext: Extension, compile=False):
342342
if compile and os.path.exists(stub_file):
343343
raise BaseError(stub_file + " already exists! Please delete.")
344344
if not self.dry_run:
345-
f = open(stub_file, 'w')
346-
f.write(
347-
'\n'.join([
345+
with open(stub_file, 'w', encoding="utf-8") as f:
346+
content = '\n'.join([
348347
"def __bootstrap__():",
349348
" global __bootstrap__, __file__, __loader__",
350349
" import sys, os, pkg_resources, importlib.util" + if_dl(", dl"),
@@ -368,8 +367,7 @@ def _write_stub_file(self, stub_file: str, ext: Extension, compile=False):
368367
"__bootstrap__()",
369368
"", # terminal \n
370369
])
371-
)
372-
f.close()
370+
f.write(content)
373371
if compile:
374372
self._compile_and_remove_stub(stub_file)
375373

setuptools/command/develop.py

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
from setuptools import namespaces
1111
import setuptools
1212

13+
from ..unicode_utils import _read_utf8_with_fallback
14+
1315

1416
class develop(namespaces.DevelopInstaller, easy_install):
1517
"""Set up package for development"""
@@ -119,7 +121,7 @@ def install_for_development(self):
119121
# create an .egg-link in the installation dir, pointing to our egg
120122
log.info("Creating %s (link to %s)", self.egg_link, self.egg_base)
121123
if not self.dry_run:
122-
with open(self.egg_link, "w") as f:
124+
with open(self.egg_link, "w", encoding="utf-8") as f:
123125
f.write(self.egg_path + "\n" + self.setup_path)
124126
# postprocess the installed distro, fixing up .pth, installing scripts,
125127
# and handling requirements
@@ -128,9 +130,12 @@ def install_for_development(self):
128130
def uninstall_link(self):
129131
if os.path.exists(self.egg_link):
130132
log.info("Removing %s (link to %s)", self.egg_link, self.egg_base)
131-
egg_link_file = open(self.egg_link)
132-
contents = [line.rstrip() for line in egg_link_file]
133-
egg_link_file.close()
133+
134+
contents = [
135+
line.rstrip()
136+
for line in _read_utf8_with_fallback(self.egg_link).splitlines()
137+
]
138+
134139
if contents not in ([self.egg_path], [self.egg_path, self.setup_path]):
135140
log.warn("Link points to %s: uninstall aborted", contents)
136141
return
@@ -156,8 +161,7 @@ def install_egg_scripts(self, dist):
156161
for script_name in self.distribution.scripts or []:
157162
script_path = os.path.abspath(convert_path(script_name))
158163
script_name = os.path.basename(script_path)
159-
with open(script_path) as strm:
160-
script_text = strm.read()
164+
script_text = _read_utf8_with_fallback(script_path)
161165
self.install_script(dist, script_name, script_text, script_path)
162166

163167
return None

setuptools/command/easy_install.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -873,7 +873,9 @@ def write_script(self, script_name, contents, mode="t", blockers=()):
873873
ensure_directory(target)
874874
if os.path.exists(target):
875875
os.unlink(target)
876-
with open(target, "w" + mode) as f: # TODO: is it safe to use utf-8?
876+
877+
encoding = None if "b" in mode else "utf-8"
878+
with open(target, "w" + mode, encoding=encoding) as f:
877879
f.write(contents)
878880
chmod(target, 0o777 - mask)
879881

@@ -1017,12 +1019,11 @@ def install_exe(self, dist_filename, tmpdir):
10171019

10181020
# Write EGG-INFO/PKG-INFO
10191021
if not os.path.exists(pkg_inf):
1020-
f = open(pkg_inf, 'w') # TODO: probably it is safe to use utf-8
1021-
f.write('Metadata-Version: 1.0\n')
1022-
for k, v in cfg.items('metadata'):
1023-
if k != 'target_version':
1024-
f.write('%s: %s\n' % (k.replace('_', '-').title(), v))
1025-
f.close()
1022+
with open(pkg_inf, 'w', encoding="utf-8") as f:
1023+
f.write('Metadata-Version: 1.0\n')
1024+
for k, v in cfg.items('metadata'):
1025+
if k != 'target_version':
1026+
f.write('%s: %s\n' % (k.replace('_', '-').title(), v))
10261027
script_dir = os.path.join(_egg_info, 'scripts')
10271028
# delete entry-point scripts to avoid duping
10281029
self.delete_blockers([
@@ -1088,9 +1089,8 @@ def process(src, dst):
10881089
if locals()[name]:
10891090
txt = os.path.join(egg_tmp, 'EGG-INFO', name + '.txt')
10901091
if not os.path.exists(txt):
1091-
f = open(txt, 'w') # TODO: probably it is safe to use utf-8
1092-
f.write('\n'.join(locals()[name]) + '\n')
1093-
f.close()
1092+
with open(txt, 'w', encoding="utf-8") as f:
1093+
f.write('\n'.join(locals()[name]) + '\n')
10941094

10951095
def install_wheel(self, wheel_path, tmpdir):
10961096
wheel = Wheel(wheel_path)

setuptools/command/install_scripts.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,10 @@ def write_script(self, script_name, contents, mode="t", *ignored):
5757
target = os.path.join(self.install_dir, script_name)
5858
self.outfiles.append(target)
5959

60+
encoding = None if "b" in mode else "utf-8"
6061
mask = current_umask()
6162
if not self.dry_run:
6263
ensure_directory(target)
63-
f = open(target, "w" + mode)
64-
f.write(contents)
65-
f.close()
64+
with open(target, "w" + mode, encoding=encoding) as f:
65+
f.write(contents)
6666
chmod(target, 0o777 - mask)

setuptools/command/setopt.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
import os
66
import configparser
77

8-
from setuptools import Command
8+
from .. import Command
9+
from ..unicode_utils import _cfg_read_utf8_with_fallback
910

1011
__all__ = ['config_file', 'edit_config', 'option_base', 'setopt']
1112

@@ -36,7 +37,8 @@ def edit_config(filename, settings, dry_run=False):
3637
log.debug("Reading configuration from %s", filename)
3738
opts = configparser.RawConfigParser()
3839
opts.optionxform = lambda x: x
39-
opts.read([filename])
40+
_cfg_read_utf8_with_fallback(opts, filename)
41+
4042
for section, options in settings.items():
4143
if options is None:
4244
log.info("Deleting section [%s] from %s", section, filename)
@@ -62,7 +64,7 @@ def edit_config(filename, settings, dry_run=False):
6264

6365
log.info("Writing %s", filename)
6466
if not dry_run:
65-
with open(filename, 'w') as f:
67+
with open(filename, 'w', encoding="utf-8") as f:
6668
opts.write(f)
6769

6870

setuptools/dist.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -685,7 +685,7 @@ def get_egg_cache_dir(self):
685685
os.mkdir(egg_cache_dir)
686686
windows_support.hide_file(egg_cache_dir)
687687
readme_txt_filename = os.path.join(egg_cache_dir, 'README.txt')
688-
with open(readme_txt_filename, 'w') as f:
688+
with open(readme_txt_filename, 'w', encoding="utf-8") as f:
689689
f.write(
690690
'This directory contains eggs that were downloaded '
691691
'by setuptools to build, test, and run plug-ins.\n\n'

0 commit comments

Comments
 (0)