diff options
| -rw-r--r-- | changelog.d/2712.change.rst | 1 | ||||
| -rw-r--r-- | docs/userguide/declarative_config.rst | 1 | ||||
| -rw-r--r-- | setuptools/config.py | 31 | ||||
| -rw-r--r-- | setuptools/tests/test_config.py | 35 |
4 files changed, 67 insertions, 1 deletions
diff --git a/changelog.d/2712.change.rst b/changelog.d/2712.change.rst new file mode 100644 index 00000000..952101f1 --- /dev/null +++ b/changelog.d/2712.change.rst @@ -0,0 +1 @@ +Added implicit globbing support for `[options.data_files]` values. diff --git a/docs/userguide/declarative_config.rst b/docs/userguide/declarative_config.rst index 128d9f97..3b72611c 100644 --- a/docs/userguide/declarative_config.rst +++ b/docs/userguide/declarative_config.rst @@ -68,6 +68,7 @@ boilerplate code in some cases. site.d/00_default.conf host.d/00_default.conf data = data/img/logo.png, data/svg/icon.svg + fonts = data/fonts/*.ttf, data/fonts/*.otf Metadata and options are set in the config sections of the same name. diff --git a/setuptools/config.py b/setuptools/config.py index 44de7cf5..7ab6bfd7 100644 --- a/setuptools/config.py +++ b/setuptools/config.py @@ -9,6 +9,7 @@ import importlib from collections import defaultdict from functools import partial from functools import wraps +from glob import iglob import contextlib from distutils.errors import DistutilsOptionError, DistutilsFileError @@ -256,6 +257,34 @@ class ConfigHandler: return [chunk.strip() for chunk in value if chunk.strip()] @classmethod + def _parse_list_glob(cls, value, separator=','): + """Equivalent to _parse_list() but expands any glob patterns using glob(). + + However, unlike with glob() calls, the results remain relative paths. + + :param value: + :param separator: List items separator character. + :rtype: list + """ + glob_characters = ('*', '?', '[', ']', '{', '}') + values = cls._parse_list(value, separator=separator) + expanded_values = [] + for value in values: + + # Has globby characters? + if any(char in value for char in glob_characters): + # then expand the glob pattern while keeping paths *relative*: + expanded_values.extend(sorted( + os.path.relpath(path, os.getcwd()) + for path in iglob(os.path.abspath(value)))) + + else: + # take the value as-is: + expanded_values.append(value) + + return expanded_values + + @classmethod def _parse_dict(cls, value): """Represents value as a dict. @@ -711,5 +740,5 @@ class ConfigOptionsHandler(ConfigHandler): :param dict section_options: """ - parsed = self._parse_section_to_dict(section_options, self._parse_list) + parsed = self._parse_section_to_dict(section_options, self._parse_list_glob) self['data_files'] = [(k, v) for k, v in parsed.items()] diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index 21f1becd..ec65250e 100644 --- a/setuptools/tests/test_config.py +++ b/setuptools/tests/test_config.py @@ -893,6 +893,41 @@ class TestOptions: ] assert sorted(dist.data_files) == sorted(expected) + def test_data_files_globby(self, tmpdir): + fake_env( + tmpdir, + '[options.data_files]\n' + 'cfg =\n' + ' a/b.conf\n' + ' c/d.conf\n' + 'data = *.dat\n' + 'icons = \n' + ' *.ico\n' + 'audio = \n' + ' *.wav\n' + ' sounds.db\n' + ) + + # Create dummy files for glob()'s sake: + tmpdir.join('a.dat').write('') + tmpdir.join('b.dat').write('') + tmpdir.join('c.dat').write('') + tmpdir.join('a.ico').write('') + tmpdir.join('b.ico').write('') + tmpdir.join('c.ico').write('') + tmpdir.join('beep.wav').write('') + tmpdir.join('boop.wav').write('') + tmpdir.join('sounds.db').write('') + + with get_dist(tmpdir) as dist: + expected = [ + ('cfg', ['a/b.conf', 'c/d.conf']), + ('data', ['a.dat', 'b.dat', 'c.dat']), + ('icons', ['a.ico', 'b.ico', 'c.ico']), + ('audio', ['beep.wav', 'boop.wav', 'sounds.db']), + ] + assert sorted(dist.data_files) == sorted(expected) + def test_python_requires_simple(self, tmpdir): fake_env( tmpdir, |
