diff options
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 |
commit | bc18532eee4098bd57805d021b53fba618428d15 (patch) | |
tree | ca6b5563bada0b00521f48aa2f0ebd20a2ad7491 | |
parent | a52930834823502254839af17cc5d972d7f82856 (diff) | |
parent | 6280606a5781147158a607b409ec7ebf4732bd92 (diff) | |
download | cpython-git-bc18532eee4098bd57805d021b53fba618428d15.tar.gz |
Branch merge
-rw-r--r-- | Doc/library/collections.rst | 2 | ||||
-rw-r--r-- | Doc/packaging/setupcfg.rst | 15 | ||||
-rw-r--r-- | Lib/packaging/config.py | 69 | ||||
-rw-r--r-- | Lib/packaging/tests/test_config.py | 38 | ||||
-rw-r--r-- | Lib/packaging/tests/test_util.py | 48 | ||||
-rw-r--r-- | Lib/packaging/util.py | 53 | ||||
-rw-r--r-- | Misc/ACKS | 1 | ||||
-rw-r--r-- | Misc/NEWS | 7 |
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 @@ -116,6 +116,7 @@ Monty Brandenberg Georg Brandl Christopher Brannon Terrence Brannon +Erik Bray Brian Brazil Dave Brennan Tom Bridgman @@ -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. |