From 327cc5b0f28f7236eaaf1c028674b6e0006611da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timoth=C3=A9e=20Mazzucotelli?= Date: Thu, 29 Feb 2024 19:12:26 +0100 Subject: [PATCH] refactor: Expose Git utilities, move `load_git` into the `loader` module --- CHANGELOG.md | 6 +- src/griffe/__init__.py | 3 +- src/griffe/cli.py | 8 +- src/griffe/git.py | 167 +++++++++++++++-------------------------- src/griffe/loader.py | 84 ++++++++++++++++++++- tests/test_git.py | 2 +- 6 files changed, 150 insertions(+), 120 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db88c0ef..fd6bb3ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -714,9 +714,9 @@ for the initial code allowing to compare two Griffe trees. ### Breaking changes -- All parameters of the [`load_git`][griffe.git.load_git] function, except `module`, are now keyword-only. -- Parameter `try_relative_path` of the [`load_git`][griffe.git.load_git] function was removed. -- Parameter `commit` was renamed `ref` in the [`load_git`][griffe.git.load_git] function. +- All parameters of the [`load_git`][griffe.loader.load_git] function, except `module`, are now keyword-only. +- Parameter `try_relative_path` of the [`load_git`][griffe.loader.load_git] function was removed. +- Parameter `commit` was renamed `ref` in the [`load_git`][griffe.loader.load_git] function. - Parameter `commit` was renamed `ref` in the `tmp_worktree` helper, which will probably become private later. - Parameters `ref` and `repo` switched positions in the `tmp_worktree` helper. - All parameters of the [`resolve_aliases`][griffe.loader.GriffeLoader.resolve_aliases] method are now keyword-only. diff --git a/src/griffe/__init__.py b/src/griffe/__init__.py index fe743c68..437e7b36 100644 --- a/src/griffe/__init__.py +++ b/src/griffe/__init__.py @@ -15,9 +15,8 @@ from griffe.docstrings.sphinx import parse as parse_sphinx from griffe.enumerations import Parser from griffe.extensions.base import Extension, load_extensions -from griffe.git import load_git from griffe.importer import dynamic_import -from griffe.loader import load +from griffe.loader import load, load_git from griffe.logger import get_logger __all__: list[str] = [ diff --git a/src/griffe/cli.py b/src/griffe/cli.py index 607c2aa3..fb3f1448 100644 --- a/src/griffe/cli.py +++ b/src/griffe/cli.py @@ -30,8 +30,8 @@ from griffe.enumerations import ExplanationStyle, Parser from griffe.exceptions import ExtensionError, GitError from griffe.extensions.base import load_extensions -from griffe.git import _get_latest_tag, _get_repo_root, load_git -from griffe.loader import GriffeLoader, load +from griffe.git import get_latest_tag, get_repo_root +from griffe.loader import GriffeLoader, load, load_git from griffe.logger import get_logger from griffe.stats import _format_stats @@ -430,12 +430,12 @@ def check( search_paths.extend(sys.path) try: - against = against or _get_latest_tag(package) + against = against or get_latest_tag(package) except GitError as error: print(f"griffe: error: {error}", file=sys.stderr) return 2 against_path = against_path or package - repository = _get_repo_root(against_path) + repository = get_repo_root(against_path) try: loaded_extensions = load_extensions(extensions or ()) diff --git a/src/griffe/git.py b/src/griffe/git.py index ca5a683d..d431fa61 100644 --- a/src/griffe/git.py +++ b/src/griffe/git.py @@ -1,59 +1,75 @@ -"""This module contains the code allowing to load modules from specific git commits. - -```python -from griffe.git import load_git - -# where `repo` is the folder *containing* `.git` -old_api = load_git("my_module", commit="v0.1.0", repo="path/to/repo") -``` -""" +"""This module contains Git utilities.""" from __future__ import annotations import os import shutil import subprocess +import warnings from contextlib import contextmanager from pathlib import Path from tempfile import TemporaryDirectory -from typing import TYPE_CHECKING, Any, Iterator, Sequence +from typing import Any, Iterator -from griffe import loader from griffe.exceptions import GitError -if TYPE_CHECKING: - from griffe.collections import LinesCollection, ModulesCollection - from griffe.dataclasses import Object - from griffe.enumerations import Parser - from griffe.extensions.base import Extensions +WORKTREE_PREFIX = "griffe-worktree-" -WORKTREE_PREFIX = "griffe-worktree-" +# TODO: Remove at some point. +def __getattr__(name: str) -> Any: + if name == "load_git": + warnings.warn( + f"Importing {name} from griffe.git is deprecated. Import it from griffe.loader instead.", + DeprecationWarning, + stacklevel=2, + ) + + from griffe.loader import load_git + + return load_git + raise AttributeError + +def assert_git_repo(path: str | Path) -> None: + """Assert that a directory is a Git repository. -def _assert_git_repo(repo: str | Path) -> None: + Parameters: + path: Path to a directory. + + Raises: + OSError: When the directory is not a Git repository. + """ if not shutil.which("git"): raise RuntimeError("Could not find git executable. Please install git.") try: subprocess.run( - ["git", "-C", str(repo), "rev-parse", "--is-inside-work-tree"], + ["git", "-C", str(path), "rev-parse", "--is-inside-work-tree"], check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, ) except subprocess.CalledProcessError as err: - raise OSError(f"Not a git repository: {repo}") from err + raise OSError(f"Not a git repository: {path}") from err -def _get_latest_tag(path: str | Path) -> str: - if isinstance(path, str): - path = Path(path) - if not path.is_dir(): - path = path.parent +def get_latest_tag(repo: str | Path) -> str: + """Get latest tag of a Git repository. + + Parameters: + repo: The path to Git repository. + + Returns: + The latest tag. + """ + if isinstance(repo, str): + repo = Path(repo) + if not repo.is_dir(): + repo = repo.parent process = subprocess.run( ["git", "tag", "-l", "--sort=-committerdate"], - cwd=path, + cwd=repo, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, @@ -61,24 +77,32 @@ def _get_latest_tag(path: str | Path) -> str: ) output = process.stdout.strip() if process.returncode != 0 or not output: - raise GitError(f"Cannot list Git tags in {path}: {output or 'no tags'}") + raise GitError(f"Cannot list Git tags in {repo}: {output or 'no tags'}") return output.split("\n", 1)[0] -def _get_repo_root(path: str | Path) -> str: - if isinstance(path, str): - path = Path(path) - if not path.is_dir(): - path = path.parent +def get_repo_root(repo: str | Path) -> str: + """Get the root of a Git repository. + + Parameters: + repo: The path to a Git repository. + + Returns: + The root of the repository. + """ + if isinstance(repo, str): + repo = Path(repo) + if not repo.is_dir(): + repo = repo.parent output = subprocess.check_output( ["git", "rev-parse", "--show-toplevel"], - cwd=path, + cwd=repo, ) return output.decode().strip() @contextmanager -def _tmp_worktree(repo: str | Path = ".", ref: str = "HEAD") -> Iterator[Path]: +def tmp_worktree(repo: str | Path = ".", ref: str = "HEAD") -> Iterator[Path]: """Context manager that checks out the given reference in the given repository to a temporary worktree. Parameters: @@ -92,7 +116,7 @@ def _tmp_worktree(repo: str | Path = ".", ref: str = "HEAD") -> Iterator[Path]: OSError: If `repo` is not a valid `.git` repository RuntimeError: If the `git` executable is unavailable, or if it cannot create a worktree """ - _assert_git_repo(repo) + assert_git_repo(repo) repo_name = Path(repo).resolve().name with TemporaryDirectory(prefix=f"{WORKTREE_PREFIX}{repo_name}-{ref}-") as tmp_dir: branch = f"griffe_{ref}" @@ -113,75 +137,4 @@ def _tmp_worktree(repo: str | Path = ".", ref: str = "HEAD") -> Iterator[Path]: subprocess.run(["git", "-C", repo, "branch", "-D", branch], stdout=subprocess.DEVNULL, check=False) -def load_git( - objspec: str | Path | None = None, - /, - *, - ref: str = "HEAD", - repo: str | Path = ".", - submodules: bool = True, - extensions: Extensions | None = None, - search_paths: Sequence[str | Path] | None = None, - docstring_parser: Parser | None = None, - docstring_options: dict[str, Any] | None = None, - lines_collection: LinesCollection | None = None, - modules_collection: ModulesCollection | None = None, - allow_inspection: bool = True, - find_stubs_package: bool = False, - # TODO: Remove at some point. - module: str | Path | None = None, -) -> Object: - """Load and return a module from a specific Git reference. - - This function will create a temporary - [git worktree](https://git-scm.com/docs/git-worktree) at the requested reference - before loading `module` with [`griffe.load`][griffe.loader.load]. - - This function requires that the `git` executable is installed. - - Parameters: - objspec: The Python path of an object, or file path to a module. - ref: A Git reference such as a commit, tag or branch. - repo: Path to the repository (i.e. the directory *containing* the `.git` directory) - submodules: Whether to recurse on the submodules. - This parameter only makes sense when loading a package (top-level module). - extensions: The extensions to use. - search_paths: The paths to search into (relative to the repository root). - docstring_parser: The docstring parser to use. By default, no parsing is done. - docstring_options: Additional docstring parsing options. - lines_collection: A collection of source code lines. - modules_collection: A collection of modules. - allow_inspection: Whether to allow inspecting modules when visiting them is not possible. - find_stubs_package: Whether to search for stubs-only package. - If both the package and its stubs are found, they'll be merged together. - If only the stubs are found, they'll be used as the package itself. - module: Deprecated. Use `objspec` positional-only parameter instead. - - Returns: - A Griffe object. - """ - with _tmp_worktree(repo, ref) as worktree: - search_paths = [worktree / path for path in search_paths or ["."]] - if isinstance(objspec, Path): - objspec = worktree / objspec - # TODO: Remove at some point. - if isinstance(module, Path): - module = worktree / module - return loader.load( - objspec, - submodules=submodules, - try_relative_path=False, - extensions=extensions, - search_paths=search_paths, - docstring_parser=docstring_parser, - docstring_options=docstring_options, - lines_collection=lines_collection, - modules_collection=modules_collection, - allow_inspection=allow_inspection, - find_stubs_package=find_stubs_package, - # TODO: Remove at some point. - module=module, - ) - - -__all__ = ["load_git"] +__all__ = ["assert_git_repo", "get_latest_tag", "get_repo_root", "tmp_worktree"] diff --git a/src/griffe/loader.py b/src/griffe/loader.py index dbf41c79..34bdec7e 100644 --- a/src/griffe/loader.py +++ b/src/griffe/loader.py @@ -16,6 +16,7 @@ import warnings from contextlib import suppress from datetime import datetime, timezone +from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar, Sequence, cast from griffe.agents.inspector import inspect @@ -27,13 +28,12 @@ from griffe.expressions import ExprName from griffe.extensions.base import Extensions from griffe.finder import ModuleFinder, NamespacePackage, Package +from griffe.git import tmp_worktree from griffe.logger import get_logger from griffe.merger import merge_stubs from griffe.stats import stats if TYPE_CHECKING: - from pathlib import Path - from griffe.enumerations import Parser logger = get_logger(__name__) @@ -759,4 +759,82 @@ def load( ) -__all__ = ["GriffeLoader", "load"] +def load_git( + objspec: str | Path | None = None, + /, + *, + ref: str = "HEAD", + repo: str | Path = ".", + submodules: bool = True, + extensions: Extensions | None = None, + search_paths: Sequence[str | Path] | None = None, + docstring_parser: Parser | None = None, + docstring_options: dict[str, Any] | None = None, + lines_collection: LinesCollection | None = None, + modules_collection: ModulesCollection | None = None, + allow_inspection: bool = True, + find_stubs_package: bool = False, + # TODO: Remove at some point. + module: str | Path | None = None, +) -> Object: + """Load and return a module from a specific Git reference. + + This function will create a temporary + [git worktree](https://git-scm.com/docs/git-worktree) at the requested reference + before loading `module` with [`griffe.load`][griffe.loader.load]. + + This function requires that the `git` executable is installed. + + Examples: + ```python + from griffe.loader import load_git + + old_api = load_git("my_module", ref="v0.1.0", repo="path/to/repo") + ``` + + Parameters: + objspec: The Python path of an object, or file path to a module. + ref: A Git reference such as a commit, tag or branch. + repo: Path to the repository (i.e. the directory *containing* the `.git` directory) + submodules: Whether to recurse on the submodules. + This parameter only makes sense when loading a package (top-level module). + extensions: The extensions to use. + search_paths: The paths to search into (relative to the repository root). + docstring_parser: The docstring parser to use. By default, no parsing is done. + docstring_options: Additional docstring parsing options. + lines_collection: A collection of source code lines. + modules_collection: A collection of modules. + allow_inspection: Whether to allow inspecting modules when visiting them is not possible. + find_stubs_package: Whether to search for stubs-only package. + If both the package and its stubs are found, they'll be merged together. + If only the stubs are found, they'll be used as the package itself. + module: Deprecated. Use `objspec` positional-only parameter instead. + + Returns: + A Griffe object. + """ + with tmp_worktree(repo, ref) as worktree: + search_paths = [worktree / path for path in search_paths or ["."]] + if isinstance(objspec, Path): + objspec = worktree / objspec + # TODO: Remove at some point. + if isinstance(module, Path): + module = worktree / module + return load( + objspec, + submodules=submodules, + try_relative_path=False, + extensions=extensions, + search_paths=search_paths, + docstring_parser=docstring_parser, + docstring_options=docstring_options, + lines_collection=lines_collection, + modules_collection=modules_collection, + allow_inspection=allow_inspection, + find_stubs_package=find_stubs_package, + # TODO: Remove at some point. + module=module, + ) + + +__all__ = ["GriffeLoader", "load", "load_git"] diff --git a/tests/test_git.py b/tests/test_git.py index af0bede5..e1b4daa8 100644 --- a/tests/test_git.py +++ b/tests/test_git.py @@ -10,7 +10,7 @@ from griffe.cli import check from griffe.dataclasses import Module -from griffe.git import load_git +from griffe.loader import load_git from tests import FIXTURES_DIR if TYPE_CHECKING: