Skip to content

Commit

Permalink
Handle bulk downloads in various source languages
Browse files Browse the repository at this point in the history
Include non-Python source files in the source code zip, and adjust
the file name if it includes non-Python sources.

Only generate Jupyter notebook zip file and link if at least
some of the files are Jupyter compatible.
  • Loading branch information
speth committed Sep 30, 2023
1 parent 8537a94 commit 439eca5
Show file tree
Hide file tree
Showing 3 changed files with 73 additions and 29 deletions.
73 changes: 47 additions & 26 deletions sphinx_gallery/downloads.py
Expand Up @@ -16,11 +16,13 @@
.. container:: sphx-glr-download sphx-glr-download-python
:download:`Download all examples in Python source code: {0} </{1}>`
:download:`Download all examples {0} source code: {1} </{2}>`
"""

NOTEBOOK_ZIP_DOWNLOAD = """
.. container:: sphx-glr-download sphx-glr-download-jupyter
:download:`Download all examples in Jupyter notebooks: {2} </{3}>`
:download:`Download all examples in Jupyter notebooks: {0} </{1}>`
"""


Expand All @@ -33,84 +35,103 @@ def python_zip(file_list, gallery_path, extension=".py"):
Holds all the file names to be included in zip file
gallery_path : str
path to where the zipfile is stored
extension : str
'.py' or '.ipynb' In order to deal with downloads of python
sources and jupyter notebooks the file extension from files in
file_list will be removed and replace with the value of this
variable while generating the zip file
extension : str | None
In order to deal with downloads of plain source files and jupyter notebooks, if
this value is not None, the file extension from files in file_list will be
removed and replace with the value of this variable while generating the zip
file
Returns
-------
zipname : str
zip file name, written as `target_dir_{python,jupyter}.zip`
depending on the extension
zip file name, written as `target_dir_python.zip`, `target_dir_jupyter.zip`,
or `target_dir.zip` depending on the extension
"""
zipname = os.path.basename(os.path.normpath(gallery_path))
zipname += "_python" if extension == ".py" else "_jupyter"
if extension == ".py":
zipname += "_python"
elif extension == ".ipynb":
zipname += "_jupyter"
zipname = os.path.join(gallery_path, zipname + ".zip")
zipname_new = zipname + ".new"
with zipfile.ZipFile(zipname_new, mode="w") as zipf:
for fname in file_list:
file_src = os.path.splitext(fname)[0] + extension
zipf.write(file_src, os.path.relpath(file_src, gallery_path))
if extension is not None:
fname = os.path.splitext(fname)[0] + extension
zipf.write(fname, os.path.relpath(fname, gallery_path))
_replace_md5(zipname_new)
return zipname


def list_downloadable_sources(target_dir):
"""Return a list of python source files is target_dir.
def list_downloadable_sources(target_dir, extensions=(".py",)):
"""Return a list of source files in target_dir.
Parameters
----------
target_dir : str
path to the directory where python source file are
path to the directory where source file are
extensions : tuple[str]
tuple of file extensions to include
Returns
-------
list
list of paths to all Python source files in `target_dir`
list of paths to all source files in `target_dir` ending with one of the
specified extensions
"""
return [
os.path.join(target_dir, fname)
for fname in os.listdir(target_dir)
if fname.endswith(".py")
if fname.endswith(extensions)
]


def generate_zipfiles(gallery_dir, src_dir):
def generate_zipfiles(gallery_dir, src_dir, gallery_conf):
"""Collects downloadable sources and makes zipfiles of them.
Collects all Python source files and Jupyter notebooks in gallery_dir.
Collects all source files and Jupyter notebooks in gallery_dir.
Parameters
----------
gallery_dir : str
path of the gallery to collect downloadable sources
src_dir : str
The build source directory. Needed to make the reST paths relative.
gallery_conf : dict[str, Any]
Sphinx-Gallery configuration dictionary
Return
------
download_rst: str
RestructuredText to include download buttons to the generated files
"""
listdir = list_downloadable_sources(gallery_dir)
src_ext = tuple(gallery_conf["example_extensions"])
notebook_ext = tuple(gallery_conf["notebook_extensions"])
source_files = list_downloadable_sources(gallery_dir, src_ext)
notebook_files = list_downloadable_sources(gallery_dir, notebook_ext)
for directory in sorted(os.listdir(gallery_dir)):
if os.path.isdir(os.path.join(gallery_dir, directory)):
target_dir = os.path.join(gallery_dir, directory)
listdir.extend(list_downloadable_sources(target_dir))

py_zipfile = python_zip(listdir, gallery_dir)
jy_zipfile = python_zip(listdir, gallery_dir, ".ipynb")
source_files.extend(list_downloadable_sources(target_dir, src_ext))
notebook_files.extend(list_downloadable_sources(target_dir, notebook_ext))

def rst_path(filepath):
filepath = os.path.relpath(filepath, os.path.normpath(src_dir))
return filepath.replace(os.sep, "/")

all_python = all(f.endswith(".py") for f in source_files)
py_zipfile = python_zip(source_files, gallery_dir, ".py" if all_python else None)
dw_rst = CODE_ZIP_DOWNLOAD.format(
"in Python" if all_python else "as",
os.path.basename(py_zipfile),
rst_path(py_zipfile),
os.path.basename(jy_zipfile),
rst_path(jy_zipfile),
)

if notebook_files:
jy_zipfile = python_zip(notebook_files, gallery_dir, ".ipynb")
dw_rst += NOTEBOOK_ZIP_DOWNLOAD.format(
os.path.basename(jy_zipfile),
rst_path(jy_zipfile),
)

return dw_rst
2 changes: 1 addition & 1 deletion sphinx_gallery/gen_gallery.py
Expand Up @@ -674,7 +674,7 @@ def generate_gallery_rst(app):

if gallery_conf["download_all_examples"]:
download_fhindex = generate_zipfiles(
gallery_dir_abs_path, app.builder.srcdir
gallery_dir_abs_path, app.builder.srcdir, gallery_conf
)
indexst += download_fhindex

Expand Down
27 changes: 25 additions & 2 deletions sphinx_gallery/tests/test_gen_rst.py
Expand Up @@ -716,18 +716,41 @@ def test_thumbnail_path(test_str):
assert file_conf == {"thumbnail_path": "_static/demo.png"}


def test_zip_notebooks(gallery_conf):
"""Test generated zipfiles are not corrupt."""
def test_zip_python(gallery_conf):
"""Test generated zipfiles are not corrupt and have expected name and contents."""
gallery_conf.update(examples_dir=os.path.join(gallery_conf["src_dir"], "examples"))
shutil.copytree(
os.path.join(os.path.dirname(__file__), "tinybuild", "examples"),
gallery_conf["examples_dir"],
)
examples = downloads.list_downloadable_sources(gallery_conf["examples_dir"])
zipfilepath = downloads.python_zip(examples, gallery_conf["gallery_dir"])
assert zipfilepath.endswith("_python.zip")
zipf = zipfile.ZipFile(zipfilepath)
check = zipf.testzip()
assert not check, f"Bad file in zipfile: {check}"
filenames = {item.filename for item in zipf.filelist}
assert "examples/plot_command_line_args.py" in filenames
assert "examples/julia_sample.jl" not in filenames


def test_zip_mixed_source(gallery_conf):
"""Test generated zipfiles are not corrupt and have expected name and contents."""
gallery_conf.update(examples_dir=os.path.join(gallery_conf["src_dir"], "examples"))
shutil.copytree(
os.path.join(os.path.dirname(__file__), "tinybuild", "examples"),
gallery_conf["examples_dir"],
)
examples = downloads.list_downloadable_sources(
gallery_conf["examples_dir"], (".py", ".jl")
)
zipfilepath = downloads.python_zip(examples, gallery_conf["gallery_dir"], None)
zipf = zipfile.ZipFile(zipfilepath)
check = zipf.testzip()
assert not check, f"Bad file in zipfile: {check}"
filenames = {item.filename for item in zipf.filelist}
assert "examples/plot_command_line_args.py" in filenames
assert "examples/julia_sample.jl" in filenames


def test_rst_example(gallery_conf):
Expand Down

0 comments on commit 439eca5

Please sign in to comment.