From f89ebdc358402588db893e18e4dd31bc8272b7bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Araujo?= Date: Fri, 21 Oct 2011 06:27:06 +0200 Subject: Fix missing imports in setup scripts generated by packaging (#13205). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Lib/packaging/tests/test_util.py | 34 ++++++++++++++++++++---- Lib/packaging/util.py | 56 +++++++++++++++++++++++++++++++--------- Misc/ACKS | 1 + 3 files changed, 74 insertions(+), 17 deletions(-) diff --git a/Lib/packaging/tests/test_util.py b/Lib/packaging/tests/test_util.py index 92aa72a..1cf5c93 100644 --- a/Lib/packaging/tests/test_util.py +++ b/Lib/packaging/tests/test_util.py @@ -5,11 +5,10 @@ import time import logging import tempfile import textwrap +import warnings import subprocess from io import StringIO -from packaging.tests import support, unittest -from packaging.tests.test_config import SETUP_CFG from packaging.errors import ( PackagingPlatformError, PackagingByteCompileError, PackagingFileError, PackagingExecError, InstallationException) @@ -20,7 +19,11 @@ from packaging.util import ( get_compiler_versions, _MAC_OS_X_LD_VERSION, byte_compile, find_packages, spawn, get_pypirc_path, generate_pypirc, read_pypirc, resolve_name, iglob, 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 = """\ @@ -513,7 +516,9 @@ class UtilTestCase(support.EnvironRestorer, self.write_file('setup.cfg', SETUP_CFG % opts, encoding='utf-8') 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 dist = Distribution() dist.parse_config_files() @@ -539,6 +544,26 @@ class UtilTestCase(support.EnvironRestorer, self.assertEqual(args['scripts'], dist.scripts) 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): fields = [('username', 'wok'), ('password', 'secret')] files = [('picture', 'wok.png', b'PNG89')] @@ -590,7 +615,6 @@ class GlobTestCase(GlobTestCaseBase): super(GlobTestCase, self).tearDown() def assertGlobMatch(self, glob, spec): - """""" tempdir = self.build_files_tree(spec) expected = self.clean_tree(spec) os.chdir(tempdir) diff --git a/Lib/packaging/util.py b/Lib/packaging/util.py index 2af1149..8dd715f 100644 --- a/Lib/packaging/util.py +++ b/Lib/packaging/util.py @@ -6,6 +6,7 @@ import csv import imp import sys import errno +import codecs import shutil import string import hashlib @@ -417,7 +418,8 @@ byte_compile(files, optimize=%r, force=%r, # cfile - byte-compiled file # dfile - purported source filename (same as 'file' by default) if optimize >= 0: - cfile = imp.cache_from_source(file, debug_override=not optimize) + cfile = imp.cache_from_source(file, + debug_override=not optimize) else: cfile = imp.cache_from_source(file) dfile = file @@ -931,6 +933,24 @@ def _iglob(path_glob): 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'): """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, 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 D1_D2_SETUP_ARGS = {"name": ("metadata",), @@ -986,10 +1004,11 @@ def cfg_to_args(path='setup.cfg'): # The real code starts here config = RawConfigParser() - if not os.path.exists(path): - raise PackagingFileError("file '%s' does not exist" % - os.path.abspath(path)) - config.read(path, encoding='utf-8') + f = codecs.open(path, encoding='utf-8') + try: + config.readfp(f) + finally: + f.close() kwargs = {} for arg in D1_D2_SETUP_ARGS: @@ -1011,8 +1030,11 @@ def cfg_to_args(path='setup.cfg'): filenames = split_multiline(filenames) in_cfg_value = [] for filename in filenames: - with open(filename) as fp: + fp = codecs.open(filename, encoding='utf-8') + try: in_cfg_value.append(fp.read()) + finally: + fp.close() in_cfg_value = '\n\n'.join(in_cfg_value) else: continue @@ -1029,13 +1051,19 @@ def cfg_to_args(path='setup.cfg'): return kwargs -_SETUP_TMPL = """\ +SETUP_TEMPLATE = """\ # This script was automatically generated by packaging import os +import codecs from distutils.core import setup -from ConfigParser import RawConfigParser +try: + from ConfigParser import RawConfigParser +except ImportError: + from configparser import RawConfigParser + +%(split_multiline)s -%(func)s +%(cfg_to_args)s setup(**cfg_to_args()) """ @@ -1049,8 +1077,10 @@ def generate_setup_py(): if os.path.exists("setup.py"): 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: - fp.write(_SETUP_TMPL % {'func': getsource(cfg_to_args)}) + fp.write(source) # 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, preserve_symlinks=False, update=False, verbose=True, 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 if not dry_run and not os.path.isdir(src): diff --git a/Misc/ACKS b/Misc/ACKS index 6d3a6d9..08531b9 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -56,6 +56,7 @@ Nicolas Bareil Chris Barker Nick Barnes Quentin Barnes +David Barnett Richard Barran Cesar Eduardo Barros Des Barry -- cgit v0.12 From 4d4b19e294e3748b0e68fec4278d5bde0b0011c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Araujo?= Date: Fri, 21 Oct 2011 07:34:00 +0200 Subject: =?UTF-8?q?Document=20that=20packaging=20doesn=E2=80=99t=20create?= =?UTF-8?q?=20=5F=5Finit=5F=5F.py=20files=20(#3902).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bug reported expected distutils to create an __init__.py file for a project using only C extension modules. IMO, how Python imports packages and submodules is well documented, and it’s never suggested that distutils might create an __init__.py file, so I’m adding this clarification to the packaging docs but won’t backport unless other people tell me they shared the same wrong expectation. Thanks to Mike Hoy for his help with the patch. --- Doc/library/packaging.compiler.rst | 4 ++++ Doc/packaging/setupscript.rst | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/Doc/library/packaging.compiler.rst b/Doc/library/packaging.compiler.rst index d771972..89b6e45 100644 --- a/Doc/library/packaging.compiler.rst +++ b/Doc/library/packaging.compiler.rst @@ -675,3 +675,7 @@ extension modules. | | abort the build process, but | | | | simply skip the extension. | | +------------------------+--------------------------------+---------------------------+ + +To distribute extension modules that live in a package (e.g. ``package.ext``), +you need to create you need to create a :file:`{package}/__init__.py` file to +let Python recognize and import your module. diff --git a/Doc/packaging/setupscript.rst b/Doc/packaging/setupscript.rst index dbac3dd..cafde20 100644 --- a/Doc/packaging/setupscript.rst +++ b/Doc/packaging/setupscript.rst @@ -177,6 +177,10 @@ resulting object code are identical in both cases; the only difference is where in the filesystem (and therefore where in Python's namespace hierarchy) the resulting extension lives. +If your distribution contains only one or more extension modules in a package, +you need to create a :file:`{package}/__init__.py` file anyway, otherwise Python +won't be able to import anything. + If you have a number of extensions all in the same package (or all under the same base package), use the :option:`ext_package` keyword argument to :func:`setup`. For example, :: -- cgit v0.12 From 89d3a69d831971d91eab7f799d8c406b00e3f575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20Araujo?= Date: Fri, 21 Oct 2011 07:56:32 +0200 Subject: Add tests for packaging.tests.support (#12659). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks to Francisco Martín Brugué for the patch. --- Lib/packaging/tests/test_support.py | 78 +++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 Lib/packaging/tests/test_support.py diff --git a/Lib/packaging/tests/test_support.py b/Lib/packaging/tests/test_support.py new file mode 100644 index 0000000..0ae9e1b --- /dev/null +++ b/Lib/packaging/tests/test_support.py @@ -0,0 +1,78 @@ +import os +import tempfile + +from packaging.dist import Distribution +from packaging.tests import support, unittest + + +class TestingSupportTestCase(unittest.TestCase): + + def test_fake_dec(self): + @support.fake_dec(1, 2, k=3) + def func(arg0, *args, **kargs): + return arg0, args, kargs + self.assertEqual(func(-1, -2, k=-3), (-1, (-2,), {'k': -3})) + + def test_TempdirManager(self): + files = {} + + class Tester(support.TempdirManager, unittest.TestCase): + + def test_mktempfile(self2): + tmpfile = self2.mktempfile() + files['test_mktempfile'] = tmpfile.name + self.assertTrue(os.path.isfile(tmpfile.name)) + + def test_mkdtemp(self2): + tmpdir = self2.mkdtemp() + files['test_mkdtemp'] = tmpdir + self.assertTrue(os.path.isdir(tmpdir)) + + def test_write_file(self2): + tmpdir = self2.mkdtemp() + files['test_write_file'] = tmpdir + self2.write_file((tmpdir, 'file1'), 'me file 1') + file1 = os.path.join(tmpdir, 'file1') + self.assertTrue(os.path.isfile(file1)) + text = '' + with open(file1, 'r') as f: + text = f.read() + self.assertEqual(text, 'me file 1') + + def test_create_dist(self2): + project_dir, dist = self2.create_dist() + files['test_create_dist'] = project_dir + self.assertTrue(os.path.isdir(project_dir)) + self.assertIsInstance(dist, Distribution) + + def test_assertIsFile(self2): + fd, fn = tempfile.mkstemp() + os.close(fd) + self.addCleanup(support.unlink, fn) + self2.assertIsFile(fn) + self.assertRaises(AssertionError, self2.assertIsFile, 'foO') + + def test_assertIsNotFile(self2): + tmpdir = self2.mkdtemp() + self2.assertIsNotFile(tmpdir) + + tester = Tester() + for name in ('test_mktempfile', 'test_mkdtemp', 'test_write_file', + 'test_create_dist', 'test_assertIsFile', + 'test_assertIsNotFile'): + tester.setUp() + try: + getattr(tester, name)() + finally: + tester.tearDown() + + # check clean-up + if name in files: + self.assertFalse(os.path.exists(files[name])) + + +def test_suite(): + return unittest.makeSuite(TestingSupportTestCase) + +if __name__ == "__main__": + unittest.main(defaultTest="test_suite") -- cgit v0.12