pax_global_header00006660000000000000000000000064150344166250014520gustar00rootroot0000000000000052 comment=11a221a608131e792bed7f9b821ed88030559911 hatch-sphinx-0.0.4/000077500000000000000000000000001503441662500141175ustar00rootroot00000000000000hatch-sphinx-0.0.4/.github/000077500000000000000000000000001503441662500154575ustar00rootroot00000000000000hatch-sphinx-0.0.4/.github/workflows/000077500000000000000000000000001503441662500175145ustar00rootroot00000000000000hatch-sphinx-0.0.4/.github/workflows/ci.yml000066400000000000000000000031731503441662500206360ustar00rootroot00000000000000name: Test on: [push, pull_request] defaults: run: shell: bash jobs: test: runs-on: ${{ matrix.os }} strategy: matrix: os: [macos-latest, ubuntu-latest, windows-latest] python-version: ["3.10", "3.11", "3.12", "3.13"] steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Install Python dependencies run: | python -m pip install --upgrade pip python -m pip install build pytest - name: Build and install package run: | python -m build --wheel find dist -type f python -m pip install dist/*.whl - name: Test with pytest run: | pytest -v - name: Integration test run: | # build sasdata to make sure it builds the package # location of the wheel for hatch-sphinx DIST=$PWD/dist # clean directory for this test mkdir buildtest cd buildtest # obtain the sdist for sasdata; if only this could be done reliably with pip pip install unearth unearth --no-binary --download=. sasdata # build sasdata with the newly created hatch-sphinx wheel cd sasdata* pip wheel -v --find-links=$DIST --pre --no-binary sasdata . # Rudimentary inspection of the wheel zipinfo sasdata*whl if [ $(zipinfo sasdata*whl | grep -c sasdata/docs/) -lt 100 ]; then echo "Didn't find sphinx output in wheel" exit 1 fi hatch-sphinx-0.0.4/.gitignore000066400000000000000000000004321503441662500161060ustar00rootroot00000000000000__pycache__/ *.py[cod] *$py.class .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ sdist/ wheels/ *.egg-info/ .installed.cfg *.egg pip-log.txt htmlcov/ .cache nosetests.xml coverage.xml .pytest_cache/ *.log .env .venv env/ venv/ .mypy_cache/ hatch_sphinx/_version.py hatch-sphinx-0.0.4/LICENSE000066400000000000000000000020721503441662500151250ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2024 Stuart Prescott Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. hatch-sphinx-0.0.4/README.md000066400000000000000000000125061503441662500154020ustar00rootroot00000000000000# Hatch Sphinx Plugin A plugin for [Hatch](https://github.com/pypa/hatch) that allows you to build documentation with Sphinx and include the output in your package distributions. ## Installation `hatch-sphinx` is a plugin for the `hatchling` build system, so to use it in your project you'll need to to be using that build-backend. You can add `hatch-sphinx` to your project's `pyproject.toml` file as a `build-system` requirement: ```toml [build-system] requires = ["hatchling", "hatch-sphinx"] build-backend = "hatchling.build" ``` Standard Python package builders (`pip install `, `python -m build`, `hatchling build`, ...) will install the `hatch-sphinx` package for you into the build environment. (Or at least, they will do so once `hatch-sphinx` is uploaded to PyPI!) ## Usage The set of Sphinx build steps needed by your project are configured in the the `[[tool.hatch.build.targets.wheel.hooks.sphinx.tools]]` tables in your `pyproject.toml` file. You can repeat this table multiple times to run different tools (e.g. sphinx-apidoc and then sphinx-build). Which tool is being run is configured via the `tool` configuration key. The known values are `build`, `apidoc`, `custom`, and the different configuration keys they support are listed below ### Common options | Key | Default | Description | | --- | ------- | ----------- | | `tool` | required | Select the tool to be run `build`, `apidoc` or `custom` | | `doc_dir` | `"doc"`, `"docs"`, `"."` (first existing) | The 'base' of the Sphinx documentation tree; set as the working directory prior to invoking any tool. | | `out_dir` | `"output"` | The directory the tool outputs will be placed into, relative do `doc_dir`; created if it doesn't exist and deleted on clean. | | `work_dir` | `"."` | The directory to run the commands in. All artifact patterns are relative to this directory. | | `sphinx_opts` | `""` | Any additional options to be sent to the tool. | | `environment` | `{}` | Dictionary of environment settings passed to the tool. | ### Tool `apidoc` options | Key | Default | Description | | --- | ------- | ----------- | | `tool` | required | `apidoc` | | `depth` | `3` | API depth to be included (`-d 3` option). | | `private` | `false` | Include private members in documentation (`--private` option). | | `separate` | `true` | Make a separate rst file per module (`--separate` option). | | `header` | `None` | Header for documentation (`-H header` option). | | `source` | `"."` | Source code to be included in the API docs. | | `tool_apidoc` | `["python", "-m", "sphinx.ext.apidoc"` | Command to run as a list of strings | ### Tool `build` options | Key | Default | Description | | --- | ------- | ----------- | | `tool` | required | `build` | | `format` | `"html"` | Output format for documentation (`-b html` option). | | `warnings` | `false` | Treat warnings as errors (`--warnings` option). | | `keep_going` | `false` | Continue after warnings rather exiting immediately (`--keep-going` option). | | `source` | `"."` | Source code to be included in the docs. | | `tool_build` | `["python", "-m", "sphinx", "build"` | Command to run as a list of strings | ### Tool `custom` options | Key | Default | Description | | --- | ------- | ----------- | | `tool` | required | `custom` | | `commands` | required | List of commands to be executed; the magic string `{python}` is replaced with current interpreter (`sys.executable`). Each command can be a list of strings (preferred) or a single string. | | `shell` | `false` | Whether to run the command via the shell (i.e. the `shell` parameter for `subprocess.run`) which permits wildcard expansion and scripting; note that the command cannot be a list of strings in `shell=true` mode. The standard warnings from `subprocess.run` to avoid using the shell apply here too. | | `expand_globs` | `false` | Whether to expand globs in the command arguments. | Notes: - Not all combinations of `shell=true`, `expand_globs=true` and the individual command being a single string are supported. The recommended configuration is to use `shell=false` and each command as a list, with `expand_globs=true` if wildcard expansion is needed. - Quoting within a single string is almost impossible to do in a portable fashion. If you need to work with filenames with spaces, for instance, then avoid trying to stack backslashes in this in an effort to get this to work. ### Examples A sample configuration for generating the API docs and then running Sphinx ```toml [[tool.hatch.build.targets.wheel.hooks.sphinx.tools]] tool = "apidoc" source = "../src/mymodule" out_dir = "source/api" depth = 4 header = "MyModule API Documentation" [[tool.hatch.build.targets.wheel.hooks.sphinx.tools]] tool = "build" format = "html" source = "source" out_dir = "build" [[tool.hatch.build.targets.wheel.hooks.sphinx.tools]] tool = "custom" out_dir = "build" shell = false expand_globs = true commands = [ [ "ls", "-l", "*.py" ], "ls -l *.py", [ "{python}", "-c", "import shutil; shutil.copytree('foo', 'bar')"], ] ``` ## Notes - The examples above are focusing on the `wheel` stage but it is possible to build the docs in the `sdist` stage instead if desired. - The `output` directory is deleted in the clean step if used (e.g. `hatchling build --clean`) ## To-do list - support a `make` tool for `Makefile` based invocation of Sphinx - add option to allow `commands` to exit uncleanly hatch-sphinx-0.0.4/hatch_sphinx/000077500000000000000000000000001503441662500165775ustar00rootroot00000000000000hatch-sphinx-0.0.4/hatch_sphinx/__init__.py000066400000000000000000000002651503441662500207130ustar00rootroot00000000000000"""hatch-sphinx: hatchling build plugin for Sphinx document builder""" try: from ._version import __version__ # type: ignore except ImportError: __version__ = "0.0.0.dev" hatch-sphinx-0.0.4/hatch_sphinx/hooks.py000066400000000000000000000006241503441662500202760ustar00rootroot00000000000000"""Register hooks for the plugin.""" from hatchling.plugin import hookimpl from hatchling.builders.hooks.plugin.interface import BuildHookInterface from hatchling.builders.config import BuilderConfig from hatch_sphinx.plugin import SphinxBuildHook @hookimpl def hatch_register_build_hook() -> type[BuildHookInterface[BuilderConfig]]: """Get the hook implementation.""" return SphinxBuildHook hatch-sphinx-0.0.4/hatch_sphinx/plugin.py000066400000000000000000000351001503441662500204460ustar00rootroot00000000000000"""hatch-sphinx: hatchling build plugin for Sphinx document system""" from __future__ import annotations from collections.abc import Sequence import contextlib from dataclasses import MISSING, asdict, dataclass, field, fields import logging import glob import os from pathlib import Path import subprocess import shlex import shutil import sys from typing import Any, Optional from hatchling.builders.hooks.plugin.interface import BuildHookInterface from hatchling.builders.config import BuilderConfig try: from contextlib import chdir except ImportError: # CRUFT: contextlib.chdir added Python 3.11 class chdir(contextlib.AbstractContextManager): # type: ignore # pylint: disable=invalid-name """Non thread-safe context manager to change the current working directory.""" def __init__(self, path: str) -> None: self.path = path self._old_cwd: list[str] = [] def __enter__(self) -> None: self._old_cwd.append(os.getcwd()) os.chdir(self.path) def __exit__(self, *excinfo: Any) -> None: os.chdir(self._old_cwd.pop()) log = logging.getLogger(__name__) log_level = logging.getLevelName(os.getenv("HATCH_SPHINX_LOG_LEVEL", "INFO")) log.setLevel(log_level) if os.getenv("HATCH_SPHINX_LOG_LEVEL", None): logging.basicConfig(level=log_level) class SphinxBuildHook(BuildHookInterface[BuilderConfig]): """Build hook to run Sphinx tools during the build""" PLUGIN_NAME = "sphinx" def clean( self, versions: list[str], ) -> None: """Clean the output from all configured tools""" root_path = Path(self.root) all_tools = load_tools(self.config) for tool in all_tools: log.debug("Tool config: %s", asdict(tool)) doc_path = tool.auto_doc_path(root_path) out_path = doc_path / tool.out_dir self.app.display(f"Cleaning {out_path}") shutil.rmtree(out_path, ignore_errors=True) def initialize( self, version: str, # noqa: ARG002 build_data: dict[str, Any], ) -> None: """Run whatever sphinx tools are configured""" # Start with some info for the process self.app.display_mini_header(self.PLUGIN_NAME) self.app.display_debug("options") self.app.display_debug(str(self.config), level=1) root_path = Path(self.root) all_tools = load_tools(self.config) for tool in all_tools: log.debug("Tool config: %s", asdict(tool)) # Ensure output location exists doc_path = tool.auto_doc_path(root_path) out_path = doc_path / tool.out_dir out_path.mkdir(parents=True, exist_ok=True) # Locate the appropriate function to run for this tool try: tool_runner = getattr(self, f"_run_{tool.tool}") except AttributeError as e: self.app.display_error( f"hatch-sphinx: unknown tool requested in: {tool.tool}" ) self.app.display_error(f"hatch-sphinx: error: {e}") self.app.abort("hatch-sphinx: aborting build due to misconfiguration") raise # Run the tool self.app.display_info(f"hatch-sphinx: running sphinx tool {tool.tool}") result = tool_runner(doc_path, out_path, tool) # Report the result if result: self.app.display_success( f"hatch-sphinx: tool {tool.tool} completed successfully" ) else: self.app.display_error(f"hatch-sphinx: tool {tool.tool} failed") self.app.abort("hatch-sphinx: aborting on failure") def _run_build(self, doc_path: Path, out_path: Path, tool: ToolConfig) -> bool: """run sphinx-build""" args: list[str | None] = [ *( tool.tool_build if tool.tool_build else [sys.executable, "-m", "sphinx", "build"] ), "-W" if tool.warnings else None, "--keep-going" if tool.keep_going else None, "-b" if tool.format else None, tool.format, *(shlex.split(tool.sphinx_opts) if tool.sphinx_opts else []), tool.source, str(out_path.resolve()), ] cleaned_args = list(filter(None, args)) self.app.display_info(f"hatch-sphinx: executing: {cleaned_args}") try: res = subprocess.run( cleaned_args, check=False, cwd=doc_path, shell=False, env=self._env(tool), ) except OSError as e: self.app.display_error( "hatch-sphinx: could not execute sphinx-build. Is it installed?" ) self.app.display_error(f"hatch-sphinx: error: {e}") return False return res.returncode == 0 def _run_apidoc(self, doc_path: Path, out_path: Path, tool: ToolConfig) -> bool: """run sphinx-apidoc""" tool.source = tool.source.rstrip("/") args: list[str | None] = [ *( tool.tool_apidoc if tool.tool_apidoc else [sys.executable, "-m", "sphinx.ext.apidoc"] ), "-o", str(out_path.resolve()), "-d", str(tool.depth), "--private" if tool.private else None, "--separate" if tool.separate else None, "-H" if tool.header else None, tool.header, *(shlex.split(tool.sphinx_opts) if tool.sphinx_opts else []), tool.source, *(tool.source + os.sep + e for e in tool.exclude), ] cleaned_args = list(filter(None, args)) self.app.display_info(f"hatch-sphinx: executing: {cleaned_args}") try: res = subprocess.run( cleaned_args, check=False, cwd=doc_path, shell=False, env=self._env(tool), ) except OSError as e: self.app.display_error( "hatch-sphinx: could not execute sphinx-apidoc. Is it installed?" ) self.app.display_error(f"hatch-sphinx: error: {e}") return False return res.returncode == 0 def _run_custom( self, doc_path: Path, out_path: Path, # pylint: disable=unused-argument tool: ToolConfig, ) -> bool: """run a custom command""" for c in tool.commands: # Matrix of options # # shell globs type(c) supported # # True True str ✘ # True True list ✘ # True False str ✔ # True False list ✘ # # False True str ✔ # False True list ✔ # False False str ✔ # False False list ✔ # When args is a list, args needs to be joined to make a string # for the shell in shell=True mode, but this cannot be done # reliably, so is unsupported aborting = False if not isinstance(c, (str, list, tuple)): # shouldn't be possible but better to check than have an issue self.app.display_error( f"hatch-sphinx: unknown type for command {type(c)}" ) aborting = True # normalise the iterable type if isinstance(c, tuple): c = list(c) if tool.shell and not isinstance(c, str): self.app.display_error( "hatch-sphinx: cannot pass a list of args for a custom command " "in shell=true mode." ) aborting = True if tool.expand_globs and tool.shell: self.app.display_error( "hatch-sphinx: expanding globs cannot be done reliably in shell=true mode." ) aborting = True if aborting: self.app.abort("hatch-sphinx: exiting on misconfiguration.") # Where we can, work with a list not a str if not tool.shell and not isinstance(c, list): c = shlex.split(c) self.app.display_debug( "hatch-sphinx: splitting the command with shlex.split; avoid " "this by specifying the command as a list in the configuration. " f"Command: {c}" ) # c is either a list: [command, option1, option2, ...] # or a str: "command option1 option2" # process it for: # - tokens in the command like {python}, always # - glob expansion, if configured c = self._replace_tokens(c) if tool.expand_globs: assert isinstance(c, list) # config options above enforce this c = self._expand_globs(c, doc_path) self.app.display_info(f"hatch-sphinx: executing '{c}'") try: subprocess.run( c, check=True, cwd=doc_path, shell=tool.shell, env=self._env(tool) ) except (OSError, subprocess.CalledProcessError) as e: self.app.display_error(f"hatch-sphinx: command failed: {e}") return False return True def _env(self, tool: ToolConfig) -> dict[str, str]: """merge in any extra environment variables specified in the config""" env = os.environ.copy() if tool.environment: for k, v in tool.environment.items(): if k in env: if k == "PYTHONPATH": env[k] = f"{v}{os.pathsep}{env[k]}" else: self.app.display_warning( "hatch-sphinx: overwriting environment from configuration: " f"{k}: {v}" ) else: env[k] = v return env def _expand_globs(self, args: list[str], root_dir: str | Path) -> list[str]: """expand globs in the command""" expanded = [] for a in args: if "*" in a or "?" in a or "[" in a: # CRUFT: root_dir arg added to glob in Python 3.10 # globs = glob.glob(a, root_dir=root_dir) with chdir(root_dir): globs = glob.glob(a) if not globs: self.app.display_warning( f"hatch-sphinx: glob '{a}' evaluated to empty string: " "this is probably not what you want." ) expanded.extend(globs) else: expanded.append(a) return expanded def _replace_tokens(self, args: str | list[str]) -> str | list[str]: """replace defined tokens in the command""" if isinstance(args, str): return self._replace_tokens_str(args) return [self._replace_tokens_str(a) for a in args] def _replace_tokens_str(self, arg: str) -> str: """replace defined tokens in the command""" return arg.replace("{python}", sys.executable) def load_tools(config: dict[str, Any]) -> Sequence[ToolConfig]: """Obtain all config related to this plugin""" tool_defaults = dataclass_defaults(ToolConfig) tool_defaults.update({k: config[k] for k in tool_defaults if k in config}) return [ ToolConfig(**{**tool_defaults, **tool_config}) for tool_config in config.get("tools", []) ] @dataclass class ToolConfig(BuilderConfig): """A configuration for a sphinx tool.""" # pylint: disable=too-many-instance-attributes tool: str """The sphinx tool to be used: apidoc, build, custom""" doc_dir: Optional[str] = None """Path where sphinx sources are to be found. defaults to doc, docs, .; relative to root of build""" out_dir: str = "output" """Path where sphinx build output will be saved. Relative to {doc_dir} """ sphinx_opts: str = "" """Additional options for the tool; will be split using shlex""" environment: dict[str, str] = field(default_factory=dict) """Extra environment variables for tool execution""" # Config items for the 'build' tool tool_build: Optional[list[str]] = None """Command to use (defaults to `python -m sphinx build`)""" format: str = "html" """Output format selected for 'build' tool""" warnings: bool = False """-W: Turn warnings into errors""" keep_going: bool = False """--keep-going: With -W option, keep going after warnings""" # Config items for the 'apidoc' tool tool_apidoc: Optional[list[str]] = None """Command to use (defaults to `python -m sphinx apidoc`)""" depth: int = 3 """Depth to recurse into the structures for API docs""" private: bool = False """Include private members in API docs""" separate: bool = True """Split each module into a separate page""" header: Optional[str] = None """Header to use on the API docs""" source: str = "." """Source to be included in the API docs""" exclude: list[str] = field(default_factory=list) """Patterns of filepaths to exclude from analysis""" # Config items for the 'commands' tool commands: list[str | list[str]] = field(default_factory=list) """Custom command to run within the {doc_dir}; if provided as a str then it is split with shlex.split prior to use""" shell: bool = False """Let the shell expand the command""" expand_globs: bool = False """Expand globs in the command prior to running, particularly useful for avoiding shell=true on non-un*x systems""" def auto_doc_path(self, root: Path) -> Path: """Determine the doc root for sphinx""" if self.doc_dir: return root / self.doc_dir for d in ["doc", "docs"]: p = root / d if p.exists() and p.is_dir(): return p return root def dataclass_defaults(obj: Any) -> dict[str, Any]: """Find the default values from the dataclass Permits easy updating from toml later """ defaults: dict[str, Any] = {} for f in fields(obj): if f.default is not MISSING: defaults[f.name] = f.default elif f.default_factory is not MISSING: defaults[f.name] = f.default_factory() return defaults hatch-sphinx-0.0.4/hatch_sphinx/py.typed000066400000000000000000000000001503441662500202640ustar00rootroot00000000000000hatch-sphinx-0.0.4/pyproject.toml000066400000000000000000000024721503441662500170400ustar00rootroot00000000000000[build-system] requires = [ "hatchling", "hatch-vcs", ] build-backend = "hatchling.build" [project] name = "hatch-sphinx" dynamic = ["version"] description = 'A plugin for Hatch for building documentation with Sphinx' readme = "README.md" requires-python = ">=3.9" license = "MIT" keywords = [] authors = [ { name = "Stuart Prescott", email = "stuart@debian.org" }, ] classifiers = [ "Framework :: Hatch", "Development Status :: 4 - Beta", "Programming Language :: Python", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] dependencies = [ "hatchling", "sphinx", ] [project.urls] Documentation = "https://github.com/llimeht/hatch-sphinx#readme" Issues = "https://github.com/llimeht/hatch-sphinx/issues" Source = "https://github.com/llimeht/hatch-sphinx" [tool.hatch.version] source = "vcs" [tool.hatch.build.hooks.vcs] version-file = "hatch_sphinx/_version.py" [project.entry-points.hatch] sphinx = "hatch_sphinx.hooks" [tool.hatch.build.targets.wheel] include = [ "hatch_sphinx/*.py", "hatch_sphinx/py.typed", ] hatch-sphinx-0.0.4/tests/000077500000000000000000000000001503441662500152615ustar00rootroot00000000000000hatch-sphinx-0.0.4/tests/__init__.py000066400000000000000000000000001503441662500173600ustar00rootroot00000000000000hatch-sphinx-0.0.4/tests/conftest.py000066400000000000000000000057421503441662500174700ustar00rootroot00000000000000"""pytest fixtures for hatch-sphinx""" from __future__ import annotations from collections.abc import Iterator from contextlib import contextmanager from pathlib import Path import shutil import subprocess import sys from tempfile import TemporaryDirectory from typing import Generator import zipfile import pytest @pytest.fixture(scope="session") def plugin_path() -> Generator[Path, None, None]: """make a copy of the hatch-sphinx plugin for testing See https://hatch.pypa.io/latest/how-to/plugins/testing-builds/ """ with TemporaryDirectory() as d: directory = Path(d, "plugin") shutil.copytree(Path.cwd(), directory, ignore=shutil.ignore_patterns(".git")) yield directory.resolve() @pytest.fixture def new_project( tmp_path: Path, plugin_path: Path, # pylint: disable=redefined-outer-name ) -> FixtureProject: """create a new Python project as a fixture""" project_dir = tmp_path / "my-test-app" project_dir.mkdir() project = FixtureProject(project_dir) project.pyproject.write_text( f"""\ [build-system] requires = ["hatchling", "hatch-sphinx @ {plugin_path.as_uri()}"] build-backend = "hatchling.build" [project] name = "my-test-app" version = "0.1.1" [tool.hatch] verbose = true # # [tool.hatch.build.targets.wheel] # artifacts = [ # "docs/output", # ] [tool.hatch.build.targets.wheel.force-include] "docs/output" = "my-test-app/docs" """, encoding="utf-8", ) package_dir = project_dir / "src" / "my_test_app" package_dir.mkdir(parents=True) package_root = package_dir / "__init__.py" package_root.write_text("") return project class FixtureProject: """Python project ready for building as a test fixture""" def __init__(self, path: Path) -> None: """Create project in the specified path""" self.path = path @property def pyproject(self) -> Path: """Path to the pyproject.toml file for this project""" return self.path / "pyproject.toml" def add_tool_config(self, toolconf: str) -> None: """merge config snippet with generic project""" with open(self.pyproject, "ta", encoding="UTF-8") as fh: fh.write("\n") fh.write(toolconf) def build(self) -> None: """Attempt to build the project""" subprocess.run( # [sys.executable, "-m", "hatchling", "build"], [ sys.executable, "-m", "build", "--no-isolation", "--skip-dependency-check", "--wheel", "--config-setting", "display_debug=true", ], check=True, cwd=self.path, ) @contextmanager def wheel(self) -> Iterator[zipfile.ZipFile]: """Find the build output (wheel)""" files = list((self.path / "dist").glob("*.whl")) assert len(files) == 1 with zipfile.ZipFile(str(files[0])) as whl: yield whl hatch-sphinx-0.0.4/tests/test_plugin.py000066400000000000000000000143451503441662500201770ustar00rootroot00000000000000"""test hook function and configuration""" from __future__ import annotations from pathlib import Path import subprocess import sys import pytest from .conftest import FixtureProject def test_tool_build(new_project: FixtureProject, tmp_path: Path) -> None: """test sphinx 'build' tool""" new_project.add_tool_config( """\ [[tool.hatch.build.targets.wheel.hooks.sphinx.tools]] tool = "build" source = "source" out_dir = "output/html" format = "html" """ ) (new_project.path / "docs" / "source").mkdir(exist_ok=True, parents=True) (new_project.path / "docs" / "source" / "conf.py").write_text( """\ project = 'Test Project' extensions = [ 'sphinx.ext.autodoc', ] html_theme = 'default' html_static_path = ['_static'] """ ) (new_project.path / "docs" / "source" / "index.rst").write_text( """\ Test Documentation ===================== Contents -------- **Test Project** is a Fake Python library. .. toctree:: :maxdepth: 1 Indices and Search ------------------ * :ref:`genindex` * :ref:`search` """ ) new_project.build() # Check that the file got created in the source tree assert (new_project.path / "docs" / "output" / "html" / "objects.inv").exists() assert (new_project.path / "docs" / "output" / "html" / "index.html").exists() extract_dir = tmp_path / "extract" extract_dir.mkdir() with new_project.wheel() as whl: whl.extractall(extract_dir) # Check that the file made it into the wheel assert (extract_dir / "my-test-app" / "docs" / "html" / "index.html").exists() def test_tool_apidoc(new_project: FixtureProject, tmp_path: Path) -> None: """test sphinx 'apiddoc' tool""" new_project.add_tool_config( """\ [[tool.hatch.build.targets.wheel.hooks.sphinx.tools]] tool = "apidoc" out_dir = "output/api" """ ) (new_project.path / "docs").mkdir(exist_ok=True) new_project.build() # Check that the file got created in the source tree assert (new_project.path / "docs" / "output" / "api" / "modules.rst").exists() extract_dir = tmp_path / "extract" extract_dir.mkdir() with new_project.wheel() as whl: whl.extractall(extract_dir) # Check that the file made it into the wheel assert (extract_dir / "my-test-app" / "docs" / "api" / "modules.rst").exists() def test_tool_custom_strings(new_project: FixtureProject, tmp_path: Path) -> None: """test 'custom' commands tool""" new_project.add_tool_config( """\ [[tool.hatch.build.targets.wheel.hooks.sphinx.tools]] tool = "custom" out_dir = "output" shell = true expand_globs = false commands = [ "touch output/foo.html", "touch output/a1.html output/a2.html", "rm output/a*.html", ] """ ) (new_project.path / "docs").mkdir(exist_ok=True) new_project.build() # Check that the files got created in the source tree assert (new_project.path / "docs" / "output" / "foo.html").exists() # Check handling of multi-arg commands with spaces (shell=True) assert not (new_project.path / "docs" / "output" / "a1.html").exists() extract_dir = tmp_path / "extract" extract_dir.mkdir() with new_project.wheel() as whl: whl.extractall(extract_dir) # Check that the file made it into the wheel assert (extract_dir / "my-test-app" / "docs" / "foo.html").exists() def test_tool_custom_lists_globs(new_project: FixtureProject, tmp_path: Path) -> None: """test 'custom' commands tool""" new_project.add_tool_config( """\ [[tool.hatch.build.targets.wheel.hooks.sphinx.tools]] tool = "custom" out_dir = "output" shell = false expand_globs = true commands = [ "touch output/foo.html output/bar.html", "rm output/f*.html", ["touch", "output/3 4.html", "output/c1.html", "output/c2.html"], ["rm", "output/c*.html"], ] """ ) (new_project.path / "docs").mkdir(exist_ok=True) new_project.build() # Check for shell=False but expand_globs=True where plugin does globbing assert not (new_project.path / "docs" / "output" / "foo.html").exists() assert (new_project.path / "docs" / "output" / "bar.html").exists() assert (new_project.path / "docs" / "output" / "3 4.html").exists() assert not (new_project.path / "docs" / "output" / "c1.html").exists() extract_dir = tmp_path / "extract" extract_dir.mkdir() with new_project.wheel() as whl: whl.extractall(extract_dir) # Check that the file made it into the wheel assert (extract_dir / "my-test-app" / "docs" / "bar.html").exists() @pytest.mark.skipif(sys.platform.startswith("win"), reason="Windows does not support filenames with glob characters") def test_tool_custom_lists_noglobs(new_project: FixtureProject, tmp_path: Path) -> None: """test 'custom' commands tool""" new_project.add_tool_config( """\ [[tool.hatch.build.targets.wheel.hooks.sphinx.tools]] tool = "custom" out_dir = "output" shell = false expand_globs = false commands = [ ["touch", "output/1*.html"], ["touch", "output/2*.html"], ["touch", "output/a b.html"], ] """ ) (new_project.path / "docs").mkdir(exist_ok=True) new_project.build() # test for shell=False where wildcards will not be expanded assert (new_project.path / "docs" / "output" / "1*.html").exists() assert (new_project.path / "docs" / "output" / "a b.html").exists() extract_dir = tmp_path / "extract" extract_dir.mkdir() with new_project.wheel() as whl: whl.extractall(extract_dir) # Check that the file made it into the wheel assert (extract_dir / "my-test-app" / "docs" / "1*.html").exists() assert (extract_dir / "my-test-app" / "docs" / "a b.html").exists() def test_tool_custom_config_error(new_project: FixtureProject) -> None: """test 'custom' commands tool config errors""" new_project.add_tool_config( """\ [[tool.hatch.build.targets.wheel.hooks.sphinx.tools]] tool = "custom" out_dir = "output" shell = true expand_globs = false commands = [ ["touch", "output/3\\\\ 4.html", "output/b1.html", "output/b2.html"], ] """ ) (new_project.path / "docs").mkdir(exist_ok=True) # Check that the invalid configuration errors out with pytest.raises(subprocess.CalledProcessError): new_project.build()