Skip to content

Commit

Permalink
refactor: Expose Git utilities, move load_git into the loader module
Browse files Browse the repository at this point in the history
  • Loading branch information
pawamoy committed Feb 29, 2024
1 parent 8f48193 commit 327cc5b
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 120 deletions.
6 changes: 3 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 1 addition & 2 deletions src/griffe/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = [
Expand Down
8 changes: 4 additions & 4 deletions src/griffe/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 ())
Expand Down
167 changes: 60 additions & 107 deletions src/griffe/git.py
Original file line number Diff line number Diff line change
@@ -1,84 +1,108 @@
"""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,
check=False,
)
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:
Expand All @@ -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}"
Expand All @@ -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"]
84 changes: 81 additions & 3 deletions src/griffe/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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__)
Expand Down Expand Up @@ -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"]
2 changes: 1 addition & 1 deletion tests/test_git.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit 327cc5b

Please sign in to comment.