Allow multiple setup hooks in packaging’s setup.cfg files (#12240).
Original patch by Erik Bray.
diff --git a/Doc/packaging/setupcfg.rst b/Doc/packaging/setupcfg.rst
index 463522b..2b01ffb 100644
--- a/Doc/packaging/setupcfg.rst
+++ b/Doc/packaging/setupcfg.rst
@@ -176,15 +176,19 @@
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
diff --git a/Lib/packaging/config.py b/Lib/packaging/config.py
index 3427d9a..21bbcf8 100644
--- a/Lib/packaging/config.py
+++ b/Lib/packaging/config.py
@@ -61,17 +61,15 @@
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
@@ -131,17 +129,20 @@
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
diff --git a/Lib/packaging/tests/test_config.py b/Lib/packaging/tests/test_config.py
index 1669862..6be63eb 100644
--- a/Lib/packaging/tests/test_config.py
+++ b/Lib/packaging/tests/test_config.py
@@ -90,7 +90,7 @@
compilers =
packaging.tests.test_config.DCompiler
-setup_hook = %(setup-hook)s
+setup_hooks = %(setup-hooks)s
@@ -135,8 +135,16 @@
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 @@
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,13 +326,27 @@
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 README2'})
diff --git a/Lib/packaging/tests/test_util.py b/Lib/packaging/tests/test_util.py
index 68ad8eb..f657ab2 100644
--- a/Lib/packaging/tests/test_util.py
+++ b/Lib/packaging/tests/test_util.py
@@ -495,7 +495,7 @@
def test_cfg_to_args(self):
opts = {'description-file': 'README', 'extra-files': '',
- 'setup-hook': 'packaging.tests.test_config.hook'}
+ 'setup-hooks': 'packaging.tests.test_config.version_hook'}
self.write_file('setup.cfg', SETUP_CFG % opts)
self.write_file('README', 'loooong description')
diff --git a/Misc/NEWS b/Misc/NEWS
index 5b01308..47f3b0e 100644
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -187,6 +187,9 @@
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.