From a05790e40db0816397112885fcf3c0ebf98bd1e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Gr=C3=BCter?= Date: Fri, 24 Oct 2025 01:19:00 +0200 Subject: [PATCH 01/10] Track generated inplace stubs for docstub itself I thought about not doing this but then it becomes more complicated to include the stubs into the package in the release workflow (cd.yml). I want to keep the release workflow as minimal as possible to reduce the risk of a supply chain attack. This approach sidesteps that problem, because the stubs are already there and tracked. However, we need to assert that the tracked stubs are identical with the docstub generated ones. That is a bit tricky, because `git diff` actually ignores untracked files! So I came up with `assert-unchanged.sh`. I really struggled with this becaue I initally tried to use git ls-files with the "-z" option which returns null-delimited paths. I couldn't figure out how to pass them to `git add` and `git unstage` later on. I'm worried that this approach is a bit brittle (due to `assert-unchanged.sh`) and increases the diff in PRs. On the other hand being able to review the stubs is probably not a bad thing. We'll see how that goes. --- .github/scripts/assert-unchanged.sh | 34 ++++++++ .github/workflows/ci.yml | 6 +- pyproject.toml | 1 - src/docstub/__init__.pyi | 5 ++ src/docstub/__main__.pyi | 4 + src/docstub/_analysis.pyi | 78 ++++++++++++++++++ src/docstub/_cache.pyi | 48 +++++++++++ src/docstub/_cli.pyi | 42 ++++++++++ src/docstub/_config.pyi | 30 +++++++ src/docstub/_docstrings.pyi | 115 ++++++++++++++++++++++++++ src/docstub/_path_utils.pyi | 33 ++++++++ src/docstub/_report.pyi | 53 ++++++++++++ src/docstub/_stubs.pyi | 120 ++++++++++++++++++++++++++++ src/docstub/_utils.pyi | 13 +++ src/docstub/_vendored/__init__.pyi | 1 + src/docstub/_vendored/stdlib.pyi | 12 +++ src/docstub/py.typed | 0 17 files changed, 591 insertions(+), 4 deletions(-) create mode 100644 .github/scripts/assert-unchanged.sh create mode 100644 src/docstub/__init__.pyi create mode 100644 src/docstub/__main__.pyi create mode 100644 src/docstub/_analysis.pyi create mode 100644 src/docstub/_cache.pyi create mode 100644 src/docstub/_cli.pyi create mode 100644 src/docstub/_config.pyi create mode 100644 src/docstub/_docstrings.pyi create mode 100644 src/docstub/_path_utils.pyi create mode 100644 src/docstub/_report.pyi create mode 100644 src/docstub/_stubs.pyi create mode 100644 src/docstub/_utils.pyi create mode 100644 src/docstub/_vendored/__init__.pyi create mode 100644 src/docstub/_vendored/stdlib.pyi create mode 100644 src/docstub/py.typed diff --git a/.github/scripts/assert-unchanged.sh b/.github/scripts/assert-unchanged.sh new file mode 100644 index 0000000..9cd6410 --- /dev/null +++ b/.github/scripts/assert-unchanged.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash + +# Assert that there are no changes in a given directory compared to HEAD. +# Expects a relative directory as the one and only argument. + +set -e + +CHECK_DIR=$1 + +# Add untracked files so the following command picks them up +UNTRACKED=$(git ls-files --others --exclude-standard "$CHECK_DIR") + +if [ -n "$UNTRACKED" ]; then + git add "$UNTRACKED" +fi + +set +e +# Display changes in tracked files and capture non-zero exit code if so +git diff --exit-code HEAD "$CHECK_DIR" +GIT_DIFF_EXIT_CODE=$? +set -e + +# Unstage again (useful for local debugging) +if [ -n "$UNTRACKED" ]; then + git restore --staged "$UNTRACKED" +fi + +# Display changes in tracked files and capture exit status +if [ $GIT_DIFF_EXIT_CODE -ne 0 ]; then + echo "::error::Uncommited changes in directory: $CHECK_DIR" + exit $GIT_DIFF_EXIT_CODE +fi + +set +e \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ee2f57..1bb02e1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,6 @@ env: # Many color libraries just need this to be set to any value, but at least # one distinguishes color depth, where "3" -> "256-bit color". FORCE_COLOR: 3 - MYPYPATH: ${{ github.workspace }}/stubs defaults: run: @@ -84,11 +83,12 @@ jobs: --config=examples/docstub.toml \ --out-dir=examples/example_pkg-stubs \ examples/example_pkg - git diff --exit-code examples/ && echo "Stubs for example_pkg did not change" + .github/scripts/assert-unchanged.sh examples/ - name: Generate stubs for docstub run: | - python -m docstub run -v src/docstub -o ${MYPYPATH}/docstub + python -m docstub run -v src/docstub + .github/scripts/assert-unchanged.sh src/docstub - name: Check with mypy.stubtest run: | diff --git a/pyproject.toml b/pyproject.toml index 953ec88..34bb588 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -163,5 +163,4 @@ ignore_missing_imports = true [tool.basedpyright] -stubPath = "stubs/" typeCheckingMode = "standard" diff --git a/src/docstub/__init__.pyi b/src/docstub/__init__.pyi new file mode 100644 index 0000000..0f6f1df --- /dev/null +++ b/src/docstub/__init__.pyi @@ -0,0 +1,5 @@ +# File generated with docstub + +from ._version import __version__ + +__all__ = ["__version__"] diff --git a/src/docstub/__main__.pyi b/src/docstub/__main__.pyi new file mode 100644 index 0000000..ced2abe --- /dev/null +++ b/src/docstub/__main__.pyi @@ -0,0 +1,4 @@ +# File generated with docstub + +if __name__ == "__main__": + pass diff --git a/src/docstub/_analysis.pyi b/src/docstub/_analysis.pyi new file mode 100644 index 0000000..4e34524 --- /dev/null +++ b/src/docstub/_analysis.pyi @@ -0,0 +1,78 @@ +# File generated with docstub + +import logging +from collections.abc import Iterable +from dataclasses import dataclass +from pathlib import Path +from typing import Any, ClassVar + +import libcst as cst + +logger: logging.Logger + +def _shared_leading_qualname(*qualnames: tuple[str]) -> str: ... +@dataclass(slots=True, frozen=True) +class PyImport: + import_: str | None = ... + from_: str | None = ... + as_: str | None = ... + implicit: str | None = ... + + @classmethod + def typeshed_Incomplete(cls) -> PyImport: ... + def format_import(self, relative_to: str | None = ...) -> str: ... + @property + def target(self) -> str: ... + @property + def has_import(self) -> None: ... + def __post_init__(self) -> None: ... + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + +def _is_type(value: Any) -> bool: ... +def _builtin_types() -> dict[str, PyImport]: ... +def _runtime_types_in_module(module_name: str) -> dict[str, PyImport]: ... +def common_known_types() -> dict[str, PyImport]: ... + +class TypeCollector(cst.CSTVisitor): + class ImportSerializer: + suffix: ClassVar[str] + encoding: ClassVar[str] + + def hash_args(self, path: Path) -> str: ... + def serialize( + self, data: tuple[dict[str, PyImport], dict[str, PyImport]] + ) -> bytes: ... + def deserialize( + self, raw: bytes + ) -> tuple[dict[str, PyImport], dict[str, PyImport]]: ... + + @classmethod + def collect(cls, file: Path) -> tuple[dict[str, PyImport], dict[str, PyImport]]: ... + def __init__(self, *, module_name: str) -> None: ... + def visit_ClassDef(self, node: cst.ClassDef) -> bool: ... + def leave_ClassDef(self, original_node: cst.ClassDef) -> None: ... + def visit_FunctionDef(self, node: cst.FunctionDef) -> bool: ... + def visit_TypeAlias(self, node: cst.TypeAlias) -> bool: ... + def visit_AnnAssign(self, node: cst.AnnAssign) -> bool: ... + def visit_ImportFrom(self, node: cst.ImportFrom) -> bool: ... + def visit_Import(self, node: cst.Import) -> bool: ... + def _collect_type_annotation(self, stack: Iterable[str]) -> None: ... + +class TypeMatcher: + types: dict[str, PyImport] + type_prefixes: dict[str, PyImport] + type_nicknames: dict[str, str] + successful_queries: int + unknown_qualnames: list + current_file: Path | None + + def __init__( + self, + *, + types: dict[str, PyImport] | None = ..., + type_prefixes: dict[str, PyImport] | None = ..., + type_nicknames: dict[str, str] | None = ..., + ) -> None: ... + def _resolve_nickname(self, name: str) -> str: ... + def match(self, search: str) -> tuple[str | None, PyImport | None]: ... diff --git a/src/docstub/_cache.pyi b/src/docstub/_cache.pyi new file mode 100644 index 0000000..e2f4496 --- /dev/null +++ b/src/docstub/_cache.pyi @@ -0,0 +1,48 @@ +# File generated with docstub + +import logging +from collections.abc import Callable +from functools import cached_property +from pathlib import Path +from typing import Any, Protocol + +logger: logging.Logger + +CACHE_DIR_NAME: str + +CACHEDIR_TAG_CONTENT: str + +GITHUB_IGNORE_CONTENT: str + +def _directory_size(path: Path) -> int: ... +def create_cache(path: Path) -> None: ... +def validate_cache(path: Path) -> None: ... + +class FuncSerializer[T](Protocol): + suffix: str + + def hash_args(self, *args: Any, **kwargs: Any) -> str: ... + def serialize(self, data: T) -> bytes: ... + def deserialize(self, raw: bytes) -> T: ... + +class FileCache: + func: Callable + serializer: FuncSerializer + sub_dir: str + cache_hits: int + cache_misses: int + cached_last_call: bool | None + + def __init__( + self, + *, + func: Callable, + serializer: FuncSerializer, + cache_dir: Path, + sub_dir: str | None = ..., + ) -> None: ... + @cached_property + def cache_dir(self) -> Path: ... + @property + def cache_sub_dir(self) -> None: ... + def __call__(self, *args: Any, **kwargs: Any) -> Any: ... diff --git a/src/docstub/_cli.pyi b/src/docstub/_cli.pyi new file mode 100644 index 0000000..fa6270a --- /dev/null +++ b/src/docstub/_cli.pyi @@ -0,0 +1,42 @@ +# File generated with docstub + +import logging +from collections.abc import Iterable, Sequence +from pathlib import Path +from typing import Literal + +import click + +from ._analysis import PyImport +from ._config import Config + +logger: logging.Logger + +def _cache_dir_in_cwd() -> Path: ... +def _load_configuration(config_paths: list[Path] | None = ...) -> Config: ... +def _calc_verbosity( + *, verbose: Literal[0, 1, 2], quiet: Literal[0, 1, 2] +) -> Literal[-2, -1, 0, 1, 2]: ... +def _collect_type_info( + root_path: Path, *, ignore: Sequence[str] = ..., cache: bool = ... +) -> tuple[dict[str, PyImport], dict[str, PyImport]]: ... +def _format_unknown_names(unknown_names: Iterable[str]) -> str: ... +def log_execution_time() -> None: ... +@click.group() +def cli() -> None: ... +@cli.command() +def run( + *, + root_path: Path, + out_dir: Path, + config_paths: Sequence[Path], + ignore: Sequence[str], + group_errors: bool, + allow_errors: int, + fail_on_warning: bool, + no_cache: bool, + verbose: int, + quiet: int, +) -> None: ... +@cli.command() +def clean(verbose: int, quiet: int) -> None: ... diff --git a/src/docstub/_config.pyi b/src/docstub/_config.pyi new file mode 100644 index 0000000..21452a9 --- /dev/null +++ b/src/docstub/_config.pyi @@ -0,0 +1,30 @@ +# File generated with docstub + +import dataclasses +import logging +from collections.abc import Mapping +from pathlib import Path +from typing import ClassVar, Self + +logger: logging.Logger + +@dataclasses.dataclass(frozen=True, slots=True, kw_only=True) +class Config: + TEMPLATE_PATH: ClassVar[Path] + NUMPY_PATH: ClassVar[Path] + + types: dict[str, str] = ... + type_prefixes: dict[str, str] = ... + type_nicknames: dict[str, str] = ... + ignore_files: list[str] = ... + + config_paths: tuple[Path, ...] = ... + + @classmethod + def from_toml(cls, path: Path | str) -> Self: ... + def merge(self, other: Self) -> Self: ... + def to_dict(self) -> None: ... + def __post_init__(self) -> None: ... + def __repr__(self) -> str: ... + @staticmethod + def validate(mapping: Mapping) -> None: ... diff --git a/src/docstub/_docstrings.pyi b/src/docstub/_docstrings.pyi new file mode 100644 index 0000000..6fd534c --- /dev/null +++ b/src/docstub/_docstrings.pyi @@ -0,0 +1,115 @@ +# File generated with docstub + +import logging +from collections.abc import Generator, Iterable +from dataclasses import dataclass +from functools import cached_property +from pathlib import Path +from typing import Any, ClassVar + +import lark +import lark.visitors +import numpydoc.docscrape as npds + +from ._analysis import PyImport, TypeMatcher +from ._report import ContextReporter +from ._utils import DocstubError + +logger: logging.Logger + +here: Path +grammar_path: Path + +with grammar_path.open() as file: + _grammar: str + +_lark: lark.Lark + +def _find_one_token(tree: lark.Tree, *, name: str) -> lark.Token: ... +@dataclass(frozen=True, slots=True, kw_only=True) +class Annotation: + value: str + imports: frozenset[PyImport] = ... + + def __post_init__(self) -> None: ... + def __str__(self) -> str: ... + @classmethod + def many_as_tuple(cls, types: Iterable[Annotation]) -> Annotation: ... + @classmethod + def as_generator( + cls, + *, + yield_types: Iterable[Annotation], + receive_types: Iterable[Annotation] = ..., + return_types: Iterable[Annotation] = ..., + ) -> Annotation: ... + def as_union_with_none(self) -> Annotation: ... + @staticmethod + def _aggregate_annotations( + *types: Iterable[Annotation], + ) -> tuple[list[str], set[PyImport]]: ... + +FallbackAnnotation: Annotation + +class QualnameIsKeyword(DocstubError): + pass + +class DoctypeTransformer(lark.visitors.Transformer): + matcher: TypeMatcher + stats: dict[str, Any] + + blacklisted_qualnames: ClassVar[frozenset[str]] + + def __init__( + self, *, matcher: TypeMatcher | None = ..., **kwargs: dict[Any, Any] + ) -> None: ... + def doctype_to_annotation( + self, doctype: str + ) -> tuple[Annotation, list[tuple[str, int, int]]]: ... + def qualname(self, tree: lark.Tree) -> lark.Token: ... + def rst_role(self, tree: lark.Tree) -> lark.Token: ... + def union(self, tree: lark.Tree) -> str: ... + def subscription(self, tree: lark.Tree) -> str: ... + def natlang_literal(self, tree: lark.Tree) -> str: ... + def natlang_container(self, tree: lark.Tree) -> str: ... + def natlang_array(self, tree: lark.Tree) -> str: ... + def array_name(self, tree: lark.Tree) -> lark.Token: ... + def shape(self, tree: lark.Tree) -> lark.visitors._DiscardType: ... + def optional_info(self, tree: lark.Tree) -> lark.visitors._DiscardType: ... + def __default__( + self, data: lark.Token, children: list[lark.Token], meta: lark.tree.Meta + ) -> lark.Token | list[lark.Token]: ... + def _match_import(self, qualname: str, *, meta: lark.tree.Meta) -> str: ... + +def _uncombine_numpydoc_params( + params: list[npds.Parameter], +) -> Generator[npds.Parameter]: ... + +class DocstringAnnotations: + docstring: str + transformer: DoctypeTransformer + reporter: ContextReporter + + def __init__( + self, + docstring: str, + *, + transformer: DoctypeTransformer, + reporter: ContextReporter | None = ..., + ) -> None: ... + def _doctype_to_annotation( + self, doctype: str, ds_line: int = ... + ) -> Annotation: ... + @cached_property + def attributes(self) -> dict[str, Annotation]: ... + @cached_property + def parameters(self) -> dict[str, Annotation]: ... + @cached_property + def returns(self) -> Annotation | None: ... + @cached_property + def _returns(self) -> Annotation | None: ... + @cached_property + def _yields(self) -> Annotation | None: ... + def _handle_missing_whitespace(self, param: npds.Parameter) -> npds.Parameter: ... + def _section_annotations(self, name: str) -> dict[str, Annotation]: ... + def _find_docstring_line(self, *substrings: str) -> int: ... diff --git a/src/docstub/_path_utils.pyi b/src/docstub/_path_utils.pyi new file mode 100644 index 0000000..b6baa70 --- /dev/null +++ b/src/docstub/_path_utils.pyi @@ -0,0 +1,33 @@ +# File generated with docstub + +import logging +import re +import sys +from collections.abc import Generator, Sequence +from pathlib import Path + +if sys.version_info >= (3, 13): + pass +else: + pass + +logger: logging.Logger + +STUB_HEADER_COMMENT: str + +def is_docstub_generated(stub_path: Path) -> bool: ... +def is_python_or_stub_file(path: Path) -> bool: ... +def is_python_package_dir(path: Path) -> bool: ... +def find_package_root(path: Path) -> Path: ... +def glob_patterns_to_regex( + patterns: tuple[str, ...], relative_to: Path | None = ... +) -> re.Pattern | None: ... +def _walk_source_package( + path: Path, *, ignore_regex: re.Pattern +) -> Generator[Path]: ... +def walk_source_package( + path: Path, *, ignore: Sequence[str] = ... +) -> Generator[Path]: ... +def walk_source_and_targets( + root_path: Path, target_dir: Path, *, ignore: Sequence[str] = ... +) -> Generator[tuple[Path, Path]]: ... diff --git a/src/docstub/_report.pyi b/src/docstub/_report.pyi new file mode 100644 index 0000000..cf3c019 --- /dev/null +++ b/src/docstub/_report.pyi @@ -0,0 +1,53 @@ +# File generated with docstub + +import dataclasses +import logging +from pathlib import Path +from typing import Any, ClassVar, Self, TextIO + +logger: logging.Logger + +@dataclasses.dataclass(kw_only=True, slots=True, frozen=True) +class ContextReporter: + logger: logging.Logger + path: Path | None = ... + line: int | None = ... + + def copy_with( + self, + *, + logger: logging.Logger | None = ..., + path: Path | None = ..., + line: int | None = ..., + line_offset: int | None = ..., + ) -> Self: ... + def report( + self, short: str, *, log_level: int, details: str | None = ..., **log_kw: Any + ) -> None: ... + def debug( + self, short: str, *, details: str | None = ..., **log_kw: Any + ) -> None: ... + def info(self, short: str, *, details: str | None = ..., **log_kw: Any) -> None: ... + def warn(self, short: str, *, details: str | None = ..., **log_kw: Any) -> None: ... + def error( + self, short: str, *, details: str | None = ..., **log_kw: Any + ) -> None: ... + def __post_init__(self) -> None: ... + @staticmethod + def underline(line: str, *, char: str = ...) -> str: ... + +class ReportHandler(logging.StreamHandler): + group_errors: bool + error_count: int + warning_count: int + + level_to_color: ClassVar[dict[int, str]] + + def __init__( + self, stream: TextIO | None = ..., group_errors: bool = ... + ) -> None: ... + def format(self, record: logging.LogRecord) -> str: ... + def emit(self, record: logging.LogRecord) -> None: ... + def emit_grouped(self) -> None: ... + +def setup_logging(*, verbosity: int, group_errors: bool) -> ReportHandler: ... diff --git a/src/docstub/_stubs.pyi b/src/docstub/_stubs.pyi new file mode 100644 index 0000000..22ffb1a --- /dev/null +++ b/src/docstub/_stubs.pyi @@ -0,0 +1,120 @@ +# File generated with docstub + +import enum +import logging +from dataclasses import dataclass +from pathlib import Path +from typing import ClassVar, Literal + +import libcst as cst + +from ._analysis import PyImport, TypeMatcher +from ._docstrings import ( + Annotation, + DocstringAnnotations, + DoctypeTransformer, +) +from ._report import ContextReporter + +logger: logging.Logger + +def try_format_stub(stub: str) -> str: ... + +class ScopeType(enum.StrEnum): + MODULE = "module" + CLASS = "class" + FUNC = "func" + METHOD = "method" + CLASSMETHOD = "classmethod" + STATICMETHOD = "staticmethod" + +@dataclass(slots=True, frozen=True) +class _Scope: + type: ScopeType + node: cst.CSTNode | None = ... + + @property + def has_self_or_cls(self) -> bool: ... + @property + def is_method(self) -> bool: ... + @property + def is_class_init(self) -> bool: ... + @property + def is_dataclass(self) -> bool: ... + +def _get_docstring_node( + node: cst.FunctionDef | cst.ClassDef | cst.Module, +) -> cst.SimpleString | cst.ConcatenatedString | None: ... +def _log_error_with_line_context(cls: Py2StubTransformer) -> Py2StubTransformer: ... +def _docstub_comment_directives(cls: Py2StubTransformer) -> Py2StubTransformer: ... +def _inline_node_as_code(node: cst.CSTNode) -> str: ... + +class Py2StubTransformer(cst.CSTTransformer): + transformer: DoctypeTransformer + + METADATA_DEPENDENCIES: ClassVar[tuple] + + _body_replacement: ClassVar[cst.SimpleStatementSuite] + _Annotation_Incomplete: ClassVar[cst.Annotation] + _Annotation_None: ClassVar[cst.Annotation] + + def __init__(self, *, matcher: TypeMatcher | None = ...) -> None: ... + @property + def current_source(self) -> Path: ... + @current_source.setter + def current_source(self, value: Path) -> None: ... + @property + def is_inside_function_def(self) -> bool: ... + def python_to_stub(self, source: str, *, module_path: Path | None = ...) -> str: ... + def visit_ClassDef(self, node: cst.ClassDef) -> Literal[True]: ... + def leave_ClassDef( + self, original_node: cst.ClassDef, updated_node: cst.ClassDef + ) -> cst.ClassDef: ... + def visit_FunctionDef(self, node: cst.FunctionDef) -> Literal[True]: ... + def visit_IndentedBlock(self, node: cst.IndentedBlock) -> bool: ... + def visit_SimpleStatementSuite(self, node: cst.SimpleStatementSuite) -> bool: ... + def leave_FunctionDef( + self, original_node: cst.FunctionDef, updated_node: cst.FunctionDef + ) -> cst.FunctionDef: ... + def leave_Param( + self, original_node: cst.Param, updated_node: cst.Param + ) -> cst.Param: ... + def leave_Expr( + self, original_node: cst.Expr, updated_node: cst.Expr + ) -> cst.RemovalSentinel: ... + def leave_Comment( + self, original_node: cst.Comment, updated_node: cst.Comment + ) -> cst.Comment: ... + def leave_Assign( + self, original_node: cst.Assign, updated_node: cst.Assign + ) -> cst.Assign | cst.FlattenSentinel: ... + def leave_AnnAssign( + self, original_node: cst.AnnAssign, updated_node: cst.AnnAssign + ) -> cst.AnnAssign: ... + def visit_Module(self, node: cst.Module) -> Literal[True]: ... + def leave_Module( + self, original_node: cst.Module, updated_node: cst.Module + ) -> cst.Module: ... + def visit_Lambda(self, node: cst.Lambda) -> Literal[False]: ... + def leave_Decorator( + self, original_node: cst.Decorator, updated_node: cst.Decorator + ) -> cst.Decorator | cst.RemovalSentinel: ... + @staticmethod + def _parse_imports( + imports: set[PyImport], *, current_module: str | None = ... + ) -> tuple[cst.SimpleStatementLine, ...]: ... + def _function_type(self, func_def: cst.FunctionDef) -> ScopeType: ... + def _annotations_from_node( + self, node: cst.FunctionDef | cst.ClassDef | cst.Module + ) -> DocstringAnnotations: ... + def _create_annotated_assign( + self, + *, + name: str, + trailing_semicolon: bool = ..., + reporter: ContextReporter | None = ..., + ) -> cst.AnnAssign: ... + def _insert_instance_attributes( + self, updated_node: cst.ClassDef, attributes: dict[str, Annotation] + ) -> cst.ClassDef: ... + def _reporter_with_ctx(self, node: cst.CSTNode) -> ContextReporter: ... diff --git a/src/docstub/_utils.pyi b/src/docstub/_utils.pyi new file mode 100644 index 0000000..9ae912b --- /dev/null +++ b/src/docstub/_utils.pyi @@ -0,0 +1,13 @@ +# File generated with docstub + +from collections.abc import Callable +from pathlib import Path + +def accumulate_qualname(qualname: str, *, start_right: bool = ...) -> None: ... +def escape_qualname(name: str) -> str: ... +def _resolve_path_before_caching(func: Callable) -> Callable: ... +def module_name_from_path(path: Path) -> str: ... +def pyfile_checksum(path: Path) -> str: ... + +class DocstubError(Exception): + pass diff --git a/src/docstub/_vendored/__init__.pyi b/src/docstub/_vendored/__init__.pyi new file mode 100644 index 0000000..9cf3027 --- /dev/null +++ b/src/docstub/_vendored/__init__.pyi @@ -0,0 +1 @@ +# File generated with docstub diff --git a/src/docstub/_vendored/stdlib.pyi b/src/docstub/_vendored/stdlib.pyi new file mode 100644 index 0000000..2fed41e --- /dev/null +++ b/src/docstub/_vendored/stdlib.pyi @@ -0,0 +1,12 @@ +# File generated with docstub + +from collections.abc import Sequence + +def _fnmatch_translate(pat: str, STAR: str, QUESTION_MARK: str) -> str: ... +def glob_translate( + pat: str, + *, + recursive: bool = ..., + include_hidden: bool = ..., + seps: Sequence[str] | None = ..., +) -> str: ... diff --git a/src/docstub/py.typed b/src/docstub/py.typed new file mode 100644 index 0000000..e69de29 From b09317d08b3e819433fa0c3a6e5f55a4bfd44a14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Gr=C3=BCter?= Date: Fri, 24 Oct 2025 01:24:50 +0200 Subject: [PATCH 02/10] Fix now failing doctest that assumed non-existing stub file --- src/docstub/_path_utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/docstub/_path_utils.py b/src/docstub/_path_utils.py index 4737541..e32ae72 100644 --- a/src/docstub/_path_utils.py +++ b/src/docstub/_path_utils.py @@ -365,8 +365,6 @@ def walk_source_and_targets(root_path, target_dir, *, ignore=()): '.../docstub/__init__.py' >>> stub_path.as_posix() '.../docstub/__init__.pyi' - >>> stub_path.is_file() - False """ if root_path.is_file(): stub_path = target_dir / root_path.with_suffix(".pyi").name From 1adf85a9f7a14ac9a5195a6243a359cd8a1949ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Gr=C3=BCter?= Date: Fri, 24 Oct 2025 01:37:53 +0200 Subject: [PATCH 03/10] Revert "Fix now failing doctest that assumed non-existing stub file" This reverts commit b09317d08b3e819433fa0c3a6e5f55a4bfd44a14. --- src/docstub/_path_utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/docstub/_path_utils.py b/src/docstub/_path_utils.py index e32ae72..4737541 100644 --- a/src/docstub/_path_utils.py +++ b/src/docstub/_path_utils.py @@ -365,6 +365,8 @@ def walk_source_and_targets(root_path, target_dir, *, ignore=()): '.../docstub/__init__.py' >>> stub_path.as_posix() '.../docstub/__init__.pyi' + >>> stub_path.is_file() + False """ if root_path.is_file(): stub_path = target_dir / root_path.with_suffix(".pyi").name From c5872de2b88a0a3deb08a6d1c7f2e14b885831e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Gr=C3=BCter?= Date: Fri, 24 Oct 2025 01:40:24 +0200 Subject: [PATCH 04/10] Move stubs into src/docstub-stubs Apparently this works fine with mypy and basedpyright! --- src/{docstub => docstub-stubs}/__init__.pyi | 0 src/{docstub => docstub-stubs}/__main__.pyi | 0 src/{docstub => docstub-stubs}/_analysis.pyi | 0 src/{docstub => docstub-stubs}/_cache.pyi | 0 src/{docstub => docstub-stubs}/_cli.pyi | 0 src/{docstub => docstub-stubs}/_config.pyi | 0 src/{docstub => docstub-stubs}/_docstrings.pyi | 0 src/{docstub => docstub-stubs}/_path_utils.pyi | 0 src/{docstub => docstub-stubs}/_report.pyi | 0 src/{docstub => docstub-stubs}/_stubs.pyi | 0 src/{docstub => docstub-stubs}/_utils.pyi | 0 src/{docstub => docstub-stubs}/_vendored/__init__.pyi | 0 src/{docstub => docstub-stubs}/_vendored/stdlib.pyi | 0 src/docstub-stubs/_version.pyi | 8 ++++++++ src/{docstub => docstub-stubs}/py.typed | 0 15 files changed, 8 insertions(+) rename src/{docstub => docstub-stubs}/__init__.pyi (100%) rename src/{docstub => docstub-stubs}/__main__.pyi (100%) rename src/{docstub => docstub-stubs}/_analysis.pyi (100%) rename src/{docstub => docstub-stubs}/_cache.pyi (100%) rename src/{docstub => docstub-stubs}/_cli.pyi (100%) rename src/{docstub => docstub-stubs}/_config.pyi (100%) rename src/{docstub => docstub-stubs}/_docstrings.pyi (100%) rename src/{docstub => docstub-stubs}/_path_utils.pyi (100%) rename src/{docstub => docstub-stubs}/_report.pyi (100%) rename src/{docstub => docstub-stubs}/_stubs.pyi (100%) rename src/{docstub => docstub-stubs}/_utils.pyi (100%) rename src/{docstub => docstub-stubs}/_vendored/__init__.pyi (100%) rename src/{docstub => docstub-stubs}/_vendored/stdlib.pyi (100%) create mode 100644 src/docstub-stubs/_version.pyi rename src/{docstub => docstub-stubs}/py.typed (100%) diff --git a/src/docstub/__init__.pyi b/src/docstub-stubs/__init__.pyi similarity index 100% rename from src/docstub/__init__.pyi rename to src/docstub-stubs/__init__.pyi diff --git a/src/docstub/__main__.pyi b/src/docstub-stubs/__main__.pyi similarity index 100% rename from src/docstub/__main__.pyi rename to src/docstub-stubs/__main__.pyi diff --git a/src/docstub/_analysis.pyi b/src/docstub-stubs/_analysis.pyi similarity index 100% rename from src/docstub/_analysis.pyi rename to src/docstub-stubs/_analysis.pyi diff --git a/src/docstub/_cache.pyi b/src/docstub-stubs/_cache.pyi similarity index 100% rename from src/docstub/_cache.pyi rename to src/docstub-stubs/_cache.pyi diff --git a/src/docstub/_cli.pyi b/src/docstub-stubs/_cli.pyi similarity index 100% rename from src/docstub/_cli.pyi rename to src/docstub-stubs/_cli.pyi diff --git a/src/docstub/_config.pyi b/src/docstub-stubs/_config.pyi similarity index 100% rename from src/docstub/_config.pyi rename to src/docstub-stubs/_config.pyi diff --git a/src/docstub/_docstrings.pyi b/src/docstub-stubs/_docstrings.pyi similarity index 100% rename from src/docstub/_docstrings.pyi rename to src/docstub-stubs/_docstrings.pyi diff --git a/src/docstub/_path_utils.pyi b/src/docstub-stubs/_path_utils.pyi similarity index 100% rename from src/docstub/_path_utils.pyi rename to src/docstub-stubs/_path_utils.pyi diff --git a/src/docstub/_report.pyi b/src/docstub-stubs/_report.pyi similarity index 100% rename from src/docstub/_report.pyi rename to src/docstub-stubs/_report.pyi diff --git a/src/docstub/_stubs.pyi b/src/docstub-stubs/_stubs.pyi similarity index 100% rename from src/docstub/_stubs.pyi rename to src/docstub-stubs/_stubs.pyi diff --git a/src/docstub/_utils.pyi b/src/docstub-stubs/_utils.pyi similarity index 100% rename from src/docstub/_utils.pyi rename to src/docstub-stubs/_utils.pyi diff --git a/src/docstub/_vendored/__init__.pyi b/src/docstub-stubs/_vendored/__init__.pyi similarity index 100% rename from src/docstub/_vendored/__init__.pyi rename to src/docstub-stubs/_vendored/__init__.pyi diff --git a/src/docstub/_vendored/stdlib.pyi b/src/docstub-stubs/_vendored/stdlib.pyi similarity index 100% rename from src/docstub/_vendored/stdlib.pyi rename to src/docstub-stubs/_vendored/stdlib.pyi diff --git a/src/docstub-stubs/_version.pyi b/src/docstub-stubs/_version.pyi new file mode 100644 index 0000000..d02022d --- /dev/null +++ b/src/docstub-stubs/_version.pyi @@ -0,0 +1,8 @@ +__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"] + +VERSION_TUPLE = tuple[int | str, ...] + +version: str +__version__: str +__version_tuple__: VERSION_TUPLE +version_tuple: VERSION_TUPLE diff --git a/src/docstub/py.typed b/src/docstub-stubs/py.typed similarity index 100% rename from src/docstub/py.typed rename to src/docstub-stubs/py.typed From c1e52ea2a8810bd2767d985bbbe8e89f6593feea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Gr=C3=BCter?= Date: Fri, 24 Oct 2025 02:06:11 +0200 Subject: [PATCH 05/10] Refactor assert-unchanged.sh to not use `git add` I think that triggered a permission denied error in the GitHub CI. This solution feels a lot cleaner too! --- .github/scripts/assert-unchanged.sh | 21 +++++++-------------- .github/workflows/ci.yml | 8 +++++--- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/.github/scripts/assert-unchanged.sh b/.github/scripts/assert-unchanged.sh index 9cd6410..477020e 100644 --- a/.github/scripts/assert-unchanged.sh +++ b/.github/scripts/assert-unchanged.sh @@ -7,28 +7,21 @@ set -e CHECK_DIR=$1 -# Add untracked files so the following command picks them up +# Find untracked files UNTRACKED=$(git ls-files --others --exclude-standard "$CHECK_DIR") +# and display their content by comparing with '/dev/null' +echo "$UNTRACKED" | xargs -I _ git --no-pager diff --no-index /dev/null _ -if [ -n "$UNTRACKED" ]; then - git add "$UNTRACKED" -fi - -set +e # Display changes in tracked files and capture non-zero exit code if so +set +e git diff --exit-code HEAD "$CHECK_DIR" -GIT_DIFF_EXIT_CODE=$? +GIT_DIFF_HEAD_EXIT_CODE=$? set -e -# Unstage again (useful for local debugging) -if [ -n "$UNTRACKED" ]; then - git restore --staged "$UNTRACKED" -fi - # Display changes in tracked files and capture exit status -if [ $GIT_DIFF_EXIT_CODE -ne 0 ]; then +if [ $GIT_DIFF_HEAD_EXIT_CODE -ne 0 ] || [ -n "$UNTRACKED" ]; then echo "::error::Uncommited changes in directory: $CHECK_DIR" - exit $GIT_DIFF_EXIT_CODE + exit 1 fi set +e \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1bb02e1..d270ff3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -85,10 +85,12 @@ jobs: examples/example_pkg .github/scripts/assert-unchanged.sh examples/ - - name: Generate stubs for docstub + - name: Check docstub-stubs + # Check that stubs for docstub are up-to-date by regenerating them + # with docstub and looking for differences. run: | - python -m docstub run -v src/docstub - .github/scripts/assert-unchanged.sh src/docstub + python -m docstub run -v src/docstub -o src/docstub-stubs + .github/scripts/assert-unchanged.sh src/docstub-stubs/ - name: Check with mypy.stubtest run: | From c70a286eb2efdbf15411f378a686b090a066f210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Gr=C3=BCter?= Date: Fri, 24 Oct 2025 02:08:12 +0200 Subject: [PATCH 06/10] Enable tracing in assert-unchanged.sh to debug permission error --- .github/scripts/assert-unchanged.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/scripts/assert-unchanged.sh b/.github/scripts/assert-unchanged.sh index 477020e..8e96405 100644 --- a/.github/scripts/assert-unchanged.sh +++ b/.github/scripts/assert-unchanged.sh @@ -3,7 +3,7 @@ # Assert that there are no changes in a given directory compared to HEAD. # Expects a relative directory as the one and only argument. -set -e +set -ex CHECK_DIR=$1 @@ -24,4 +24,4 @@ if [ $GIT_DIFF_HEAD_EXIT_CODE -ne 0 ] || [ -n "$UNTRACKED" ]; then exit 1 fi -set +e \ No newline at end of file +set +ex \ No newline at end of file From 270a31a8c467059cbe7d5a18858653f5d9bd4d02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Gr=C3=BCter?= Date: Fri, 24 Oct 2025 02:12:43 +0200 Subject: [PATCH 07/10] Set execution bit for assert-unchanged.sh *facepalm* --- .github/scripts/assert-unchanged.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) mode change 100644 => 100755 .github/scripts/assert-unchanged.sh diff --git a/.github/scripts/assert-unchanged.sh b/.github/scripts/assert-unchanged.sh old mode 100644 new mode 100755 index 8e96405..80fa519 --- a/.github/scripts/assert-unchanged.sh +++ b/.github/scripts/assert-unchanged.sh @@ -3,7 +3,7 @@ # Assert that there are no changes in a given directory compared to HEAD. # Expects a relative directory as the one and only argument. -set -ex +set -e CHECK_DIR=$1 @@ -24,4 +24,4 @@ if [ $GIT_DIFF_HEAD_EXIT_CODE -ne 0 ] || [ -n "$UNTRACKED" ]; then exit 1 fi -set +ex \ No newline at end of file +set +e From b55bb1501e3a5ecb8b685dcef174f8135381f4d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Gr=C3=BCter?= Date: Fri, 24 Oct 2025 02:15:58 +0200 Subject: [PATCH 08/10] Exclude docstub-stubs from pre-commit and undo changes --- .pre-commit-config.yaml | 1 + src/docstub-stubs/__main__.pyi | 2 ++ src/docstub-stubs/_analysis.pyi | 12 +++++++++++- src/docstub-stubs/_cache.pyi | 1 + src/docstub-stubs/_cli.pyi | 17 ++++++++++++++++- src/docstub-stubs/_config.pyi | 1 + src/docstub-stubs/_docstrings.pyi | 7 +++++-- src/docstub-stubs/_path_utils.pyi | 5 +++-- src/docstub-stubs/_report.pyi | 6 +++++- src/docstub-stubs/_stubs.pyi | 9 ++++++++- src/docstub-stubs/_utils.pyi | 4 ++++ src/docstub-stubs/_vendored/stdlib.pyi | 2 ++ 12 files changed, 59 insertions(+), 8 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1c5fdb4..bb4437c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,6 +3,7 @@ exclude: | (?x)^( examples/.*-stubs/.*| + src/docstub-stubs/.*| )$ repos: diff --git a/src/docstub-stubs/__main__.pyi b/src/docstub-stubs/__main__.pyi index ced2abe..ce39888 100644 --- a/src/docstub-stubs/__main__.pyi +++ b/src/docstub-stubs/__main__.pyi @@ -1,4 +1,6 @@ # File generated with docstub +from ._cli import cli + if __name__ == "__main__": pass diff --git a/src/docstub-stubs/_analysis.pyi b/src/docstub-stubs/_analysis.pyi index 4e34524..4e439d9 100644 --- a/src/docstub-stubs/_analysis.pyi +++ b/src/docstub-stubs/_analysis.pyi @@ -1,18 +1,27 @@ # File generated with docstub +import builtins +import importlib +import json import logging +import re from collections.abc import Iterable -from dataclasses import dataclass +from dataclasses import asdict, dataclass +from functools import cache from pathlib import Path from typing import Any, ClassVar import libcst as cst +import libcst.matchers as cstm + +from ._utils import accumulate_qualname, module_name_from_path, pyfile_checksum logger: logging.Logger def _shared_leading_qualname(*qualnames: tuple[str]) -> str: ... @dataclass(slots=True, frozen=True) class PyImport: + import_: str | None = ... from_: str | None = ... as_: str | None = ... @@ -36,6 +45,7 @@ def common_known_types() -> dict[str, PyImport]: ... class TypeCollector(cst.CSTVisitor): class ImportSerializer: + suffix: ClassVar[str] encoding: ClassVar[str] diff --git a/src/docstub-stubs/_cache.pyi b/src/docstub-stubs/_cache.pyi index e2f4496..43c2af2 100644 --- a/src/docstub-stubs/_cache.pyi +++ b/src/docstub-stubs/_cache.pyi @@ -19,6 +19,7 @@ def create_cache(path: Path) -> None: ... def validate_cache(path: Path) -> None: ... class FuncSerializer[T](Protocol): + suffix: str def hash_args(self, *args: Any, **kwargs: Any) -> str: ... diff --git a/src/docstub-stubs/_cli.pyi b/src/docstub-stubs/_cli.pyi index fa6270a..bab8dd4 100644 --- a/src/docstub-stubs/_cli.pyi +++ b/src/docstub-stubs/_cli.pyi @@ -1,14 +1,29 @@ # File generated with docstub import logging +import shutil +import sys +import time +from collections import Counter from collections.abc import Iterable, Sequence +from contextlib import contextmanager from pathlib import Path from typing import Literal import click -from ._analysis import PyImport +from ._analysis import PyImport, TypeCollector, TypeMatcher, common_known_types +from ._cache import CACHE_DIR_NAME, FileCache, validate_cache from ._config import Config +from ._path_utils import ( + STUB_HEADER_COMMENT, + find_package_root, + walk_source_and_targets, + walk_source_package, +) +from ._report import setup_logging +from ._stubs import Py2StubTransformer, try_format_stub +from ._version import __version__ logger: logging.Logger diff --git a/src/docstub-stubs/_config.pyi b/src/docstub-stubs/_config.pyi index 21452a9..2bfebcf 100644 --- a/src/docstub-stubs/_config.pyi +++ b/src/docstub-stubs/_config.pyi @@ -2,6 +2,7 @@ import dataclasses import logging +import tomllib from collections.abc import Mapping from pathlib import Path from typing import ClassVar, Self diff --git a/src/docstub-stubs/_docstrings.pyi b/src/docstub-stubs/_docstrings.pyi index 6fd534c..4fda48d 100644 --- a/src/docstub-stubs/_docstrings.pyi +++ b/src/docstub-stubs/_docstrings.pyi @@ -1,19 +1,21 @@ # File generated with docstub import logging +import traceback from collections.abc import Generator, Iterable -from dataclasses import dataclass +from dataclasses import dataclass, field from functools import cached_property from pathlib import Path from typing import Any, ClassVar +import click import lark import lark.visitors import numpydoc.docscrape as npds from ._analysis import PyImport, TypeMatcher from ._report import ContextReporter -from ._utils import DocstubError +from ._utils import DocstubError, escape_qualname logger: logging.Logger @@ -28,6 +30,7 @@ _lark: lark.Lark def _find_one_token(tree: lark.Tree, *, name: str) -> lark.Token: ... @dataclass(frozen=True, slots=True, kw_only=True) class Annotation: + value: str imports: frozenset[PyImport] = ... diff --git a/src/docstub-stubs/_path_utils.pyi b/src/docstub-stubs/_path_utils.pyi index b6baa70..1108f4f 100644 --- a/src/docstub-stubs/_path_utils.pyi +++ b/src/docstub-stubs/_path_utils.pyi @@ -4,12 +4,13 @@ import logging import re import sys from collections.abc import Generator, Sequence +from functools import lru_cache from pathlib import Path if sys.version_info >= (3, 13): - pass + from glob import translate as glob_translate else: - pass + from ._vendored.stdlib import glob_translate logger: logging.Logger diff --git a/src/docstub-stubs/_report.pyi b/src/docstub-stubs/_report.pyi index cf3c019..9b0740b 100644 --- a/src/docstub-stubs/_report.pyi +++ b/src/docstub-stubs/_report.pyi @@ -3,12 +3,16 @@ import dataclasses import logging from pathlib import Path +from textwrap import indent from typing import Any, ClassVar, Self, TextIO +import click + logger: logging.Logger @dataclasses.dataclass(kw_only=True, slots=True, frozen=True) class ContextReporter: + logger: logging.Logger path: Path | None = ... line: int | None = ... @@ -19,7 +23,7 @@ class ContextReporter: logger: logging.Logger | None = ..., path: Path | None = ..., line: int | None = ..., - line_offset: int | None = ..., + line_offset: int | None = ... ) -> Self: ... def report( self, short: str, *, log_level: int, details: str | None = ..., **log_kw: Any diff --git a/src/docstub-stubs/_stubs.pyi b/src/docstub-stubs/_stubs.pyi index 22ffb1a..a6521f6 100644 --- a/src/docstub-stubs/_stubs.pyi +++ b/src/docstub-stubs/_stubs.pyi @@ -3,24 +3,30 @@ import enum import logging from dataclasses import dataclass +from functools import wraps from pathlib import Path from typing import ClassVar, Literal import libcst as cst +import libcst.matchers as cstm +from _typeshed import Incomplete from ._analysis import PyImport, TypeMatcher from ._docstrings import ( Annotation, DocstringAnnotations, DoctypeTransformer, + FallbackAnnotation, ) from ._report import ContextReporter +from ._utils import module_name_from_path logger: logging.Logger def try_format_stub(stub: str) -> str: ... class ScopeType(enum.StrEnum): + MODULE = "module" CLASS = "class" FUNC = "func" @@ -30,6 +36,7 @@ class ScopeType(enum.StrEnum): @dataclass(slots=True, frozen=True) class _Scope: + type: ScopeType node: cst.CSTNode | None = ... @@ -112,7 +119,7 @@ class Py2StubTransformer(cst.CSTTransformer): *, name: str, trailing_semicolon: bool = ..., - reporter: ContextReporter | None = ..., + reporter: ContextReporter | None = ... ) -> cst.AnnAssign: ... def _insert_instance_attributes( self, updated_node: cst.ClassDef, attributes: dict[str, Annotation] diff --git a/src/docstub-stubs/_utils.pyi b/src/docstub-stubs/_utils.pyi index 9ae912b..8b8b4bd 100644 --- a/src/docstub-stubs/_utils.pyi +++ b/src/docstub-stubs/_utils.pyi @@ -1,7 +1,11 @@ # File generated with docstub +import itertools +import re from collections.abc import Callable +from functools import lru_cache, wraps from pathlib import Path +from zlib import crc32 def accumulate_qualname(qualname: str, *, start_right: bool = ...) -> None: ... def escape_qualname(name: str) -> str: ... diff --git a/src/docstub-stubs/_vendored/stdlib.pyi b/src/docstub-stubs/_vendored/stdlib.pyi index 2fed41e..b4ff650 100644 --- a/src/docstub-stubs/_vendored/stdlib.pyi +++ b/src/docstub-stubs/_vendored/stdlib.pyi @@ -1,5 +1,7 @@ # File generated with docstub +import os +import re from collections.abc import Sequence def _fnmatch_translate(pat: str, STAR: str, QUESTION_MARK: str) -> str: ... From 22a2f0adaf48a7ec5a5d3230fd6f09028c1ea697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Gr=C3=BCter?= Date: Fri, 24 Oct 2025 02:21:45 +0200 Subject: [PATCH 09/10] Tweak step name and add comment --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d270ff3..e99fe4f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -77,7 +77,9 @@ jobs: # TODO upload coverage statistics, and fail on decrease? - - name: Compare example stubs + - name: Check example_pkg-stubs + # Check that stubs for example_pkg are up-to-date by regenerating them + # with docstub and looking for differences. run: | python -m docstub run -v \ --config=examples/docstub.toml \ From 9f0410a8241a730532375d4846a86c05da407663 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lars=20Gr=C3=BCter?= Date: Fri, 24 Oct 2025 02:31:45 +0200 Subject: [PATCH 10/10] Add success message to assert-unchanged.sh --- .github/scripts/assert-unchanged.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/scripts/assert-unchanged.sh b/.github/scripts/assert-unchanged.sh index 80fa519..ef32496 100755 --- a/.github/scripts/assert-unchanged.sh +++ b/.github/scripts/assert-unchanged.sh @@ -20,8 +20,10 @@ set -e # Display changes in tracked files and capture exit status if [ $GIT_DIFF_HEAD_EXIT_CODE -ne 0 ] || [ -n "$UNTRACKED" ]; then - echo "::error::Uncommited changes in directory: $CHECK_DIR" + echo "::error::Uncommited changes in directory '$CHECK_DIR'" exit 1 +else + echo "::notice::No Uncommited changes, directory '$CHECK_DIR' is clean" fi set +e