summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorÉric Araujo <merwok@netwok.org>2011-06-11 19:56:09 +0200
committerÉric Araujo <merwok@netwok.org>2011-06-11 19:56:09 +0200
commitbc18532eee4098bd57805d021b53fba618428d15 (patch)
treeca6b5563bada0b00521f48aa2f0ebd20a2ad7491
parenta52930834823502254839af17cc5d972d7f82856 (diff)
parent6280606a5781147158a607b409ec7ebf4732bd92 (diff)
downloadcpython-git-bc18532eee4098bd57805d021b53fba618428d15.tar.gz
Branch merge
-rw-r--r--Doc/library/collections.rst2
-rw-r--r--Doc/packaging/setupcfg.rst15
-rw-r--r--Lib/packaging/config.py69
-rw-r--r--Lib/packaging/tests/test_config.py38
-rw-r--r--Lib/packaging/tests/test_util.py48
-rw-r--r--Lib/packaging/util.py53
-rw-r--r--Misc/ACKS1
-rw-r--r--Misc/NEWS7
8 files changed, 156 insertions, 77 deletions
diff --git a/Doc/library/collections.rst b/Doc/library/collections.rst
index 6c9b1e58f9..39a03dd988 100644
--- a/Doc/library/collections.rst
+++ b/Doc/library/collections.rst
@@ -83,7 +83,7 @@ The class can be used to simulate nested scopes and is useful in templating.
creating subcontexts that can be updated without altering values in any
of the parent mappings.
- .. attribute:: parents()
+ .. method:: parents()
Returns a new :class:`ChainMap` containing all of the maps in the current
instance except the first one. This is useful for skipping the first map
diff --git a/Doc/packaging/setupcfg.rst b/Doc/packaging/setupcfg.rst
index aa8216fdf1..2b01ffb021 100644
--- a/Doc/packaging/setupcfg.rst
+++ b/Doc/packaging/setupcfg.rst
@@ -176,15 +176,19 @@ compilers
compilers =
hotcompiler.SmartCCompiler
-setup_hook
- defines a callable that will be called right after the
- :file:`setup.cfg` file is read. The callable receives the configuration
- in form of a mapping and can make some changes to it. *optional*
+setup_hooks
+ Defines a list of callables to be called right after the :file:`setup.cfg`
+ file is read, before any other processing. The callables are executed in the
+ order they're found in the file; if one of them cannot be found, tools should
+ not stop, but for example produce a warning and continue with the next line.
+ Each callable receives the configuration as a dictionary (keys are
+ :file:`setup.cfg` sections, values are dictionaries of fields) and can make
+ any changes to it. *optional*, *multi*
Example::
[global]
- setup_hook = package.setup.customize_dist
+ setup_hooks = package.setup.customize_dist
Metadata
@@ -285,6 +289,7 @@ One extra field not present in PEP 345 is supported:
description-file
Path to a text file that will be used to fill the ``description`` field.
+ Multiple values are accepted; they must be separated by whitespace.
``description-file`` and ``description`` are mutually exclusive. *optional*
diff --git a/Lib/packaging/config.py b/Lib/packaging/config.py
index 6df2babb27..21bbcf8567 100644
--- a/Lib/packaging/config.py
+++ b/Lib/packaging/config.py
@@ -9,7 +9,8 @@ from configparser import RawConfigParser
from packaging import logger
from packaging.errors import PackagingOptionError
from packaging.compiler.extension import Extension
-from packaging.util import check_environ, iglob, resolve_name, strtobool
+from packaging.util import (check_environ, iglob, resolve_name, strtobool,
+ split_multiline)
from packaging.compiler import set_compiler
from packaging.command import set_command
from packaging.markers import interpret
@@ -60,17 +61,15 @@ def get_resources_dests(resources_root, rules):
class Config:
- """Reads configuration files and work with the Distribution instance
- """
+ """Class used to work with configuration files"""
def __init__(self, dist):
self.dist = dist
- self.setup_hook = None
+ self.setup_hooks = []
- def run_hook(self, config):
- if self.setup_hook is None:
- return
- # the hook gets only the config
- self.setup_hook(config)
+ def run_hooks(self, config):
+ """Run setup hooks in the order defined in the spec."""
+ for hook in self.setup_hooks:
+ hook(config)
def find_config_files(self):
"""Find as many configuration files as should be processed for this
@@ -124,29 +123,26 @@ class Config:
# XXX
return value
- def _multiline(self, value):
- value = [v for v in
- [v.strip() for v in value.split('\n')]
- if v != '']
- return value
-
def _read_setup_cfg(self, parser, cfg_filename):
cfg_directory = os.path.dirname(os.path.abspath(cfg_filename))
content = {}
for section in parser.sections():
content[section] = dict(parser.items(section))
- # global:setup_hook is called *first*
+ # global setup hooks are called first
if 'global' in content:
- if 'setup_hook' in content['global']:
- setup_hook = content['global']['setup_hook']
- try:
- self.setup_hook = resolve_name(setup_hook)
- except ImportError as e:
- logger.warning('could not import setup_hook: %s',
- e.args[0])
- else:
- self.run_hook(content)
+ if 'setup_hooks' in content['global']:
+ setup_hooks = split_multiline(content['global']['setup_hooks'])
+
+ for line in setup_hooks:
+ try:
+ hook = resolve_name(line)
+ except ImportError as e:
+ logger.warning('cannot find setup hook: %s', e.args[0])
+ else:
+ self.setup_hooks.append(hook)
+
+ self.run_hooks(content)
metadata = self.dist.metadata
@@ -155,7 +151,7 @@ class Config:
for key, value in content['metadata'].items():
key = key.replace('_', '-')
if metadata.is_multi_field(key):
- value = self._multiline(value)
+ value = split_multiline(value)
if key == 'project-url':
value = [(label.strip(), url.strip())
@@ -168,21 +164,18 @@ class Config:
"mutually exclusive")
raise PackagingOptionError(msg)
- if isinstance(value, list):
- filenames = value
- else:
- filenames = value.split()
+ filenames = value.split()
- # concatenate each files
- value = ''
+ # concatenate all files
+ value = []
for filename in filenames:
# will raise if file not found
with open(filename) as description_file:
- value += description_file.read().strip() + '\n'
+ value.append(description_file.read().strip())
# add filename as a required file
if filename not in metadata.requires_files:
metadata.requires_files.append(filename)
- value = value.strip()
+ value = '\n'.join(value).strip()
key = 'description'
if metadata.is_metadata_field(key):
@@ -192,7 +185,7 @@ class Config:
files = content['files']
self.dist.package_dir = files.pop('packages_root', None)
- files = dict((key, self._multiline(value)) for key, value in
+ files = dict((key, split_multiline(value)) for key, value in
files.items())
self.dist.packages = []
@@ -310,7 +303,7 @@ class Config:
opt = opt.replace('-', '_')
if opt == 'sub_commands':
- val = self._multiline(val)
+ val = split_multiline(val)
if isinstance(val, str):
val = [val]
@@ -348,14 +341,14 @@ class Config:
raise PackagingOptionError(msg)
def _load_compilers(self, compilers):
- compilers = self._multiline(compilers)
+ compilers = split_multiline(compilers)
if isinstance(compilers, str):
compilers = [compilers]
for compiler in compilers:
set_compiler(compiler.strip())
def _load_commands(self, commands):
- commands = self._multiline(commands)
+ commands = split_multiline(commands)
if isinstance(commands, str):
commands = [commands]
for command in commands:
diff --git a/Lib/packaging/tests/test_config.py b/Lib/packaging/tests/test_config.py
index 9198ead131..6be63ebbeb 100644
--- a/Lib/packaging/tests/test_config.py
+++ b/Lib/packaging/tests/test_config.py
@@ -90,7 +90,7 @@ commands =
compilers =
packaging.tests.test_config.DCompiler
-setup_hook = %(setup-hook)s
+setup_hooks = %(setup-hooks)s
@@ -135,8 +135,16 @@ class DCompiler:
pass
-def hook(content):
- content['metadata']['version'] += '.dev1'
+def version_hook(config):
+ config['metadata']['version'] += '.dev1'
+
+
+def first_hook(config):
+ config['files']['modules'] += '\n first'
+
+
+def third_hook(config):
+ config['files']['modules'] += '\n third'
class FooBarBazTest:
@@ -186,7 +194,7 @@ class ConfigTestCase(support.TempdirManager,
def write_setup(self, kwargs=None):
opts = {'description-file': 'README', 'extra-files': '',
- 'setup-hook': 'packaging.tests.test_config.hook'}
+ 'setup-hooks': 'packaging.tests.test_config.version_hook'}
if kwargs:
opts.update(kwargs)
self.write_file('setup.cfg', SETUP_CFG % opts, encoding='utf-8')
@@ -318,16 +326,30 @@ class ConfigTestCase(support.TempdirManager,
self.assertEqual(ext.extra_compile_args, cargs)
self.assertEqual(ext.language, 'cxx')
- def test_missing_setuphook_warns(self):
- self.write_setup({'setup-hook': 'this.does._not.exist'})
+ def test_missing_setup_hook_warns(self):
+ self.write_setup({'setup-hooks': 'this.does._not.exist'})
self.write_file('README', 'yeah')
dist = self.get_dist()
logs = self.get_logs(logging.WARNING)
self.assertEqual(1, len(logs))
- self.assertIn('could not import setup_hook', logs[0])
+ self.assertIn('cannot find setup hook', logs[0])
+
+ def test_multiple_setup_hooks(self):
+ self.write_setup({
+ 'setup-hooks': '\n packaging.tests.test_config.first_hook'
+ '\n packaging.tests.test_config.missing_hook'
+ '\n packaging.tests.test_config.third_hook'
+ })
+ self.write_file('README', 'yeah')
+ dist = self.get_dist()
+
+ self.assertEqual(['haven', 'first', 'third'], dist.py_modules)
+ logs = self.get_logs(logging.WARNING)
+ self.assertEqual(1, len(logs))
+ self.assertIn('cannot find setup hook', logs[0])
def test_metadata_requires_description_files_missing(self):
- self.write_setup({'description-file': 'README\n README2'})
+ self.write_setup({'description-file': 'README README2'})
self.write_file('README', 'yeah')
self.write_file('README2', 'yeah')
os.mkdir('src')
diff --git a/Lib/packaging/tests/test_util.py b/Lib/packaging/tests/test_util.py
index 5a94a7386e..f657ab25fa 100644
--- a/Lib/packaging/tests/test_util.py
+++ b/Lib/packaging/tests/test_util.py
@@ -8,16 +8,18 @@ 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)
from packaging import util
+from packaging.dist import Distribution
from packaging.util import (
convert_path, change_root, split_quoted, strtobool, rfc822_escape,
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)
+ get_install_method, cfg_to_args)
PYPIRC = """\
@@ -88,13 +90,15 @@ class UtilTestCase(support.EnvironRestorer,
support.LoggingCatcher,
unittest.TestCase):
- restore_environ = ['HOME']
+ restore_environ = ['HOME', 'PLAT']
def setUp(self):
super(UtilTestCase, self).setUp()
- self.tmp_dir = self.mkdtemp()
- self.rc = os.path.join(self.tmp_dir, '.pypirc')
- os.environ['HOME'] = self.tmp_dir
+ self.addCleanup(os.chdir, os.getcwd())
+ tempdir = self.mkdtemp()
+ self.rc = os.path.join(tempdir, '.pypirc')
+ os.environ['HOME'] = tempdir
+ os.chdir(tempdir)
# saving the environment
self.name = os.name
self.platform = sys.platform
@@ -103,7 +107,6 @@ class UtilTestCase(support.EnvironRestorer,
self.join = os.path.join
self.isabs = os.path.isabs
self.splitdrive = os.path.splitdrive
- #self._config_vars = copy(sysconfig._config_vars)
# patching os.uname
if hasattr(os, 'uname'):
@@ -137,7 +140,6 @@ class UtilTestCase(support.EnvironRestorer,
os.uname = self.uname
else:
del os.uname
- #sysconfig._config_vars = copy(self._config_vars)
util.find_executable = self.old_find_executable
subprocess.Popen = self.old_popen
sys.old_stdout = self.old_stdout
@@ -491,6 +493,38 @@ class UtilTestCase(support.EnvironRestorer,
content = f.read()
self.assertEqual(content, WANTED)
+ def test_cfg_to_args(self):
+ opts = {'description-file': 'README', 'extra-files': '',
+ 'setup-hooks': 'packaging.tests.test_config.version_hook'}
+ self.write_file('setup.cfg', SETUP_CFG % opts)
+ self.write_file('README', 'loooong description')
+
+ args = cfg_to_args()
+ # use Distribution to get the contents of the setup.cfg file
+ dist = Distribution()
+ dist.parse_config_files()
+ metadata = dist.metadata
+
+ self.assertEqual(args['name'], metadata['Name'])
+ # + .dev1 because the test SETUP_CFG also tests a hook function in
+ # test_config.py for appending to the version string
+ self.assertEqual(args['version'] + '.dev1', metadata['Version'])
+ self.assertEqual(args['author'], metadata['Author'])
+ self.assertEqual(args['author_email'], metadata['Author-Email'])
+ self.assertEqual(args['maintainer'], metadata['Maintainer'])
+ self.assertEqual(args['maintainer_email'],
+ metadata['Maintainer-Email'])
+ self.assertEqual(args['description'], metadata['Summary'])
+ self.assertEqual(args['long_description'], metadata['Description'])
+ self.assertEqual(args['classifiers'], metadata['Classifier'])
+ self.assertEqual(args['requires'], metadata['Requires-Dist'])
+ self.assertEqual(args['provides'], metadata['Provides-Dist'])
+
+ self.assertEqual(args['package_dir'].get(''), dist.package_dir)
+ self.assertEqual(args['packages'], dist.packages)
+ self.assertEqual(args['scripts'], dist.scripts)
+ self.assertEqual(args['py_modules'], dist.py_modules)
+
class GlobTestCaseBase(support.TempdirManager,
support.LoggingCatcher,
diff --git a/Lib/packaging/util.py b/Lib/packaging/util.py
index 812dbe3c29..29994c0881 100644
--- a/Lib/packaging/util.py
+++ b/Lib/packaging/util.py
@@ -250,6 +250,14 @@ def split_quoted(s):
return words
+def split_multiline(value):
+ """Split a multiline string into a list, excluding blank lines."""
+
+ return [element for element in
+ (line.strip() for line in value.split('\n'))
+ if element]
+
+
def execute(func, args, msg=None, verbose=0, dry_run=False):
"""Perform some action that affects the outside world.
@@ -542,18 +550,15 @@ def write_file(filename, contents):
def _is_package(path):
- if not os.path.isdir(path):
- return False
- return os.path.isfile(os.path.join(path, '__init__.py'))
+ return os.path.isdir(path) and os.path.isfile(
+ os.path.join(path, '__init__.py'))
# Code taken from the pip project
def _is_archive_file(name):
archives = ('.zip', '.tar.gz', '.tar.bz2', '.tgz', '.tar')
ext = splitext(name)[1].lower()
- if ext in archives:
- return True
- return False
+ return ext in archives
def _under(path, root):
@@ -772,12 +777,13 @@ def spawn(cmd, search_path=True, verbose=0, dry_run=False, env=None):
Raise PackagingExecError if running the program fails in any way; just
return on success.
"""
- logger.info(' '.join(cmd))
+ logger.debug('spawn: running %r', cmd)
if dry_run:
+ logging.debug('dry run, no process actually spawned')
return
exit_status = subprocess.call(cmd, env=env)
if exit_status != 0:
- msg = "command '%s' failed with exit status %d"
+ msg = "command %r failed with exit status %d"
raise PackagingExecError(msg % (cmd, exit_status))
@@ -1010,16 +1016,20 @@ def cfg_to_args(path='setup.cfg'):
"requires": ("metadata", "requires_dist"),
"provides": ("metadata", "provides_dist"), # **
"obsoletes": ("metadata", "obsoletes_dist"), # **
+ "package_dir": ("files", 'packages_root'),
"packages": ("files",),
"scripts": ("files",),
"py_modules": ("files", "modules"), # **
}
MULTI_FIELDS = ("classifiers",
- "requires",
"platforms",
+ "requires",
+ "provides",
+ "obsoletes",
"packages",
- "scripts")
+ "scripts",
+ "py_modules")
def has_get_option(config, section, option):
if config.has_option(section, option):
@@ -1031,9 +1041,9 @@ def cfg_to_args(path='setup.cfg'):
# The real code starts here
config = RawConfigParser()
- if not os.path.exists(file):
+ if not os.path.exists(path):
raise PackagingFileError("file '%s' does not exist" %
- os.path.abspath(file))
+ os.path.abspath(path))
config.read(path)
kwargs = {}
@@ -1050,17 +1060,24 @@ def cfg_to_args(path='setup.cfg'):
in_cfg_value = has_get_option(config, section, option)
if not in_cfg_value:
# There is no such option in the setup.cfg
- if arg == "long_description":
- filename = has_get_option(config, section, "description_file")
- if filename:
- with open(filename) as fp:
- in_cfg_value = fp.read()
+ if arg == 'long_description':
+ filenames = has_get_option(config, section, 'description-file')
+ if filenames:
+ filenames = split_multiline(filenames)
+ in_cfg_value = []
+ for filename in filenames:
+ with open(filename) as fp:
+ in_cfg_value.append(fp.read())
+ in_cfg_value = '\n\n'.join(in_cfg_value)
else:
continue
+ if arg == 'package_dir' and in_cfg_value:
+ in_cfg_value = {'': in_cfg_value}
+
if arg in MULTI_FIELDS:
# support multiline options
- in_cfg_value = in_cfg_value.strip().split('\n')
+ in_cfg_value = split_multiline(in_cfg_value)
kwargs[arg] = in_cfg_value
diff --git a/Misc/ACKS b/Misc/ACKS
index 8b3055c605..163d9b86d7 100644
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -116,6 +116,7 @@ Monty Brandenberg
Georg Brandl
Christopher Brannon
Terrence Brannon
+Erik Bray
Brian Brazil
Dave Brennan
Tom Bridgman
diff --git a/Misc/NEWS b/Misc/NEWS
index 7c48199091..5b3a372dd5 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -187,6 +187,13 @@ Core and Builtins
Library
-------
+- Issue #12240: Allow multiple setup hooks in packaging's setup.cfg files.
+ Original patch by Erik Bray.
+
+- Issue #11595: Fix assorted bugs in packaging.util.cfg_to_args, a
+ compatibility helper for the distutils-packaging transition. Original patch
+ by Erik Bray.
+
- Issue #12287: In ossaudiodev, check that the device isn't closed in several
methods.