Skip to content

Fix the docstrings the API reference site renders wrongly - #151

Draft
zeevmoney wants to merge 9 commits into
per-16779/aggregate-ci-checkfrom
per-16775/docstring-fixes
Draft

zeevmoney wants to merge 9 commits into
per-16779/aggregate-ci-checkfrom
per-16775/docstring-fixes

Conversation

@zeevmoney

Copy link
Copy Markdown
Member

Linear issues

  • PER-16775: API reference site for permit-python. This PR fixes the docstrings the site renders (PR 1 of 3).
  • Follow-up: PER-16803, the blocking-client stub's copied await examples.

Stacked on #150.

Why

The API reference site will be generated with Zensical and mkdocstrings-python. mkdocstrings parses docstrings with Griffe's Google parser and renders them as Markdown. At the base, many docstrings rendered wrongly there:

  • 23 example sections held code that was not fenced. Their # comment lines rendered as headings and their code as prose. 7 of them were headed Usage example:, which is not a Google section, so Griffe folded them into the description.
  • Griffe reported 6 warnings, all from the ResourceInstancesApi.bulk_delete() Args entry, in the source and in the generated stub.
  • Other markup broke:
    • Returns and Yields items with unindented continuation lines rendered as several rows.
    • Items written as type: description showed a return value named after the type.
    • <...> placeholders were dropped as HTML tags.
    • reST roles and a :: literal block printed as written.

This PR changes docstrings only, so the site PR can build with no docstring warnings.

What changed

  • Examples. Every example section is now a Google Examples: section with its code in a fenced python block: 8 in permit/permit.py, 9 in permit/sync.py and 3 in permit/enforcement/enforcer.py. The 7 Usage example: headings are now Examples:. Three more snippets are fenced:

    • the reST :: block in the Permit class docstring;
    • the ApiContext snippet, which had no language and no blank line above it;
    • the decimal_encoder() doctest, which is now an Examples section fenced as pycon and still passes as a doctest.

    Ruff's docstring-code-format formats the fenced code (double quotes, the bulk_check() list layout).

  • Example code that did not match the API:

    • permit.elements.loginAs(...) is now permit.elements.login_as(...), on both clients.
    • The blocking client's permit.pdp_api.role_assignments(...), which is not callable, is now permit.pdp_api.role_assignments.list().
    • The blocking client's bulk_check() example no longer uses await.
    • The bulk_check() examples' dicts keyed by the bare names type and key now use the strings "type" and "key" (Permit, sync Permit and Enforcer).
  • bulk_delete() Args. The continuation lines of the resource_instances entry are indented under it. The words are the same.

  • Returns and Yields.

    • Continuation lines are indented in 9 items: Permit.wait_for_sync() (Yields), GroupsApi.list() and get(), ResourceRelationsApi.list(), PdpsApi.refresh(), Enforcer.get_user_tenants(), async_to_sync(), and the private _LoopThread._track() and submit().
    • 17 items written as type: description now hold only the description. The type comes from the return annotation, as in the SDK's other Returns sections.
  • Markup.

    • <resourceKey:actionKey> (2 in RolesApi) and <resource>:<action> (ConditionSetRulesApi.list()) are inline code.
    • The :func: and :class: roles in the SYNC_WRAPPER_MARKER docstring are inline code.
    • The ApiContext list has the blank line it needs to render as a list.
  • Stub. permit/_sync_types.pyi is regenerated with uv run python scripts/generate_sync_stubs.py in each commit that changes a docstring it copies, so every commit passes the stub drift test.

Behaviour changes

None. Only docstrings and the regenerated stub change. For each of the 13 changed files, ast.dump of the module with every docstring removed is identical to the base.

How it was tested

  • Griffe 2.3.0, uvx --exclude-newer 2026-09-26 --from griffe griffe dump permit -s . -d google -f -o /dev/null: 0 warnings, against 6 at the base.

  • A scratch script walked every docstring Griffe parses: all of permit/ except the generated permit/api/models.py.

    base this PR
    lines that render as a Markdown heading 38 0
    code lines outside a fence in an Examples section 99 (16 sections) 0 (24 sections)
    code lines outside a fence in description text 18 0
    Usage example: headings 7 0
    fenced blocks 1 (no language) 26 (25 python, 1 pycon)

    Rendering every section with Python-Markdown and pymdownx.superfences gives 38 <h1>-<h6> elements at the base and 0 here.

  • Every fenced block parses: the python blocks with PyCF_ALLOW_TOP_LEVEL_AWAIT, and the pycon block's doctests pass. The blocks make 41 permit. calls, which were resolved on real permit.Permit and permit.sync.Permit instances. All of them exist, and each is awaited exactly when it is a coroutine, except 7 calls in the stub's SyncEnforcer copies (see below).

  • uv run pre-commit run --all-files: all hooks pass.

  • uv run mypy and uv run --group pydantic-v1 mypy: no issues in 123 source files.

  • uv run pytest -q -m 'not e2e': 1918 passed, 3 skipped (the pydantic 1 tests), 45 deselected. This includes the sync-stub drift test and tests/test_typing_surface.py.

  • Each of the 9 commits passes ruff and tests/test_typing_surface.py on its own.

  • The checks catch regressions. Each of these made its check fail: an unfenced example, a misspelled method, a stray await, an unindented Args line, a closing fence right after doctest output, and a code change outside docstrings. Editing an Enforcer docstring without regenerating the stub fails the drift test.

Not changed: the SyncEnforcer examples. Type checkers and IDEs resolve permit.enforcement.enforcer.SyncEnforcer to the stub class, and the stub generator copies its docstrings word for word from Enforcer. So its examples in permit/_sync_types.pyi still await its blocking methods, as at the base. Griffe treats that name as non-public, so the site does not show it. Fixing the examples means changing scripts/generate_sync_stubs.py, which is outside this PR; PER-16803 tracks it.

Owner actions before merge

None for this PR. For the site PR:

  • Keep mkdocstrings' default returns_named_value: true, or rewrap the first Returns line of permit.utils.sync.async_to_sync(), which contains a colon.
  • Enable pymdownx.superfences, because the fences rely on it.
  • Check how the See Also: section of Permit.wait_for_sync() renders. Griffe turns it into an admonition, and its bare URL becomes a link only with an autolink or magiclink extension.

🤖 Generated with Claude Code

zeevmoney and others added 9 commits October 3, 2026 23:21
The examples were indented code, so the API reference site would render
their comment lines as headings and their code as prose. Each one is now
an Examples section with its code in a fenced python block, which ruff
formats (docstring-code-format). The `Usage example:` headings, which
are not a Google docstring section, become `Examples:`.

Fixes in the example code:
- `elements.loginAs()` is `elements.login_as()`.
- The blocking client's `pdp_api` example called
  `role_assignments(...)`, which is not callable; it now calls
  `role_assignments.list()`, as the async client's example does.
- The blocking client's `bulk_check()` example no longer awaits.
- The `bulk_check()` examples' dicts keyed by the names `type` and
  `key` now use the strings "type" and "key".

Refs PER-16775.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The Enforcer's check(), bulk_check() and authorized_users() examples
  get the same fenced Examples sections as the clients' (bulk_check()'s
  dict keys become strings there too).
- decimal_encoder()'s doctest was plain text, which Markdown renders as
  a blockquote. It moves to an Examples section, fenced as pycon so the
  prompts and outputs stay as they are.
- ApiContext's code block was glued to the paragraph above it and had
  no language; it gets a blank line and `python`.

Regenerate permit/_sync_types.pyi, which copies the async docstrings
into the blocking stubs.

Refs PER-16775.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Griffe's Google parser reads each line at an item's own indentation as
a new item, so these Returns and Yields sections rendered as several
rows of sentence fragments. Their continuation lines are now indented
under the item:

- Permit.wait_for_sync() (Yields)
- GroupsApi.list() and GroupsApi.get()
- ResourceRelationsApi.list()
- PdpsApi.refresh()
- Enforcer.get_user_tenants()
- permit.utils.sync.async_to_sync()

Regenerate permit/_sync_types.pyi, which copies the async docstrings
into the blocking stubs.

Refs PER-16775.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ResourceInstancesApi.bulk_delete() documented `resource_instances` over
three lines at the same indentation, so Griffe read the second line as
a parameter named "Each identity can be either `resource_type" and
failed to parse the third: three warnings, each repeated for the
generated stub. The entry is rewrapped as one parameter with its
continuation lines indented, with the same words.

Regenerate permit/_sync_types.pyi, which copies the async docstrings
into the blocking stubs.

Refs PER-16775.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- RolesApi.assign_permissions() and remove_permissions(), and
  ConditionSetRulesApi.list(): the placeholders <resourceKey:actionKey>
  and <resource>:<action> were read as HTML tags and dropped from the
  page. They are inline code now.
- SYNC_WRAPPER_MARKER: the reST roles :func: and :class: were printed
  as is. The names are plain inline code now.
- ApiContext: the list of context levels had no blank line above it,
  so Python-Markdown folded it into the paragraph.

Regenerate permit/_sync_types.pyi, which copies the async docstrings
into the blocking stubs.

Refs PER-16775.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With Griffe's default Google options, a Returns or Yields item written
as `type: description` is read as a value named after the type when the
type is a bare name (`bool:`, `dict:`, `Permit:`,
`AuthorizedUsersResult:`), and keeps the type at the start of its
description otherwise (`list[bool]:` and the like). The reference site
would show a return value named "bool", or repeat the type in the
description. The items now hold the description alone; the type comes
from the return annotation, as for the SDK's other Returns sections.

The items are the Returns of check(), bulk_check(), authorized_users(),
get_user_permissions(), get_user_tenants() and filter_objects() on
permit.Permit and permit.sync.Permit, the Yields of
Permit.wait_for_sync(), and the Returns of the Enforcer's check(),
bulk_check(), authorized_users() and filter_objects().

Regenerate permit/_sync_types.pyi, which copies the async docstrings
into the blocking stubs.

Refs PER-16775.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
_LoopThread._track and _LoopThread.submit had Returns items whose second
line was not indented, so Griffe split each into two return values. They
are private, but async_to_sync in the same module had the same defect
fixed, and an unindented continuation line breaks the item for any
Google-style renderer.

Refs PER-16775.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The closing ``` sat on the line after the last expected output, so
doctest read it as part of that output and the example failed under
`python -m doctest` or `pytest --doctest-modules`. A blank line now
ends the output first; the rendered pycon block is unchanged.

Refs PER-16775.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Oct 3, 2026

Copy link
Copy Markdown

PER-16775

@github-actions

github-actions Bot commented Oct 3, 2026

Copy link
Copy Markdown

Dependency Security Audit

Scanned: pyproject.toml dependencies + dev group, resolved at Python 3.10 (the current resolution, and the lowest versions the published specs permit under each pydantic major)

✅ No known vulnerabilities found.

Both the resolved dependency set and the lowest versions the published specs permit are clean at HIGH and CRITICAL.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant