Skip to content

Replace brittle os.chdir usage in make.py with a context manager #1726

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

Merged
136 changes: 97 additions & 39 deletions make.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,20 @@
Build script for documentation
"""
import os
from contextlib import contextmanager
from shutil import which, rmtree
import subprocess
from pathlib import Path
from typing import Union, List
from typing import Union, List, Generator, Optional

PathLike = Union[Path, str, bytes]


def _resolve(p: PathLike, strict: bool = False) -> Path:
return Path(p).expanduser().resolve(strict=strict)


PROJECT_ROOT = _resolve(Path(__file__).parent, strict=True)


# General sphinx state / options
Expand All @@ -16,8 +26,11 @@
DOCDIR = "doc"
BUILDDIR = "build"


# Used for user output; relative to project root
FULL_DOC_DIR = PROJECT_ROOT / DOCDIR
FULL_BUILD_PREFIX = f"{DOCDIR}/{BUILDDIR}"
FULL_BUILD_DIR = PROJECT_ROOT / FULL_BUILD_PREFIX


# Linting
Expand All @@ -44,7 +57,6 @@
# This allows for internationalization / localization of doc.
I18NSPHINXOPTS = [*PAPER_SIZE_OPTS[PAPER_SIZE], *SPHINXOPTS, '.']

os.chdir(Path(DOCDIR))

# User-friendly check for dependencies and binaries
binaries = ['sphinx-build', 'sphinx-autobuild']
Expand Down Expand Up @@ -72,27 +84,78 @@ def find(library):
app = typer.Typer()


def run(args: Union[str, List[str]]) -> None:
result = subprocess.run(args)
@contextmanager
def cd_context(directory: PathLike) -> Generator[Path, None, None]:
"""
Temporarily move into a directory and back after, regardless of exceptions

Yields the current directory if successful. If `directory` is not found,
a FileNotFoundError will be raised. If the path exists but is a file,
a ValueError will be raised.

:param directory: The directory to cd into.
:return:
"""

# Raise FileNotFoundError if path doesn't exist
new_dir = _resolve(directory, strict=True)

if not new_dir.is_dir():
raise ValueError("Path must be a directory, not a file")

_original_dir = _resolve(Path.cwd())

# Change into the directory and yield the name
try:
os.chdir(new_dir)
yield new_dir

# Restore the old directory in an exception-resistant manner
finally:
os.chdir(_original_dir)


def run(args: Union[str, List[str]], cd: Optional[PathLike] = None) -> None:
"""
Try to run `args` with subprocess, switching into & out of `cd` if provided.

Switching back out should occur regardless of any exceptions, unless the
interpreter crashes.

:param args: the command to run.
:param cd: a directory to switch into beforehand, if any.
:return:
"""
if cd is not None:
with cd_context(_resolve(cd, strict=True)):
result = subprocess.run(args)
else:
result = subprocess.run(args)

if result.returncode != 0:
exit(result.returncode)


def run_doc(args: Union[str, List[str]]) -> None:
run(args, cd=FULL_DOC_DIR)


@app.command()
def clean():
"""
Delete built website files.
"""
if os.path.exists(BUILDDIR):
for item in Path(BUILDDIR).glob('*'):
if os.path.exists(FULL_BUILD_DIR):
for item in Path(FULL_BUILD_DIR).glob('*'):
os.remove(item) if os.path.isfile(item) else rmtree(item)


@app.command()
def html():
"""
to make standalone HTML files
"""
run([SPHINXBUILD, "-b", "html", *ALLSPHINXOPTS, f"{BUILDDIR}/html"])
run_doc([SPHINXBUILD, "-b", "html", *ALLSPHINXOPTS, f"{BUILDDIR}/html"])
print()
print(f"Build finished. The HTML pages are in {FULL_BUILD_PREFIX}/html.")

Expand All @@ -102,15 +165,15 @@ def serve():
"""
Build and serve standalone HTML files, with automatic rebuilds and live reload.
"""
run([SPHINXAUTOBUILD, *SPHINXAUTOBUILDOPTS, '-b', 'html', *ALLSPHINXOPTS, f'{BUILDDIR}/html'])
run_doc([SPHINXAUTOBUILD, *SPHINXAUTOBUILDOPTS, '-b', 'html', *ALLSPHINXOPTS, f'{BUILDDIR}/html'])


@app.command()
def dirhtml():
"""
to make HTML files named index.html in directories
"""
run([SPHINXBUILD, "-b", "dirhtml", *ALLSPHINXOPTS, f"{BUILDDIR}/dirhtml"])
run_doc([SPHINXBUILD, "-b", "dirhtml", *ALLSPHINXOPTS, f"{BUILDDIR}/dirhtml"])
print()
print(f"Build finished. The HTML pages are in {FULL_BUILD_PREFIX}/dirhtml.")

Expand All @@ -120,7 +183,7 @@ def singlehtml():
"""
to make a single large HTML file
"""
run([SPHINXBUILD, "-b", "singlehtml", *ALLSPHINXOPTS, f"{BUILDDIR}/singlehtml"])
run_doc([SPHINXBUILD, "-b", "singlehtml", *ALLSPHINXOPTS, f"{BUILDDIR}/singlehtml"])
print()
print(f"Build finished. The HTML page is in {FULL_BUILD_PREFIX}/singlehtml.")

Expand All @@ -130,7 +193,7 @@ def pickle():
"""
to make pickle files
"""
run([SPHINXBUILD, "-b", "pickle", *ALLSPHINXOPTS, f"{BUILDDIR}/pickle"])
run_doc([SPHINXBUILD, "-b", "pickle", *ALLSPHINXOPTS, f"{BUILDDIR}/pickle"])
print()
print("Build finished; now you can process the pickle files.")

Expand All @@ -140,7 +203,7 @@ def json():
"""
to make JSON files
"""
run([SPHINXBUILD, "-b", "json", *ALLSPHINXOPTS, f"{BUILDDIR}/json"])
run_doc([SPHINXBUILD, "-b", "json", *ALLSPHINXOPTS, f"{BUILDDIR}/json"])
print()
print("Build finished; now you can process the JSON files.")

Expand All @@ -150,7 +213,7 @@ def htmlhelp():
"""
to make HTML files and a HTML help project
"""
run([SPHINXBUILD, "-b", "htmlhelp", *ALLSPHINXOPTS, f"{BUILDDIR}/htmlhelp"])
run_doc([SPHINXBUILD, "-b", "htmlhelp", *ALLSPHINXOPTS, f"{BUILDDIR}/htmlhelp"])
print()
print("Build finished; now you can run HTML Help Workshop with the" +
f".hhp project file in {FULL_BUILD_PREFIX}/htmlhelp.")
Expand All @@ -161,7 +224,7 @@ def qthelp():
"""
to make HTML files and a qthelp project
"""
run([SPHINXBUILD, "-b", "qthelp", *ALLSPHINXOPTS, f"{BUILDDIR}/qthelp"])
run_doc([SPHINXBUILD, "-b", "qthelp", *ALLSPHINXOPTS, f"{BUILDDIR}/qthelp"])
print()
print('Build finished; now you can run "qcollectiongenerator" with the' +
f".qhcp project file in {FULL_BUILD_PREFIX}/qthelp, like this:")
Expand All @@ -175,7 +238,7 @@ def applehelp():
"""
to make an Apple Help Book
"""
run([SPHINXBUILD, "-b", "applehelp", *ALLSPHINXOPTS, f"{BUILDDIR}/applehelp"])
run_doc([SPHINXBUILD, "-b", "applehelp", *ALLSPHINXOPTS, f"{BUILDDIR}/applehelp"])
print()
print(f"Build finished. The help book is in {FULL_BUILD_PREFIX}/applehelp.")
print("N.B. You won't be able to view it unless you put it in" +
Expand All @@ -189,7 +252,7 @@ def devhelp():
to make HTML files and a Devhelp project
"""
home = Path.home().expanduser().resolve(strict=True)
run([SPHINXBUILD, "-b", "devhelp", *ALLSPHINXOPTS, f"{BUILDDIR}/devhelp"])
run_doc([SPHINXBUILD, "-b", "devhelp", *ALLSPHINXOPTS, f"{BUILDDIR}/devhelp"])
print()
print("Build finished.")
print("To view the help file:")
Expand All @@ -203,7 +266,7 @@ def epub():
"""
to make an epub
"""
run([SPHINXBUILD, "-b", "epub", *ALLSPHINXOPTS, f"{BUILDDIR}/epub"])
run_doc([SPHINXBUILD, "-b", "epub", *ALLSPHINXOPTS, f"{BUILDDIR}/epub"])
print()
print(f"Build finished. The epub file is in {FULL_BUILD_PREFIX}/epub.")

Expand All @@ -213,7 +276,7 @@ def latex():
"""
to make LaTeX files, you can set PAPER_SIZE=a4 or PAPER_SIZE=letter
"""
run([SPHINXBUILD, "-b", "latex", *ALLSPHINXOPTS, f"{BUILDDIR}/latex"])
run_doc([SPHINXBUILD, "-b", "latex", *ALLSPHINXOPTS, f"{BUILDDIR}/latex"])
print()
print(f"Build finished; the LaTeX files are in {FULL_BUILD_PREFIX}/latex.")
print("Run \`make' in that directory to run these through (pdf)latex" +
Expand All @@ -225,9 +288,9 @@ def latexpdf():
"""
to make LaTeX files and run them through pdflatex
"""
run([SPHINXBUILD, "-b", "latex", *ALLSPHINXOPTS, f"{BUILDDIR}/latex"])
run_doc([SPHINXBUILD, "-b", "latex", *ALLSPHINXOPTS, f"{BUILDDIR}/latex"])
print("Running LaTeX files through pdflatex...")
run(['make', '-C', f'{BUILDDIR}/latex', 'all-pdf'])
run_doc(['make', '-C', f'{BUILDDIR}/latex', 'all-pdf'])
print(f"pdflatex finished; the PDF files are in {FULL_BUILD_PREFIX}/latex.")


Expand All @@ -236,9 +299,9 @@ def latexpdfja():
"""
to make LaTeX files and run them through platex/dvipdfmx
"""
run([SPHINXBUILD, "-b", "latex", *ALLSPHINXOPTS, f"{BUILDDIR}/latex"])
run_doc([SPHINXBUILD, "-b", "latex", *ALLSPHINXOPTS, f"{BUILDDIR}/latex"])
print("Running LaTeX files through platex and dvipdfmx...")
run(['make', '-C', f'{BUILDDIR}/latex', 'all-pdf-ja'])
run_doc(['make', '-C', f'{BUILDDIR}/latex', 'all-pdf-ja'])
print(f"pdflatex finished; the PDF files are in {FULL_BUILD_PREFIX}/latex.")


Expand All @@ -247,7 +310,7 @@ def text():
"""
to make text files
"""
run([SPHINXBUILD, "-b", "text", *ALLSPHINXOPTS, f"{BUILDDIR}/text"])
run_doc([SPHINXBUILD, "-b", "text", *ALLSPHINXOPTS, f"{BUILDDIR}/text"])
print()
print(f"Build finished. The text files are in {FULL_BUILD_PREFIX}/text.")

Expand All @@ -257,7 +320,7 @@ def man():
"""
to make manual pages
"""
run([SPHINXBUILD, "-b", "man", *ALLSPHINXOPTS, f"{BUILDDIR}/man"])
run_doc([SPHINXBUILD, "-b", "man", *ALLSPHINXOPTS, f"{BUILDDIR}/man"])
print()
print(f"Build finished. The manual pages are in {FULL_BUILD_PREFIX}/man.")

Expand All @@ -267,7 +330,7 @@ def texinfo():
"""
to make Texinfo files
"""
run([SPHINXBUILD, "-b", "texinfo", *ALLSPHINXOPTS, f"{BUILDDIR}/texinfo"])
run_doc([SPHINXBUILD, "-b", "texinfo", *ALLSPHINXOPTS, f"{BUILDDIR}/texinfo"])
print()
print(f"Build finished. The Texinfo files are in {FULL_BUILD_PREFIX}/texinfo.")
print("Run \`make' in that directory to run these through makeinfo" +
Expand All @@ -279,9 +342,9 @@ def info():
"""
to make Texinfo files and run them through makeinfo
"""
run([SPHINXBUILD, "-b", "texinfo", *ALLSPHINXOPTS, f"{BUILDDIR}/texinfo"])
run_doc([SPHINXBUILD, "-b", "texinfo", *ALLSPHINXOPTS, f"{BUILDDIR}/texinfo"])
print("Running Texinfo files through makeinfo...")
run(['make', '-C', f'{BUILDDIR}/texinfo', 'info'])
run_doc(['make', '-C', f'{BUILDDIR}/texinfo', 'info'])
print(f"makeinfo finished; the Info files are in {FULL_BUILD_PREFIX}/texinfo.")


Expand All @@ -290,7 +353,7 @@ def gettext():
"""
to make PO message catalogs
"""
run([SPHINXBUILD, "-b", "gettext", *I18NSPHINXOPTS, f"{BUILDDIR}/locale"])
run_doc([SPHINXBUILD, "-b", "gettext", *I18NSPHINXOPTS, f"{BUILDDIR}/locale"])
print()
print(f"Build finished. The message catalogs are in {FULL_BUILD_PREFIX}/locale.")

Expand All @@ -300,7 +363,7 @@ def changes():
"""
to make an overview of all changed/added/deprecated items
"""
run([SPHINXBUILD, "-b", "changes", *ALLSPHINXOPTS, f"{BUILDDIR}/changes"])
run_doc([SPHINXBUILD, "-b", "changes", *ALLSPHINXOPTS, f"{BUILDDIR}/changes"])
print()
print(f"The overview file is in {FULL_BUILD_PREFIX}/changes.")

Expand All @@ -310,7 +373,7 @@ def linkcheck():
"""
to check all external links for integrity
"""
run([SPHINXBUILD, "-b", "linkcheck", *ALLSPHINXOPTS, f"{BUILDDIR}/linkcheck"])
run_doc([SPHINXBUILD, "-b", "linkcheck", *ALLSPHINXOPTS, f"{BUILDDIR}/linkcheck"])
print()
print("Link check complete; look for any errors in the above output " +
f"or in {FULL_BUILD_PREFIX}/linkcheck/output.txt.")
Expand All @@ -321,7 +384,7 @@ def doctest():
"""
to run all doctests embedded in the documentation (if enabled)
"""
run([SPHINXBUILD, "-b", "doctest", *ALLSPHINXOPTS, f"{BUILDDIR}/doctest"])
run_doc([SPHINXBUILD, "-b", "doctest", *ALLSPHINXOPTS, f"{BUILDDIR}/doctest"])
print("Testing of doctests in the sources finished, look at the " +
f"results in {FULL_BUILD_PREFIX}/doctest/output.txt.")

Expand All @@ -331,28 +394,27 @@ def coverage():
"""
to run coverage check of the documentation (if enabled)
"""
run([SPHINXBUILD, "-b", "coverage", *ALLSPHINXOPTS, f"{BUILDDIR}/coverage"])
run_doc([SPHINXBUILD, "-b", "coverage", *ALLSPHINXOPTS, f"{BUILDDIR}/coverage"])
print("Testing of coverage in the sources finished, look at the " +
f"results in {FULL_BUILD_PREFIX}/coverage/python.txt.")


@app.command()
def xml():
run([SPHINXBUILD, "-b", "xml", *ALLSPHINXOPTS, f"{BUILDDIR}/xml"])
run_doc([SPHINXBUILD, "-b", "xml", *ALLSPHINXOPTS, f"{BUILDDIR}/xml"])
print()
print(f"Build finished. The XML files are in {FULL_BUILD_PREFIX}/xml.")


@app.command()
def pseudoxml():
run([SPHINXBUILD, "-b", "pseudoxml", *ALLSPHINXOPTS, f"{BUILDDIR}/pseudoxml"])
run_doc([SPHINXBUILD, "-b", "pseudoxml", *ALLSPHINXOPTS, f"{BUILDDIR}/pseudoxml"])
print()
print(f"Build finished. The pseudo-XML files are in {FULL_BUILD_PREFIX}/pseudoxml.")


@app.command()
def lint():
os.chdir("../")
run([RUFF, *RUFFOPTS])
print("Ruff Finished.")
run([MYPY, *MYPYOPTS])
Expand All @@ -362,27 +424,23 @@ def lint():

@app.command()
def ruff():
os.chdir("../")
run([RUFF, *RUFFOPTS])
print("Ruff Finished.")


@app.command()
def mypy():
os.chdir("../")
run([MYPY, *MYPYOPTS])
print("MyPy Finished.")


@app.command()
def test_full():
os.chdir("../")
run([PYTEST, TESTDIR])


@app.command()
def test():
os.chdir("../")
run([PYTEST, UNITTESTS])


Expand Down