Fix missing imports in setup scripts generated by packaging (#13205).

I’ve made more edits than the bug report suggested to make sure the
generated setup script is compatible with many Python versions; a
comment in the source explains that in detail.

The cfg_to_args function uses old 2.x idioms like codecs.open and
RawConfigParser.readfp because I want the setup.py generated by packaging and
distutils2 to be the same.  Most users won’t see the deprecation warning and I
ignore it in the test suite.

Thanks to David Barnett for the report and original patch.
This commit is contained in:
Éric Araujo 2011-10-21 06:27:06 +02:00
parent 3bb8be6d78
commit f89ebdc358
3 changed files with 74 additions and 17 deletions

View File

@ -5,11 +5,10 @@ import time
import logging import logging
import tempfile import tempfile
import textwrap import textwrap
import warnings
import subprocess import subprocess
from io import StringIO from io import StringIO
from packaging.tests import support, unittest
from packaging.tests.test_config import SETUP_CFG
from packaging.errors import ( from packaging.errors import (
PackagingPlatformError, PackagingByteCompileError, PackagingFileError, PackagingPlatformError, PackagingByteCompileError, PackagingFileError,
PackagingExecError, InstallationException) PackagingExecError, InstallationException)
@ -20,7 +19,11 @@ from packaging.util import (
get_compiler_versions, _MAC_OS_X_LD_VERSION, byte_compile, find_packages, get_compiler_versions, _MAC_OS_X_LD_VERSION, byte_compile, find_packages,
spawn, get_pypirc_path, generate_pypirc, read_pypirc, resolve_name, iglob, spawn, get_pypirc_path, generate_pypirc, read_pypirc, resolve_name, iglob,
RICH_GLOB, egginfo_to_distinfo, is_setuptools, is_distutils, is_packaging, RICH_GLOB, egginfo_to_distinfo, is_setuptools, is_distutils, is_packaging,
get_install_method, cfg_to_args, encode_multipart) get_install_method, cfg_to_args, generate_setup_py, encode_multipart)
from packaging.tests import support, unittest
from packaging.tests.test_config import SETUP_CFG
from test.script_helper import assert_python_ok, assert_python_failure
PYPIRC = """\ PYPIRC = """\
@ -513,7 +516,9 @@ class UtilTestCase(support.EnvironRestorer,
self.write_file('setup.cfg', SETUP_CFG % opts, encoding='utf-8') self.write_file('setup.cfg', SETUP_CFG % opts, encoding='utf-8')
self.write_file('README', 'loooong description') self.write_file('README', 'loooong description')
args = cfg_to_args() with warnings.catch_warnings():
warnings.simplefilter('ignore', DeprecationWarning)
args = cfg_to_args()
# use Distribution to get the contents of the setup.cfg file # use Distribution to get the contents of the setup.cfg file
dist = Distribution() dist = Distribution()
dist.parse_config_files() dist.parse_config_files()
@ -539,6 +544,26 @@ class UtilTestCase(support.EnvironRestorer,
self.assertEqual(args['scripts'], dist.scripts) self.assertEqual(args['scripts'], dist.scripts)
self.assertEqual(args['py_modules'], dist.py_modules) self.assertEqual(args['py_modules'], dist.py_modules)
def test_generate_setup_py(self):
# undo subprocess.Popen monkey-patching before using assert_python_*
subprocess.Popen = self.old_popen
os.chdir(self.mkdtemp())
self.write_file('setup.cfg', textwrap.dedent("""\
[metadata]
name = SPAM
classifier = Programming Language :: Python
"""))
generate_setup_py()
self.assertTrue(os.path.exists('setup.py'), 'setup.py not created')
rc, out, err = assert_python_ok('setup.py', '--name')
self.assertEqual(out, b'SPAM\n')
self.assertEqual(err, b'')
# a generated setup.py should complain if no setup.cfg is present
os.unlink('setup.cfg')
rc, out, err = assert_python_failure('setup.py', '--name')
self.assertIn(b'setup.cfg', err)
def test_encode_multipart(self): def test_encode_multipart(self):
fields = [('username', 'wok'), ('password', 'secret')] fields = [('username', 'wok'), ('password', 'secret')]
files = [('picture', 'wok.png', b'PNG89')] files = [('picture', 'wok.png', b'PNG89')]
@ -590,7 +615,6 @@ class GlobTestCase(GlobTestCaseBase):
super(GlobTestCase, self).tearDown() super(GlobTestCase, self).tearDown()
def assertGlobMatch(self, glob, spec): def assertGlobMatch(self, glob, spec):
""""""
tempdir = self.build_files_tree(spec) tempdir = self.build_files_tree(spec)
expected = self.clean_tree(spec) expected = self.clean_tree(spec)
os.chdir(tempdir) os.chdir(tempdir)

View File

@ -6,6 +6,7 @@ import csv
import imp import imp
import sys import sys
import errno import errno
import codecs
import shutil import shutil
import string import string
import hashlib import hashlib
@ -417,7 +418,8 @@ byte_compile(files, optimize=%r, force=%r,
# cfile - byte-compiled file # cfile - byte-compiled file
# dfile - purported source filename (same as 'file' by default) # dfile - purported source filename (same as 'file' by default)
if optimize >= 0: if optimize >= 0:
cfile = imp.cache_from_source(file, debug_override=not optimize) cfile = imp.cache_from_source(file,
debug_override=not optimize)
else: else:
cfile = imp.cache_from_source(file) cfile = imp.cache_from_source(file)
dfile = file dfile = file
@ -931,6 +933,24 @@ def _iglob(path_glob):
yield file yield file
# HOWTO change cfg_to_args
#
# This function has two major constraints: It is copied by inspect.getsource
# in generate_setup_py; it is used in generated setup.py which may be run by
# any Python version supported by distutils2 (2.4-3.3).
#
# * Keep objects like D1_D2_SETUP_ARGS static, i.e. in the function body
# instead of global.
# * If you use a function from another module, update the imports in
# SETUP_TEMPLATE. Use only modules, classes and functions compatible with
# all versions: codecs.open instead of open, RawConfigParser.readfp instead
# of read, standard exceptions instead of Packaging*Error, etc.
# * If you use a function from this module, update the template and
# generate_setup_py.
#
# test_util tests this function and the generated setup.py, but does not test
# that it's compatible with all Python versions.
def cfg_to_args(path='setup.cfg'): def cfg_to_args(path='setup.cfg'):
"""Compatibility helper to use setup.cfg in setup.py. """Compatibility helper to use setup.cfg in setup.py.
@ -941,8 +961,6 @@ def cfg_to_args(path='setup.cfg'):
*file* is the path to the setup.cfg file. If it doesn't exist, *file* is the path to the setup.cfg file. If it doesn't exist,
PackagingFileError is raised. PackagingFileError is raised.
""" """
# We need to declare the following constants here so that it's easier to
# generate the setup.py afterwards, using inspect.getsource.
# XXX ** == needs testing # XXX ** == needs testing
D1_D2_SETUP_ARGS = {"name": ("metadata",), D1_D2_SETUP_ARGS = {"name": ("metadata",),
@ -986,10 +1004,11 @@ def cfg_to_args(path='setup.cfg'):
# The real code starts here # The real code starts here
config = RawConfigParser() config = RawConfigParser()
if not os.path.exists(path): f = codecs.open(path, encoding='utf-8')
raise PackagingFileError("file '%s' does not exist" % try:
os.path.abspath(path)) config.readfp(f)
config.read(path, encoding='utf-8') finally:
f.close()
kwargs = {} kwargs = {}
for arg in D1_D2_SETUP_ARGS: for arg in D1_D2_SETUP_ARGS:
@ -1011,8 +1030,11 @@ def cfg_to_args(path='setup.cfg'):
filenames = split_multiline(filenames) filenames = split_multiline(filenames)
in_cfg_value = [] in_cfg_value = []
for filename in filenames: for filename in filenames:
with open(filename) as fp: fp = codecs.open(filename, encoding='utf-8')
try:
in_cfg_value.append(fp.read()) in_cfg_value.append(fp.read())
finally:
fp.close()
in_cfg_value = '\n\n'.join(in_cfg_value) in_cfg_value = '\n\n'.join(in_cfg_value)
else: else:
continue continue
@ -1029,13 +1051,19 @@ def cfg_to_args(path='setup.cfg'):
return kwargs return kwargs
_SETUP_TMPL = """\ SETUP_TEMPLATE = """\
# This script was automatically generated by packaging # This script was automatically generated by packaging
import os import os
import codecs
from distutils.core import setup from distutils.core import setup
from ConfigParser import RawConfigParser try:
from ConfigParser import RawConfigParser
except ImportError:
from configparser import RawConfigParser
%(func)s %(split_multiline)s
%(cfg_to_args)s
setup(**cfg_to_args()) setup(**cfg_to_args())
""" """
@ -1049,8 +1077,10 @@ def generate_setup_py():
if os.path.exists("setup.py"): if os.path.exists("setup.py"):
raise PackagingFileError("a setup.py file already exists") raise PackagingFileError("a setup.py file already exists")
source = SETUP_TEMPLATE % {'split_multiline': getsource(split_multiline),
'cfg_to_args': getsource(cfg_to_args)}
with open("setup.py", "w", encoding='utf-8') as fp: with open("setup.py", "w", encoding='utf-8') as fp:
fp.write(_SETUP_TMPL % {'func': getsource(cfg_to_args)}) fp.write(source)
# Taken from the pip project # Taken from the pip project
@ -1307,6 +1337,8 @@ def get_install_method(path):
def copy_tree(src, dst, preserve_mode=True, preserve_times=True, def copy_tree(src, dst, preserve_mode=True, preserve_times=True,
preserve_symlinks=False, update=False, verbose=True, preserve_symlinks=False, update=False, verbose=True,
dry_run=False): dry_run=False):
# FIXME use of this function is why we get spurious logging message on
# stdout when tests run; kill and replace by shuil!
from distutils.file_util import copy_file from distutils.file_util import copy_file
if not dry_run and not os.path.isdir(src): if not dry_run and not os.path.isdir(src):

View File

@ -56,6 +56,7 @@ Nicolas Bareil
Chris Barker Chris Barker
Nick Barnes Nick Barnes
Quentin Barnes Quentin Barnes
David Barnett
Richard Barran Richard Barran
Cesar Eduardo Barros Cesar Eduardo Barros
Des Barry Des Barry