Skip to content

Commit 8c55620

Browse files
fvjautarch
authored andcommitted
Support globs and directories in extra_files
All entries in `extra_files` are now interpreted as zglob-style globs. Additionally, directories can be specified and will be expanded to a recursive glob under the directory. File path handling is the same as it has always been. Hidden files are not included in globs and must be spelled out explicitly.
1 parent 4c135e1 commit 8c55620

File tree

3 files changed

+241
-17
lines changed

3 files changed

+241
-17
lines changed

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,12 @@ Either this input or the `target` input must be provided.
101101
- **Required**: no
102102

103103
This is a list of additional files or globs to include in the archive files for a release. This
104-
should be provided as a newline-separate list.
104+
should be provided as a newline-separate list. This list can include directories as well as
105+
zglob-style recursive globs (`**`). If you specify a directory, this will be treated as a if you
106+
wrote `directory/**`, meaning that the directory and all of its children will be included.
107+
108+
Hidden files are _not_ included when you use a glob. To include a hidden path, you must specify the
109+
full path to the hidden files directly.
105110

106111
Defaults to the file specified by the `changes-file` input and any file matching `README*` in the
107112
project root.

make-archive.py

Lines changed: 231 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -108,16 +108,30 @@ def find_executable(executable_name: str, target: Optional[str]) -> List[str]:
108108

109109

110110
def gather_additional_files(
111-
extra_files: Optional[str], changes_file: Optional[str]
111+
extra_files_globs: Optional[str], changes_file: Optional[str]
112112
) -> List[str]:
113113
"""Gather additional files to include in the archive."""
114-
if extra_files:
115-
return list(filter(None, map(str.strip, extra_files.splitlines())))
114+
if not extra_files_globs:
115+
return glob.glob("README*") + [changes_file] if changes_file else []
116+
117+
from os.path import isdir
118+
119+
globs = []
120+
for g in extra_files_globs.splitlines():
121+
stripped = g.strip()
122+
if not stripped:
123+
continue
124+
125+
if isdir(g):
126+
globs.append(g.removesuffix("/") + "/**")
127+
else:
128+
globs.append(g)
129+
130+
# Collect all matching files
131+
files = [changes_file] if changes_file else []
132+
for g in globs:
133+
files.extend(glob.glob(g, recursive=True))
116134

117-
files = []
118-
if changes_file:
119-
files.append(changes_file)
120-
files.extend(glob.glob("README*"))
121135
return files
122136

123137

@@ -204,6 +218,216 @@ def target_to_archive_name(target: str) -> str:
204218
return f"{os_name}-{cpu}"
205219

206220

221+
class TestExtraFilesGlobs(unittest.TestCase):
222+
"""Test cases for handling extra files."""
223+
224+
from unittest.mock import patch
225+
from collections import namedtuple
226+
227+
ExtraFilesTestDefinition = namedtuple(
228+
"ExtraFilesTestDefinition", ["name", "arguments", "expected"]
229+
)
230+
231+
@patch("os.path.isdir")
232+
@patch("glob.glob")
233+
def test_extra_files_empty(self, mocked_glob, mocked_isdir):
234+
mocked_glob.side_effect = lambda g, *kargs, **kwargs: []
235+
mocked_isdir.return_value = False
236+
237+
test_definitions = [
238+
self.ExtraFilesTestDefinition(
239+
name="empty arguments", arguments=("\n".join([]), None), expected=[]
240+
),
241+
self.ExtraFilesTestDefinition(
242+
name="keeps changes_file",
243+
arguments=("\n".join([]), "CHANGES.md"),
244+
expected=["CHANGES.md"],
245+
),
246+
]
247+
248+
for test_input in test_definitions:
249+
with self.subTest(test_input.name):
250+
self.assertCountEqual(
251+
gather_additional_files(*test_input.arguments), test_input.expected
252+
)
253+
254+
@patch("os.path.isdir")
255+
@patch("glob.glob")
256+
def test_extra_files_literals(self, mocked_glob, mocked_isdir):
257+
mocked_glob.side_effect = lambda g, *args, **kwargs: {
258+
"README.md": ["README.md"],
259+
"CHANGES.md": ["CHANGES.md"],
260+
"dir/file.txt": ["dir/file.txt"],
261+
}.get(g, [])
262+
mocked_isdir.return_value = False
263+
264+
test_definitions = [
265+
self.ExtraFilesTestDefinition(
266+
name="keeps singular literal file",
267+
arguments=("\n".join(["README.md"]), None),
268+
expected=["README.md"],
269+
),
270+
self.ExtraFilesTestDefinition(
271+
name="keeps singular literal nested file",
272+
arguments=("\n".join(["dir/file.txt"]), None),
273+
expected=["dir/file.txt"],
274+
),
275+
self.ExtraFilesTestDefinition(
276+
name="combines singular literal nested file and changes file",
277+
arguments=("\n".join(["dir/file.txt"]), "CHANGES.md"),
278+
expected=["dir/file.txt", "CHANGES.md"],
279+
),
280+
self.ExtraFilesTestDefinition(
281+
"combines un- and nested file with changes file",
282+
arguments=("\n".join(["dir/file.txt", "README.md"]), "CHANGES.md"),
283+
expected=["README.md", "dir/file.txt", "CHANGES.md"],
284+
),
285+
]
286+
287+
for test_input in test_definitions:
288+
with self.subTest(test_input.name):
289+
self.assertCountEqual(
290+
gather_additional_files(*test_input.arguments), test_input.expected
291+
)
292+
293+
@patch("os.path.isdir")
294+
@patch("glob.glob")
295+
def test_extra_files_directory(self, mocked_glob, mocked_isdir):
296+
mocked_glob.side_effect = lambda g, *args, **kwargs: {
297+
"README.md": ["README.md"],
298+
"dir/file.txt": ["dir/file.txt"],
299+
"directory/**": ["directory/file1.txt", "directory/file2.txt"],
300+
}.get(g, [])
301+
mocked_isdir.side_effect = lambda g, *args, **kwargs: g in [
302+
"directory/",
303+
"directory",
304+
"dir",
305+
"dir/",
306+
]
307+
308+
test_definitions = [
309+
self.ExtraFilesTestDefinition(
310+
name="handles directory (no trailing slash), combines with changes file",
311+
arguments=("\n".join(["directory"]), "CHANGES.md"),
312+
expected=["directory/file1.txt", "directory/file2.txt", "CHANGES.md"],
313+
),
314+
self.ExtraFilesTestDefinition(
315+
name="handles directory (trailing slash), combines with changes file",
316+
arguments=("\n".join(["directory/"]), "CHANGES.md"),
317+
expected=["directory/file1.txt", "directory/file2.txt", "CHANGES.md"],
318+
),
319+
]
320+
321+
for test_input in test_definitions:
322+
with self.subTest(test_input.name):
323+
self.assertCountEqual(
324+
gather_additional_files(*test_input.arguments), test_input.expected
325+
)
326+
327+
@patch("os.path.isdir")
328+
@patch("glob.glob")
329+
def test_extra_files_globs(self, mocked_glob, mocked_isdir):
330+
mocked_glob.side_effect = lambda g, *args, **kwargs: {
331+
"README.md": ["README.md"],
332+
"dir/**": ["dir/file1.txt", "dir/file2.txt"],
333+
"dir/*/file*.txt": ["dir/a/file1.txt", "dir/b/file2.txt"],
334+
}.get(g, [])
335+
mocked_isdir.return_value = False
336+
337+
test_definitions = [
338+
self.ExtraFilesTestDefinition(
339+
name="handles globs, combines with changes file",
340+
arguments=("\n".join(["dir/**"]), "CHANGES.md"),
341+
expected=["dir/file1.txt", "dir/file2.txt", "CHANGES.md"],
342+
),
343+
self.ExtraFilesTestDefinition(
344+
name="handles globs, combines with literal files",
345+
arguments=("\n".join(["README.md", "dir/**"]), None),
346+
expected=["README.md", "dir/file1.txt", "dir/file2.txt"],
347+
),
348+
self.ExtraFilesTestDefinition(
349+
name="handles more complex globs, combines with changes file",
350+
arguments=("\n".join(["dir/*/file*.txt"]), "CHANGES.md"),
351+
expected=["dir/a/file1.txt", "dir/b/file2.txt", "CHANGES.md"],
352+
),
353+
]
354+
355+
for test_input in test_definitions:
356+
with self.subTest(test_input.name):
357+
self.assertCountEqual(
358+
gather_additional_files(*test_input.arguments), test_input.expected
359+
)
360+
361+
@patch("os.path.isdir")
362+
@patch("glob.glob")
363+
def test_extra_files_excludes_hidden(self, mocked_glob, mocked_isdir):
364+
# glob.glob by default doesn't match hidden files/dirs
365+
mocked_glob.side_effect = lambda g, *args, **kwargs: {
366+
"dir/**": ["dir/file1.txt", "dir/subdir/file2.txt"], # No .hidden files
367+
"**/*.txt": ["file.txt", "dir/visible.txt"], # No .hidden.txt
368+
}.get(g, [])
369+
mocked_isdir.return_value = False
370+
371+
test_definitions = [
372+
self.ExtraFilesTestDefinition(
373+
name="recursive glob excludes hidden files and directories",
374+
arguments=("\n".join(["dir/**"]), None),
375+
expected=["dir/file1.txt", "dir/subdir/file2.txt"],
376+
),
377+
self.ExtraFilesTestDefinition(
378+
name="pattern glob excludes hidden files",
379+
arguments=("\n".join(["**/*.txt"]), None),
380+
expected=["file.txt", "dir/visible.txt"],
381+
),
382+
self.ExtraFilesTestDefinition(
383+
name="multiple globs exclude hidden files, includes changes file",
384+
arguments=("\n".join(["dir/**", "**/*.txt"]), "CHANGES.md"),
385+
expected=[
386+
"dir/file1.txt",
387+
"dir/subdir/file2.txt",
388+
"file.txt",
389+
"dir/visible.txt",
390+
"CHANGES.md",
391+
],
392+
),
393+
]
394+
395+
for test_input in test_definitions:
396+
with self.subTest(test_input.name):
397+
self.assertCountEqual(
398+
gather_additional_files(*test_input.arguments), test_input.expected
399+
)
400+
401+
@patch("os.path.isdir")
402+
@patch("glob.glob")
403+
def test_extra_files_includes_explicit_hidden(self, mocked_glob, mocked_isdir):
404+
# When explicitly specified, hidden files should be included
405+
mocked_glob.side_effect = lambda g, *args, **kwargs: {
406+
".gitignore": [".gitignore"],
407+
".config/settings.json": [".config/settings.json"],
408+
}.get(g, [])
409+
mocked_isdir.return_value = False
410+
411+
test_definitions = [
412+
self.ExtraFilesTestDefinition(
413+
name="includes explicitly specified nested hidden file",
414+
arguments=("\n".join([".config/settings.json"]), None),
415+
expected=[".config/settings.json"],
416+
),
417+
self.ExtraFilesTestDefinition(
418+
name="combines explicit hidden files with changes file",
419+
arguments=("\n".join([".gitignore"]), "CHANGES.md"),
420+
expected=[".gitignore", "CHANGES.md"],
421+
),
422+
]
423+
424+
for test_input in test_definitions:
425+
with self.subTest(test_input.name):
426+
self.assertCountEqual(
427+
gather_additional_files(*test_input.arguments), test_input.expected
428+
)
429+
430+
207431
class TestTargetToArchiveName(unittest.TestCase):
208432
"""Test cases for target_to_archive_name function."""
209433

validate-inputs.py

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -136,15 +136,10 @@ def validate_extra_files(self) -> List[str]:
136136
validation_errors.append(
137137
f"Extra file '{file_path}' is not a file"
138138
)
139-
else:
140-
if not path.exists():
141-
validation_errors.append(
142-
f"Extra file '{file_path}' does not exist"
143-
)
144-
elif path.is_dir():
145-
validation_errors.append(
146-
f"Extra file '{file_path}' is a directory"
147-
)
139+
elif not path.exists():
140+
validation_errors.append(
141+
f"Extra file '{file_path}' does not exist"
142+
)
148143

149144
return validation_errors
150145

0 commit comments

Comments
 (0)