This file provides guidance to AI agents (including Claude Code, Cursor, and other LLM-powered tools) when working with code in this repository.
- ALL tests MUST pass for code to be considered complete and working
- Never describe code as "working as expected" if there are ANY failing tests
- Even if specific feature tests pass, failing tests elsewhere indicate broken functionality
- Changes that break existing tests must be fixed before considering implementation complete
- A successful implementation must pass linting, type checking, AND all existing tests
gp-libs is a Python library providing internal utilities and extensions for git-pull projects. It focuses on extending Sphinx documentation and pytest functionality with support for docutils-compatible markup formats.
Key features:
- doctest_docutils: Reimplementation of Python's doctest with support for reStructuredText and Markdown
- pytest_doctest_docutils: pytest plugin for running doctests in documentation files
- linkify_issues: Sphinx extension that converts issue references (e.g.,
#123) to hyperlinks - Supports testing doctest examples in
.rstand.mdfiles - Powers documentation testing across the git-pull ecosystem
This project uses:
- Python 3.10+
- just for command running (see also https://just.systems/)
- uv for dependency management
- ruff for linting and formatting
- mypy for type checking
- pytest for testing
- pytest-watcher for continuous testing
# Install dependencies
uv pip install --editable .
uv pip sync
# Install with development dependencies
uv pip install --editable . -G dev# Run all tests
just test
# or directly with pytest
uv run pytest
# Run a single test file
uv run pytest tests/test_doctest_docutils.py
# Run a specific test
uv run pytest tests/test_doctest_docutils.py::test_function_name
# Run tests with test watcher
just start
# or
uv run ptw .
# Run tests with doctests
uv run ptw . --now --doctest-modules# Run ruff for linting
just ruff
# or directly
uv run ruff check .
# Format code with ruff
just ruff-format
# or directly
uv run ruff format .
# Run ruff linting with auto-fixes
uv run ruff check . --fix --show-fixes
# Run mypy for type checking
just mypy
# or directly
uv run mypy src tests
# Watch mode for linting (using entr)
just watch-ruff
just watch-mypyFollow this workflow for code changes (see .cursor/rules/dev-loop.mdc):
- Format First:
uv run ruff format . - Run Tests:
uv run pytest - Run Linting:
uv run ruff check . --fix --show-fixes - Check Types:
uv run mypy - Verify Tests Again:
uv run pytest
# Build documentation
just build-docs
# Start documentation server with auto-reload
just start-docs
# Update documentation CSS/JS
just design-docsgp-libs provides utilities for documentation testing and Sphinx extensions:
src/
├── doctest_docutils.py # Core doctest reimplementation
├── pytest_doctest_docutils.py # pytest plugin
├── linkify_issues.py # Sphinx extension
├── docutils_compat.py # Compatibility layer
└── gp_libs.py # Package metadata
-
doctest_docutils (
src/doctest_docutils.py)- Reimplementation of Python's standard library
doctestmodule - Supports docutils-compatible markup (reStructuredText and Markdown)
- Handles
doctest_block,.. doctest::directive, and```{doctest}code blocks - PEP-440 version specifier support for conditional tests
- Can be run directly:
python -m doctest_docutils README.md -v
- Reimplementation of Python's standard library
-
pytest_doctest_docutils (
src/pytest_doctest_docutils.py)- pytest plugin integrating doctest_docutils with pytest
- Collects and runs doctests from
.rstand.mdfiles - Full pytest fixture and conftest.py support
- Registered as
pytest11entry point
-
linkify_issues (
src/linkify_issues.py)- Sphinx extension for automatic issue linking
- Converts
#123references to clickable hyperlinks - Configured via
issue_url_tplin Sphinx conf.py
-
docutils_compat (
src/docutils_compat.py)- Compatibility layer for cross-version docutils support
- Provides
findall()abstraction for different docutils versions
-
gp_libs (
src/gp_libs.py)- Package metadata (version, title, author, URLs)
gp-libs uses pytest for testing with custom fixtures. The test suite includes:
- Unit tests for doctest parsing and execution
- Integration tests for pytest plugin functionality
- Sphinx app factory for testing extensions
tests/
├── test_doctest_docutils.py # Tests for doctest module
├── test_pytest_doctest_docutils.py # Tests for pytest plugin
├── test_linkify_issues.py # Tests for linkify extension
├── conftest.py # Fixtures and sphinx app factory
└── regressions/ # Regression tests
-
Use functional tests only: Write tests as standalone functions, not classes. Avoid
class TestFoo:groupings - use descriptive function names and file organization instead. -
Use existing fixtures over mocks (see
.cursor/rules/dev-loop.mdc)- Use fixtures from conftest.py instead of
monkeypatchandMagicMockwhen available - Document in test docstrings why standard fixtures weren't used for exceptional cases
- Use fixtures from conftest.py instead of
-
Preferred pytest patterns
- Use
tmp_path(pathlib.Path) fixture over Python'stempfile - Use
monkeypatchfixture overunittest.mock
- Use
-
Running tests continuously
- Use pytest-watcher during development:
uv run ptw . - For doctests:
uv run ptw . --now --doctest-modules
- Use pytest-watcher during development:
For detailed coding standards, refer to .cursor/rules/dev-loop.mdc. Key highlights:
- Use namespace imports for stdlib:
import enuminstead offrom enum import Enum; third-party packages may usefrom X import Y - For typing, use
import typing as tand access via namespace:t.NamedTuple, etc. - Use
from __future__ import annotationsat the top of all Python files
Follow NumPy docstring style for all functions and methods (see .cursor/rules/dev-loop.mdc):
"""Short description of the function or class.
Detailed description using reStructuredText format.
Parameters
----------
param1 : type
Description of param1
param2 : type
Description of param2
Returns
-------
type
Description of return value
"""All functions and methods MUST have working doctests. Doctests serve as both documentation and tests.
CRITICAL RULES:
- Doctests MUST actually execute - never comment out function calls or use placeholder output
- Doctests MUST NOT be converted to
.. code-block::as a workaround (code-blocks don't run) - If you cannot create a working doctest, STOP and ask for help
Available tools for doctests:
doctest_namespacefixtures:tmp_path(add more viaconftest.py)- Ellipsis for variable output:
# doctest: +ELLIPSIS - PEP-440 version specifiers via
is_allowed_version()for version-conditional tests
# doctest: +SKIP is NOT permitted - it's just another workaround that doesn't test anything. Use the fixtures and ellipsis patterns properly.
Simple doctest example:
>>> is_allowed_version('3.3', '<=3.5')
True
>>> is_allowed_version('3.3', '>3.2, <4.0')
TrueWhen output varies, use ellipsis:
>>> parse_document(content) # doctest: +ELLIPSIS
<docutils.nodes.document ...>Additional guidelines:
- Use narrative descriptions for test sections rather than inline comments
- Move complex examples to dedicated test files at
tests/examples/<path_to_module>/test_<example>.py - Keep doctests simple and focused on demonstrating usage
- Add blank lines between test sections for improved readability
These rules guide future logging changes; existing code may not yet conform.
- Use
logging.getLogger(__name__)in every module - Add
NullHandlerin library__init__.pyfiles - Never configure handlers, levels, or formatters in library code — that's the application's job
Pass structured data on every log call where useful for filtering, searching, or test assertions.
Core keys (stable, scalar, safe at any log level):
| Key | Type | Context |
|---|---|---|
doctest_source_file |
str |
doctest source path (.rst, .md, .py) |
doctest_block_type |
str |
block type (doctest_block, code fence) |
sphinx_extension |
str |
Sphinx extension name |
Treat established keys as compatibility-sensitive — downstream users may build dashboards and alerts on them. Change deliberately.
snake_case, not dotted; project-specific prefixes (doctest_,sphinx_)- Prefer stable scalars; avoid ad-hoc objects
logger.debug("msg %s", val) not f-strings. Two rationales:
- Deferred string interpolation: skipped entirely when level is filtered
- Aggregator message template grouping:
"Running %s"is one signature grouped ×10,000; f-strings make each line unique
When computing val itself is expensive, guard with if logger.isEnabledFor(logging.DEBUG).
Increment for each wrapper layer so %(filename)s:%(lineno)d and OTel code.filepath point to the real caller. Verify whenever call depth changes.
| Level | Use for | Examples |
|---|---|---|
DEBUG |
Internal mechanics | Doctest parsing, node traversal steps |
INFO |
Lifecycle, user-visible operations | Extension loaded, document processed |
WARNING |
Recoverable issues, deprecation | Deprecated directive, missing optional dependency |
ERROR |
Failures that stop an operation | Parse error, invalid configuration |
- Lowercase, past tense for events:
"extension loaded","parse error" - No trailing punctuation
- Keep messages short; put details in
extra, not the message string
- Use
logger.exception()only insideexceptblocks when you are not re-raising - Use
logger.error(..., exc_info=True)when you need the traceback outside anexceptblock - Avoid
logger.exception()followed byraise— this duplicates the traceback. Either add context viaextrathat would otherwise be lost, or let the exception propagate
Assert on caplog.records attributes, not string matching on caplog.text:
- Scope capture:
caplog.at_level(logging.DEBUG, logger="doctest_docutils") - Filter records rather than index by position:
[r for r in caplog.records if hasattr(r, "doctest_source_file")] - Assert on schema:
record.sphinx_extension == "doctest_docutils"not"doctest_docutils" in caplog.text caplog.record_tuplescannot access extra fields — always usecaplog.records
- f-strings/
.format()in log calls - Unguarded logging in hot loops (guard with
isEnabledFor()) - Catch-log-reraise without adding new context
print()for diagnostics- Logging secret env var values (log key names only)
- Non-scalar ad-hoc objects in
extra - Requiring custom
extrafields in format strings without safe defaults (missing keys raiseKeyError)
See .cursor/rules/git-commits.mdc for detailed commit message standards.
Format commit messages as:
Scope(type[detail]): concise description
why: Explanation of necessity or impact.
what:
- Specific technical changes made
- Focused on a single topic
Common commit types:
- feat: New features or enhancements
- fix: Bug fixes
- refactor: Code restructuring without functional change
- docs: Documentation updates
- chore: Maintenance (dependencies, tooling, config)
- test: Test-related updates
- style: Code style and formatting
- py(deps): Dependencies
- py(deps[dev]): Dev Dependencies
- ai(rules[AGENTS]): AI rule updates
- ai(claude[rules]): Claude Code rules (CLAUDE.md)
- ai(claude[command]): Claude Code command changes
Example:
doctest_docutils(feat[parse]): Add support for myst-parser code blocks
why: Enable doctest execution in Markdown documentation files
what:
- Add detection for ```{doctest} fence syntax
- Register myst directives automatically
- Add tests for Markdown doctest parsing
For multi-line commits, use heredoc to preserve formatting:
git commit -m "$(cat <<'EOF'
feat(Component[method]) add feature description
why: Explanation of the change.
what:
- First change
- Second change
EOF
)"When writing documentation (README, CHANGES, docs/), follow these rules for code blocks:
One command per code block. This makes commands individually copyable.
Put explanations outside the code block, not as comments inside.
Good:
Run the tests:
$ uv run pytestRun with coverage:
$ uv run pytest --covBad:
# Run the tests
$ uv run pytest
# Run with coverage
$ uv run pytest --covSee .cursor/rules/avoid-debug-loops.mdc for detailed debugging guidance.
When stuck in debugging loops:
- Pause and acknowledge the loop
- Minimize to MVP: Remove all debugging cruft and experimental code
- Document the issue comprehensively for a fresh approach
- Format for portability (using quadruple backticks)
- Use
_ensure_directives_registered()to auto-register required directives - Supports myst-parser directives (
{doctest},{tab}) - Handles both reStructuredText and Markdown syntax
- Uses docutils for parsing
.rstfiles - Uses myst-parser for parsing
.mdfiles - Both formats support doctest blocks
In your Sphinx conf.py:
extensions = ["linkify_issues"]
issue_url_tpl = 'https://github.com/git-pull/gp-libs/issues/{issue_id}'- Documentation: https://gp-libs.git-pull.com/
- GitHub: https://github.com/git-pull/gp-libs
- PyPI: https://pypi.org/project/gp-libs/