Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement #1681 (globbing support for [options.data_files]) #2712

Merged
merged 8 commits into from
Aug 26, 2021
1 change: 1 addition & 0 deletions changelog.d/2712.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Added implicit globbing support for `[options.data_files]` values.
1 change: 1 addition & 0 deletions docs/userguide/declarative_config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
40 changes: 39 additions & 1 deletion setuptools/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from collections import defaultdict
from functools import partial
from functools import wraps
from glob import iglob
import contextlib

from distutils.errors import DistutilsOptionError, DistutilsFileError
Expand Down Expand Up @@ -255,6 +256,43 @@ def _parse_list(cls, value, separator=','):

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 direct calls to glob(), the resolved results remain relative paths.

:param value:
:param separator: List items separator character.
:rtype: list
"""
glob_characters = ('*', '?', '[', ']', '{', '}')
get_relpath = lambda value: os.path.relpath(value, os.getcwd())
values = cls._parse_list(value, separator=separator)
expanded_values = []
for value in values:
trimmed_value = value.strip()

# Has globby characters?
if any(char in value for char in glob_characters):
value = os.path.abspath(value)

# check if this path has globby characters but does in fact exist:
darkvertex marked this conversation as resolved.
Show resolved Hide resolved
if os.path.exists(value):
# if it does, treat it literally and do not expand any patterns:
expanded_values.append(get_relpath(value))
continue

# else expand the glob pattern while keeping paths *relative*:
value = sorted(
get_relpath(path) for path in iglob(value))
expanded_values.extend(value)

else:
expanded_values.append(value)

return expanded_values

@classmethod
def _parse_dict(cls, value):
"""Represents value as a dict.
Expand Down Expand Up @@ -711,5 +749,5 @@ def parse_section_data_files(self, section_options):

: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()]
38 changes: 38 additions & 0 deletions setuptools/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,44 @@ def test_data_files(self, tmpdir):
]
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'
'strangeness = literal_weird_filename?.txt\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('')
tmpdir.join('literal_weird_filename?.txt').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']),
('strangeness', ['literal_weird_filename?.txt']),
]
assert sorted(dist.data_files) == sorted(expected)

def test_python_requires_simple(self, tmpdir):
fake_env(
tmpdir,
Expand Down