From b731a7b58aa171da996d3988629a897a79b4f702 Mon Sep 17 00:00:00 2001 From: Zeev Manilovich Date: Sat, 3 Oct 2026 22:42:13 +0300 Subject: [PATCH 1/8] Fence the docstring examples of permit.Permit and permit.sync.Permit 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 --- permit/permit.py | 86 ++++++++++++++++++++++++++++-------------------- permit/sync.py | 80 +++++++++++++++++++++++++++----------------- 2 files changed, 100 insertions(+), 66 deletions(-) diff --git a/permit/permit.py b/permit/permit.py index 4683859c..6f55eb14 100644 --- a/permit/permit.py +++ b/permit/permit.py @@ -32,10 +32,12 @@ class Permit: The client keeps its HTTP connections open and reuses them: one aiohttp session, with its own pool of connections, for the Permit API and one for the PDP, per event loop it is used on. They are created by the first request from each loop. Close them with - ``await permit.close()``, or use the client as an async context manager:: + ``await permit.close()``, or use the client as an async context manager: - async with Permit(token="") as permit: - await permit.check("user", "read", "document") + ```python + async with Permit(token="") as permit: + await permit.check("user", "read", "document") + ``` A client that is never closed leaves nothing open behind it under ``asyncio.run()``, which closes the loop's sessions as it shuts the loop down, nor once it is garbage @@ -131,10 +133,11 @@ def config(self) -> PermitConfig: Once the SDK is initialized, the configuration is read-only. - Usage example: - + Examples: + ```python permit = Permit(config) pdp_url = permit.config.pdp + ``` """ return self._config.copy() @@ -205,10 +208,11 @@ def wait_for_sync( def api(self) -> PermitApiClient: """Access the Permit REST API using this property. - Usage example: - + Examples: + ```python permit = Permit(token="") await permit.api.roles.create(...) + ``` """ return self._api @@ -216,10 +220,11 @@ def api(self) -> PermitApiClient: def elements(self) -> ElementsApi: """Access the Permit Elements API using this property. - Usage example: - + Examples: + ```python permit = Permit(token="") - await permit.elements.loginAs(user, tenant) + await permit.elements.login_as(user, tenant) + ``` """ return self._elements @@ -229,10 +234,11 @@ def pdp_api(self) -> PermitPdpApiClient: Container PDP only: the cloud PDP serves none of its routes. - Usage example: - + Examples: + ```python permit = Permit(token="") await permit.pdp_api.role_assignments.list() + ``` """ return self._pdp_api @@ -259,15 +265,17 @@ async def authorized_users( PDP. Examples: + ```python # all the users that can close any issue? - await permit.authorized_users('close', 'issue') + await permit.authorized_users("close", "issue") # all the users that can close an issue who's id is 1234? - await permit.authorized_users('close', 'issue:1234') + await permit.authorized_users("close", "issue:1234") # all the users that can close (any) issues belonging to the 't1' tenant? # (in a multi tenant application) - await permit.authorized_users('close', {'type': 'issue', 'tenant': 't1'}) + await permit.authorized_users("close", {"type": "issue", "tenant": "t1"}) + ``` """ return await self._enforcer.authorized_users(action, resource, context) @@ -292,24 +300,28 @@ async def bulk_check( PDP. Examples: + ```python # Bulk query of multiple check conventions - await permit.bulk_check([ - { - "user": user, - "action": "close", - "resource": {type: "issue", key: "1234"}, - }, - { - "user": {key: "user"}, - "action": "close", - "resource": "issue:1235", - }, - { - "user": "user_a", - "action": "close", - "resource": "issue", - }, - ]) + await permit.bulk_check( + [ + { + "user": user, + "action": "close", + "resource": {"type": "issue", "key": "1234"}, + }, + { + "user": {"key": "user"}, + "action": "close", + "resource": "issue:1235", + }, + { + "user": "user_a", + "action": "close", + "resource": "issue", + }, + ] + ) + ``` """ return await self._enforcer.bulk_check(checks, context) @@ -337,15 +349,17 @@ async def check( PDP. Examples: + ```python # can the user close any issue? - await permit.check(user, 'close', 'issue') + await permit.check(user, "close", "issue") # can the user close any issue who's id is 1234? - await permit.check(user, 'close', 'issue:1234') + await permit.check(user, "close", "issue:1234") # can the user close (any) issues belonging to the 't1' tenant? # (in a multi tenant application) - await permit.check(user, 'close', {'type': 'issue', 'tenant': 't1'}) + await permit.check(user, "close", {"type": "issue", "tenant": "t1"}) + ``` """ return await self._enforcer.check(user, action, resource, context) @@ -408,9 +422,11 @@ async def get_user_tenants( other error status, or cannot be reached. Examples: + ```python # the tenants in which alice has a role tenants = await permit.get_user_tenants("alice") keys = [tenant.key for tenant in tenants] + ``` """ return await self._enforcer.get_user_tenants(user, context) diff --git a/permit/sync.py b/permit/sync.py index 2f9320b1..47ec667c 100644 --- a/permit/sync.py +++ b/permit/sync.py @@ -48,8 +48,10 @@ class Permit(AsyncPermit): address, as for the async client, ``permit.Permit``. Examples: + ```python with Permit(token="") as permit: permit.check("user", "read", "document") + ``` """ def __init__(self, config: PermitConfig | None = None, **options: Any) -> None: @@ -91,11 +93,13 @@ def close(self) -> None: # type: ignore[override] stop and join. Examples: + ```python permit = Permit(token="") try: permit.check("user", "read", "document") finally: permit.close() + ``` """ if not self._owns_sessions: return @@ -134,10 +138,11 @@ def __aenter__(self) -> None: # type: ignore[override] def api(self) -> SyncPermitApiClient: # type: ignore[override] """Access the Permit REST API using this property. - Usage example: - + Examples: + ```python permit = Permit(token="") permit.api.roles.create(...) + ``` """ return self._api # type: ignore[return-value] @@ -145,10 +150,11 @@ def api(self) -> SyncPermitApiClient: # type: ignore[override] def elements(self) -> SyncElementsApi: # type: ignore[override] """Access the Permit Elements API using this property. - Usage example: - + Examples: + ```python permit = Permit(token="") - permit.elements.loginAs(user, tenant) + permit.elements.login_as(user, tenant) + ``` """ return self._elements # type: ignore[return-value] @@ -158,9 +164,11 @@ def pdp_api(self) -> SyncPDPApi: Container PDP only: the cloud PDP serves none of its routes. - Usage example: - permit = Permit(token="") - permit.pdp_api.role_assignments(...) + Examples: + ```python + permit = Permit(token="") + permit.pdp_api.role_assignments.list() + ``` """ return self._pdp_api # type: ignore[return-value] @@ -186,24 +194,28 @@ def bulk_check( # type: ignore[override] PDP. Examples: + ```python # Bulk query of multiple check conventions - await permit.bulk_check([ - { - "user": user, - "action": "close", - "resource": {type: "issue", key: "1234"}, - }, - { - "user": {key: "user"}, - "action": "close", - "resource": "issue:1235", - }, - { - "user": "user_a", - "action": "close", - "resource": "issue", - }, - ]) + permit.bulk_check( + [ + { + "user": user, + "action": "close", + "resource": {"type": "issue", "key": "1234"}, + }, + { + "user": {"key": "user"}, + "action": "close", + "resource": "issue:1235", + }, + { + "user": "user_a", + "action": "close", + "resource": "issue", + }, + ] + ) + ``` """ return self._enforcer.bulk_check(checks, context) # type: ignore[return-value] @@ -231,15 +243,17 @@ def check( # type: ignore[override] PDP. Examples: + ```python # can the user close any issue? - permit.check(user, 'close', 'issue') + permit.check(user, "close", "issue") # can the user close any issue who's id is 1234? - permit.check(user, 'close', 'issue:1234') + permit.check(user, "close", "issue:1234") # can the user close (any) issues belonging to the 't1' tenant? # (in a multi tenant application) - permit.check(user, 'close', {'type': 'issue', 'tenant': 't1'}) + permit.check(user, "close", {"type": "issue", "tenant": "t1"}) + ``` """ return self._enforcer.check(user, action, resource, context) # type: ignore[return-value] @@ -266,15 +280,17 @@ def authorized_users( # type: ignore[override] PDP. Examples: + ```python # all the users that can close any issue? - permit.authorized_users('close', 'issue') + permit.authorized_users("close", "issue") # all the users that can close an issue who's id is 1234? - permit.authorized_users('close', 'issue:1234') + permit.authorized_users("close", "issue:1234") # all the users that can close (any) issues belonging to the 't1' tenant? # (in a multi tenant application) - permit.authorized_users('close', {'type': 'issue', 'tenant': 't1'}) + permit.authorized_users("close", {"type": "issue", "tenant": "t1"}) + ``` """ return self._enforcer.authorized_users(action, resource, context) # type: ignore[return-value] @@ -337,9 +353,11 @@ def get_user_tenants( # type: ignore[override] other error status, or cannot be reached. Examples: + ```python # the tenants in which alice has a role tenants = permit.get_user_tenants("alice") keys = [tenant.key for tenant in tenants] + ``` """ return self._enforcer.get_user_tenants(user, context) # type: ignore[return-value] From 2fb722438da728a4cbf0bb207637c526bf6a8dbd Mon Sep 17 00:00:00 2001 From: Zeev Manilovich Date: Sat, 3 Oct 2026 22:43:00 +0300 Subject: [PATCH 2/8] Fence the remaining docstring examples - 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 --- permit/_sync_types.pyi | 54 +++++++++++++++++++--------------- permit/api/context.py | 3 +- permit/api/encoders.py | 15 ++++++---- permit/enforcement/enforcer.py | 54 +++++++++++++++++++--------------- 4 files changed, 73 insertions(+), 53 deletions(-) diff --git a/permit/_sync_types.pyi b/permit/_sync_types.pyi index c5ccbb01..81ce7a82 100644 --- a/permit/_sync_types.pyi +++ b/permit/_sync_types.pyi @@ -3087,15 +3087,17 @@ class SyncEnforcer: PDP. Examples: + ```python # all the users that can close any issue? - await permit.authorized_users('close', 'issue') + await permit.authorized_users("close", "issue") # all the users that can close an issue who's id is 1234? - await permit.authorized_users('close', 'issue:1234') + await permit.authorized_users("close", "issue:1234") # all the users that can close (any) issues belonging to the 't1' tenant? # (in a multi tenant application) - await permit.authorized_users('close', {'type': 'issue', 'tenant': 't1'}) + await permit.authorized_users("close", {"type": "issue", "tenant": "t1"}) + ``` """ def bulk_check(self, checks: list[CheckQuery], context: Context | None = None) -> list[bool]: """Checks if a user is authorized to perform an action on a resource in a context. @@ -3117,24 +3119,28 @@ class SyncEnforcer: PDP. Examples: + ```python # Bulk query of multiple check conventions - await permit.bulk_check([ - { - "user": user, - "action": "close", - "resource": {type: "issue", key: "1234"}, - }, - { - "user": {key: "user"}, - "action": "close", - "resource": "issue:1235", - }, - { - "user": "user_a", - "action": "close", - "resource": "issue", - }, - ]) + await permit.bulk_check( + [ + { + "user": user, + "action": "close", + "resource": {"type": "issue", "key": "1234"}, + }, + { + "user": {"key": "user"}, + "action": "close", + "resource": "issue:1235", + }, + { + "user": "user_a", + "action": "close", + "resource": "issue", + }, + ] + ) + ``` """ def check( self, user: User, action: Action, resource: Resource, context: Context | None = None @@ -3156,15 +3162,17 @@ class SyncEnforcer: PDP. Examples: + ```python # can the user close any issue? - await permit.check(user, 'close', 'issue') + await permit.check(user, "close", "issue") # can the user close any issue who's id is 1234? - await permit.check(user, 'close', 'issue:1234') + await permit.check(user, "close", "issue:1234") # can the user close (any) issues belonging to the 't1' tenant? # (in a multi tenant application) - await permit.check(user, 'close', {'type': 'issue', 'tenant': 't1'}) + await permit.check(user, "close", {"type": "issue", "tenant": "t1"}) + ``` """ def get_user_permissions( self, diff --git a/permit/api/context.py b/permit/api/context.py index 72d3b5df..d8c8305d 100644 --- a/permit/api/context.py +++ b/permit/api/context.py @@ -78,7 +78,8 @@ class ApiContext: from that context. We then get this kind of experience: - ``` + + ```python await permit.api.roles.list() ``` diff --git a/permit/api/encoders.py b/permit/api/encoders.py index 8ef1fa55..5e919932 100644 --- a/permit/api/encoders.py +++ b/permit/api/encoders.py @@ -75,15 +75,18 @@ def decimal_encoder(dec_value: Decimal) -> int | float: results in failed round-tripping between encode and parse. Our Id type is a prime example of this. - >>> decimal_encoder(Decimal("1.0")) - 1.0 - - >>> decimal_encoder(Decimal("1")) - 1 - Raises: TypeError: If ``dec_value`` is NaN or infinite. JSON has no such values, so encoding one would send the API an invalid request body. + + Examples: + ```pycon + >>> decimal_encoder(Decimal("1.0")) + 1.0 + + >>> decimal_encoder(Decimal("1")) + 1 + ``` """ exponent = dec_value.as_tuple().exponent if not isinstance(exponent, int): diff --git a/permit/enforcement/enforcer.py b/permit/enforcement/enforcer.py index 072a13da..63b25282 100644 --- a/permit/enforcement/enforcer.py +++ b/permit/enforcement/enforcer.py @@ -152,15 +152,17 @@ async def authorized_users( PDP. Examples: + ```python # all the users that can close any issue? - await permit.authorized_users('close', 'issue') + await permit.authorized_users("close", "issue") # all the users that can close an issue who's id is 1234? - await permit.authorized_users('close', 'issue:1234') + await permit.authorized_users("close", "issue:1234") # all the users that can close (any) issues belonging to the 't1' tenant? # (in a multi tenant application) - await permit.authorized_users('close', {'type': 'issue', 'tenant': 't1'}) + await permit.authorized_users("close", {"type": "issue", "tenant": "t1"}) + ``` """ context = context or {} @@ -268,24 +270,28 @@ async def bulk_check( PDP. Examples: + ```python # Bulk query of multiple check conventions - await permit.bulk_check([ - { - "user": user, - "action": "close", - "resource": {type: "issue", key: "1234"}, - }, - { - "user": {key: "user"}, - "action": "close", - "resource": "issue:1235", - }, - { - "user": "user_a", - "action": "close", - "resource": "issue", - }, - ]) + await permit.bulk_check( + [ + { + "user": user, + "action": "close", + "resource": {"type": "issue", "key": "1234"}, + }, + { + "user": {"key": "user"}, + "action": "close", + "resource": "issue:1235", + }, + { + "user": "user_a", + "action": "close", + "resource": "issue", + }, + ] + ) + ``` """ context = context or {} request_body = [] @@ -391,15 +397,17 @@ async def check( PDP. Examples: + ```python # can the user close any issue? - await permit.check(user, 'close', 'issue') + await permit.check(user, "close", "issue") # can the user close any issue who's id is 1234? - await permit.check(user, 'close', 'issue:1234') + await permit.check(user, "close", "issue:1234") # can the user close (any) issues belonging to the 't1' tenant? # (in a multi tenant application) - await permit.check(user, 'close', {'type': 'issue', 'tenant': 't1'}) + await permit.check(user, "close", {"type": "issue", "tenant": "t1"}) + ``` """ context = context or {} From 3ca412598f972515e538f3ff876bcac985a8a279 Mon Sep 17 00:00:00 2001 From: Zeev Manilovich Date: Sat, 3 Oct 2026 22:43:45 +0300 Subject: [PATCH 3/8] Indent the continuation lines of Returns and Yields items 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 --- permit/_sync_types.pyi | 10 +++++----- permit/api/groups.py | 4 ++-- permit/api/pdps.py | 2 +- permit/api/resource_relations.py | 2 +- permit/enforcement/enforcer.py | 2 +- permit/permit.py | 8 ++++---- permit/utils/sync.py | 10 +++++----- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/permit/_sync_types.pyi b/permit/_sync_types.pyi index 81ce7a82..c24a1cb9 100644 --- a/permit/_sync_types.pyi +++ b/permit/_sync_types.pyi @@ -546,7 +546,7 @@ class SyncGroupsApi(BasePermitApi): Returns: One page of groups, with the total count. Each group's ``group_instance_key`` is - its instance key alone, and ``id`` is its instance id. + its instance key alone, and ``id`` is its instance id. Raises: PermitApiError: If the API returns an error HTTP status code. @@ -566,7 +566,7 @@ class SyncGroupsApi(BasePermitApi): Returns: The group. Its ``group_instance_key`` is its instance key alone, and ``id`` is - its instance id. + its instance id. Raises: PermitApiError: If the API returns an error HTTP status code, such as 404 when no @@ -813,7 +813,7 @@ class SyncPdpsApi(BasePermitApi): Returns: The id of the data update that carries the refresh, and the ids of the PDP - configurations it was sent to. + configurations it was sent to. Raises: pydantic.v1.ValidationError: If ``reason`` is longer than 512 characters. Nothing @@ -1716,7 +1716,7 @@ class SyncResourceRelationsApi(BasePermitApi): Returns: a PaginatedResultRelationRead holding the relations in ``.data`` and the - total number of relations on the resource in ``.total_count``. + total number of relations on the resource in ``.total_count``. Raises: PermitApiError: If the API returns an error HTTP status code. @@ -3220,7 +3220,7 @@ class SyncEnforcer: Returns: The user's tenants, each with its key and attributes. Empty when the user has no - tenant-level role or the PDP does not know the user. + tenant-level role or the PDP does not know the user. Raises: PermitConnectionError: If the PDP answers 404 (as the cloud PDP does), answers any diff --git a/permit/api/groups.py b/permit/api/groups.py index 7ab0ca84..e03a3c81 100644 --- a/permit/api/groups.py +++ b/permit/api/groups.py @@ -75,7 +75,7 @@ async def list(self, page: int = 1, per_page: int = 100) -> PaginatedResultGroup Returns: One page of groups, with the total count. Each group's ``group_instance_key`` is - its instance key alone, and ``id`` is its instance id. + its instance key alone, and ``id`` is its instance id. Raises: PermitApiError: If the API returns an error HTTP status code. @@ -104,7 +104,7 @@ async def get(self, group_instance_key: str) -> GroupReadSchema: Returns: The group. Its ``group_instance_key`` is its instance key alone, and ``id`` is - its instance id. + its instance id. Raises: PermitApiError: If the API returns an error HTTP status code, such as 404 when no diff --git a/permit/api/pdps.py b/permit/api/pdps.py index 2699e928..06687ce0 100644 --- a/permit/api/pdps.py +++ b/permit/api/pdps.py @@ -47,7 +47,7 @@ async def refresh(self, reason: str | None = None) -> PDPDataRefreshResponse: Returns: The id of the data update that carries the refresh, and the ids of the PDP - configurations it was sent to. + configurations it was sent to. Raises: pydantic.v1.ValidationError: If ``reason`` is longer than 512 characters. Nothing diff --git a/permit/api/resource_relations.py b/permit/api/resource_relations.py index b8bece2b..a7df9537 100644 --- a/permit/api/resource_relations.py +++ b/permit/api/resource_relations.py @@ -38,7 +38,7 @@ async def list( Returns: a PaginatedResultRelationRead holding the relations in ``.data`` and the - total number of relations on the resource in ``.total_count``. + total number of relations on the resource in ``.total_count``. Raises: PermitApiError: If the API returns an error HTTP status code. diff --git a/permit/enforcement/enforcer.py b/permit/enforcement/enforcer.py index 63b25282..7a316dee 100644 --- a/permit/enforcement/enforcer.py +++ b/permit/enforcement/enforcer.py @@ -600,7 +600,7 @@ async def get_user_tenants( Returns: The user's tenants, each with its key and attributes. Empty when the user has no - tenant-level role or the PDP does not know the user. + tenant-level role or the PDP does not know the user. Raises: PermitConnectionError: If the PDP answers 404 (as the cloud PDP does), answers any diff --git a/permit/permit.py b/permit/permit.py index 6f55eb14..3056d3bc 100644 --- a/permit/permit.py +++ b/permit/permit.py @@ -177,10 +177,10 @@ def wait_for_sync( Yields: Permit: A Permit instance that is configured to wait for facts to be synced. It - sends its requests over this client's connections, so it needs no ``close()``: - closing this client closes them, and its own ``close()`` does nothing. With - ``proxy_facts_via_pdp`` off, it logs a warning and yields this client itself, - whose ``close()`` closes them. + sends its requests over this client's connections, so it needs no ``close()``: + closing this client closes them, and its own ``close()`` does nothing. With + ``proxy_facts_via_pdp`` off, it logs a warning and yields this client itself, + whose ``close()`` closes them. See Also: https://docs.permit.io/how-to/manage-data/local-facts-uploader diff --git a/permit/utils/sync.py b/permit/utils/sync.py index 2aaf7974..bca05c77 100644 --- a/permit/utils/sync.py +++ b/permit/utils/sync.py @@ -550,11 +550,11 @@ def async_to_sync(func: Callable[P, Coroutine[Any, Any, T]]) -> Callable[P, T]: Returns: A callable that runs `func` to completion and returns its result: on the background - loop of the sync client that the first argument (`self`, for a method) belongs to, - otherwise in an event loop of its own. When it is called from inside a coroutine - that a blocking call is already driving, the coroutine is handed back untouched - instead, so that internal `await self.public_method(...)` calls keep working on a - converted class. + loop of the sync client that the first argument (`self`, for a method) belongs to, + otherwise in an event loop of its own. When it is called from inside a coroutine + that a blocking call is already driving, the coroutine is handed back untouched + instead, so that internal `await self.public_method(...)` calls keep working on a + converted class. """ @wraps(func) From f9fdb43cbc0141e21e2ede736d3b6fbdd8b1f42c Mon Sep 17 00:00:00 2001 From: Zeev Manilovich Date: Sat, 3 Oct 2026 22:43:58 +0300 Subject: [PATCH 4/8] Indent the bulk_delete() Args entry's continuation lines 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 --- permit/_sync_types.pyi | 6 +++--- permit/api/resource_instances.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/permit/_sync_types.pyi b/permit/_sync_types.pyi index c24a1cb9..f8c85371 100644 --- a/permit/_sync_types.pyi +++ b/permit/_sync_types.pyi @@ -1689,9 +1689,9 @@ class SyncResourceInstancesApi(BasePermitApi): this method raises as a ``PermitApiError`` that says so. Args: - resource_instances: The resource instance identities to delete. - Each identity can be either `resource_type:instance_key` (like Repository:react) or the - resource instance uuid. + resource_instances: The resource instance identities to delete. Each identity can + be either `resource_type:instance_key` (like Repository:react) or the resource + instance uuid. Returns: the bulk delete report. diff --git a/permit/api/resource_instances.py b/permit/api/resource_instances.py index 82007494..4207fb41 100644 --- a/permit/api/resource_instances.py +++ b/permit/api/resource_instances.py @@ -392,9 +392,9 @@ async def bulk_delete( this method raises as a ``PermitApiError`` that says so. Args: - resource_instances: The resource instance identities to delete. - Each identity can be either `resource_type:instance_key` (like Repository:react) or the - resource instance uuid. + resource_instances: The resource instance identities to delete. Each identity can + be either `resource_type:instance_key` (like Repository:react) or the resource + instance uuid. Returns: the bulk delete report. From 0e4b8260237a399f1c6d5155471ec99294294ad1 Mon Sep 17 00:00:00 2001 From: Zeev Manilovich Date: Sat, 3 Oct 2026 22:44:33 +0300 Subject: [PATCH 5/8] Fix docstring markup that Markdown renders wrongly - RolesApi.assign_permissions() and remove_permissions(), and ConditionSetRulesApi.list(): the placeholders and : 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 --- permit/_sync_types.pyi | 10 +++++----- permit/api/condition_set_rules.py | 2 +- permit/api/context.py | 1 + permit/api/roles.py | 8 ++++---- permit/utils/sync.py | 4 ++-- 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/permit/_sync_types.pyi b/permit/_sync_types.pyi index f8c85371..2d429d03 100644 --- a/permit/_sync_types.pyi +++ b/permit/_sync_types.pyi @@ -132,7 +132,7 @@ class SyncConditionSetRulesApi(BasePermitApi): Args: user_set_key: the key of the userset, if used only rules matching that userset will be fetched. - permission_key: the key of the permission, formatted as :. + permission_key: the key of the permission, formatted as `:`. if used, only rules granting that permission will be fetched. resource_set_key: the key of the resourceset, if used only rules matching that resourceset will be fetched. @@ -2416,8 +2416,8 @@ class SyncRolesApi(BasePermitApi): Args: role_key: The key of the role. - permissions: An array of permission keys () to be assigned to the - role. + permissions: An array of permission keys (``) to be assigned + to the role. Returns: A RoleRead object representing the updated role. @@ -2432,8 +2432,8 @@ class SyncRolesApi(BasePermitApi): Args: role_key: The key of the role. - permissions: An array of permission keys () to be removed from - the role. + permissions: An array of permission keys (``) to be removed + from the role. Returns: A RoleRead object representing the updated role. diff --git a/permit/api/condition_set_rules.py b/permit/api/condition_set_rules.py index f0d42320..a9330732 100644 --- a/permit/api/condition_set_rules.py +++ b/permit/api/condition_set_rules.py @@ -41,7 +41,7 @@ async def list( Args: user_set_key: the key of the userset, if used only rules matching that userset will be fetched. - permission_key: the key of the permission, formatted as :. + permission_key: the key of the permission, formatted as `:`. if used, only rules granting that permission will be fetched. resource_set_key: the key of the resourceset, if used only rules matching that resourceset will be fetched. diff --git a/permit/api/context.py b/permit/api/context.py index d8c8305d..aeefc2a7 100644 --- a/permit/api/context.py +++ b/permit/api/context.py @@ -69,6 +69,7 @@ class ApiContext: the full object hierarchy in every request. For example, in order to list roles, the user need to specify the (id or key) of the: + - the org - the project - then environment diff --git a/permit/api/roles.py b/permit/api/roles.py index fb2900ee..6948835d 100644 --- a/permit/api/roles.py +++ b/permit/api/roles.py @@ -180,8 +180,8 @@ async def assign_permissions(self, role_key: str, permissions: builtins.list[str Args: role_key: The key of the role. - permissions: An array of permission keys () to be assigned to the - role. + permissions: An array of permission keys (``) to be assigned + to the role. Returns: A RoleRead object representing the updated role. @@ -205,8 +205,8 @@ async def remove_permissions(self, role_key: str, permissions: builtins.list[str Args: role_key: The key of the role. - permissions: An array of permission keys () to be removed from - the role. + permissions: An array of permission keys (``) to be removed + from the role. Returns: A RoleRead object representing the updated role. diff --git a/permit/utils/sync.py b/permit/utils/sync.py index bca05c77..20419fce 100644 --- a/permit/utils/sync.py +++ b/permit/utils/sync.py @@ -33,10 +33,10 @@ """A coroutine function that closes the HTTP sessions of a sync client.""" SYNC_WRAPPER_MARKER = "__permit_sync_wrapper__" -"""Attribute set on every wrapper produced by :func:`async_to_sync`. +"""Attribute set on every wrapper produced by `async_to_sync`. It marks a callable as "already converted", which makes the conversion done by -:class:`SyncClass` idempotent and keeps :func:`iscoroutine_func` from walking +`SyncClass` idempotent and keeps `iscoroutine_func` from walking into the coroutine function such a wrapper consumes. """ From 19f59a0adea5e003f088e9c44fc04d65240f51b0 Mon Sep 17 00:00:00 2001 From: Zeev Manilovich Date: Sat, 3 Oct 2026 22:51:16 +0300 Subject: [PATCH 6/8] Drop the Returns type prefixes that Griffe reads as names 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 --- permit/_sync_types.pyi | 8 ++++---- permit/enforcement/enforcer.py | 8 ++++---- permit/permit.py | 14 +++++++------- permit/sync.py | 12 ++++++------ 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/permit/_sync_types.pyi b/permit/_sync_types.pyi index 2d429d03..c7e0ee4c 100644 --- a/permit/_sync_types.pyi +++ b/permit/_sync_types.pyi @@ -3079,7 +3079,7 @@ class SyncEnforcer: Defaults to None. Returns: - AuthorizedUsersResult: Contains all the authorized users and the role assignments that + Contains all the authorized users and the role assignments that granted the permission. Raises: @@ -3111,7 +3111,7 @@ class SyncEnforcer: Defaults to None. Returns: - list[bool]: A list of booleans indicating whether the user is authorized for each + A list of booleans indicating whether the user is authorized for each resource. Raises: @@ -3155,7 +3155,7 @@ class SyncEnforcer: Defaults to None. Returns: - bool: True if the user is authorized, False otherwise. + True if the user is authorized, False otherwise. Raises: PermitConnectionError: If an error occurs while sending the authorization request to the @@ -3239,7 +3239,7 @@ class SyncEnforcer: key, which is sent as the resource context of that check. Returns: - list[dict]: The subset of ``resources`` the user is authorized for, in input order. + The subset of ``resources`` the user is authorized for, in input order. """ class SyncPdpRoleAssignmentsApi(BasePdpPermitApi): diff --git a/permit/enforcement/enforcer.py b/permit/enforcement/enforcer.py index 7a316dee..b66d9efc 100644 --- a/permit/enforcement/enforcer.py +++ b/permit/enforcement/enforcer.py @@ -144,7 +144,7 @@ async def authorized_users( Defaults to None. Returns: - AuthorizedUsersResult: Contains all the authorized users and the role assignments that + Contains all the authorized users and the role assignments that granted the permission. Raises: @@ -262,7 +262,7 @@ async def bulk_check( Defaults to None. Returns: - list[bool]: A list of booleans indicating whether the user is authorized for each + A list of booleans indicating whether the user is authorized for each resource. Raises: @@ -390,7 +390,7 @@ async def check( Defaults to None. Returns: - bool: True if the user is authorized, False otherwise. + True if the user is authorized, False otherwise. Raises: PermitConnectionError: If an error occurs while sending the authorization request to the @@ -666,7 +666,7 @@ async def filter_objects( key, which is sent as the resource context of that check. Returns: - list[dict]: The subset of ``resources`` the user is authorized for, in input order. + The subset of ``resources`` the user is authorized for, in input order. """ requests: list[CheckQuery] = [] for resource in resources: diff --git a/permit/permit.py b/permit/permit.py index 3056d3bc..8462c9a5 100644 --- a/permit/permit.py +++ b/permit/permit.py @@ -176,7 +176,7 @@ def wait_for_sync( default when that is None too. Yields: - Permit: A Permit instance that is configured to wait for facts to be synced. It + A Permit instance that is configured to wait for facts to be synced. It sends its requests over this client's connections, so it needs no ``close()``: closing this client closes them, and its own ``close()`` does nothing. With ``proxy_facts_via_pdp`` off, it logs a warning and yields this client itself, @@ -257,7 +257,7 @@ async def authorized_users( Defaults to None. Returns: - AuthorizedUsersResult: Contains all the authorized users and the role assignments that + Contains all the authorized users and the role assignments that granted the permission. Raises: @@ -292,7 +292,7 @@ async def bulk_check( Defaults to None. Returns: - list[bool]: A list of booleans indicating whether the user is authorized for each + A list of booleans indicating whether the user is authorized for each resource. Raises: @@ -342,7 +342,7 @@ async def check( Defaults to None. Returns: - bool: True if the user is authorized, False otherwise. + True if the user is authorized, False otherwise. Raises: PermitConnectionError: If an error occurs while sending the authorization request to the @@ -384,7 +384,7 @@ async def get_user_permissions( either; pass ``{}`` to send the base context alone. Returns: - dict: User permissions per tenant + User permissions per tenant Raises: PermitConnectionError: If an error occurs while sending the request to the PDP @@ -414,7 +414,7 @@ async def get_user_tenants( Defaults to None. Returns: - list[TenantDetails]: The user's tenants, each with its key and attributes. Empty + The user's tenants, each with its key and attributes. Empty when the user has no tenant-level role or the PDP does not know the user. Raises: @@ -443,7 +443,7 @@ async def filter_objects( `type`, `key`, `context`, `attributes` and `tenant`. Returns: - list[dict[str, Any]]: The permitted subset of `resources`, in their original order + The permitted subset of `resources`, in their original order Raises: PermitConnectionError: If an error occurs while sending the request to the PDP diff --git a/permit/sync.py b/permit/sync.py index 47ec667c..e453d89c 100644 --- a/permit/sync.py +++ b/permit/sync.py @@ -186,7 +186,7 @@ def bulk_check( # type: ignore[override] Defaults to None. Returns: - list[bool]: A list of booleans indicating whether the user is authorized for each + A list of booleans indicating whether the user is authorized for each resource. Raises: @@ -236,7 +236,7 @@ def check( # type: ignore[override] Defaults to None. Returns: - bool: True if the user is authorized, False otherwise. + True if the user is authorized, False otherwise. Raises: PermitConnectionError: If an error occurs while sending the authorization request to the @@ -272,7 +272,7 @@ def authorized_users( # type: ignore[override] Defaults to None. Returns: - AuthorizedUsersResult: Contains all the authorized users and the role assignments that + Contains all the authorized users and the role assignments that granted the permission. Raises: @@ -315,7 +315,7 @@ def get_user_permissions( # type: ignore[override] either; pass ``{}`` to send the base context alone. Returns: - dict: User permissions per tenant + User permissions per tenant Raises: PermitConnectionError: If an error occurs while sending the request to the PDP @@ -345,7 +345,7 @@ def get_user_tenants( # type: ignore[override] Defaults to None. Returns: - list[TenantDetails]: The user's tenants, each with its key and attributes. Empty + The user's tenants, each with its key and attributes. Empty when the user has no tenant-level role or the PDP does not know the user. Raises: @@ -374,7 +374,7 @@ def filter_objects( # type: ignore[override] `type`, `key`, `context`, `attributes` and `tenant`. Returns: - list[dict[str, Any]]: The permitted subset of `resources`, in their original order + The permitted subset of `resources`, in their original order Raises: PermitConnectionError: If an error occurs while sending the request to the PDP From 3680a543db3e6546039e4c4fdec39fcd24b743e0 Mon Sep 17 00:00:00 2001 From: Zeev Manilovich Date: Sat, 3 Oct 2026 22:59:44 +0300 Subject: [PATCH 7/8] Indent the _LoopThread Returns continuation lines _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 --- permit/utils/sync.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/permit/utils/sync.py b/permit/utils/sync.py index 20419fce..967ad229 100644 --- a/permit/utils/sync.py +++ b/permit/utils/sync.py @@ -233,7 +233,7 @@ async def _track(self, coroutine: Coroutine[Any, Any, T], call_site: _CallSite) Returns: What the coroutine returns, or the exception it raises, as a `_Raised`. A - cancellation, of the task or from the coroutine, is raised. + cancellation, of the task or from the coroutine, is raised. """ # Never None: this coroutine only ever runs as a task. task = cast("asyncio.Task[Any]", asyncio.current_task()) @@ -261,7 +261,7 @@ def submit( Returns: The future of the coroutine's result, or of the exception it raised, as a - `_Raised`. + `_Raised`. Raises: RuntimeError: If the loop is closed. `coroutine` is closed, never started. From e6e72de0d2124bf85bce387770e089b6884c85e6 Mon Sep 17 00:00:00 2001 From: Zeev Manilovich Date: Sat, 3 Oct 2026 23:22:48 +0300 Subject: [PATCH 8/8] End the decimal_encoder doctest's output before its closing fence 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 --- permit/api/encoders.py | 1 + 1 file changed, 1 insertion(+) diff --git a/permit/api/encoders.py b/permit/api/encoders.py index 5e919932..d8020190 100644 --- a/permit/api/encoders.py +++ b/permit/api/encoders.py @@ -86,6 +86,7 @@ def decimal_encoder(dec_value: Decimal) -> int | float: >>> decimal_encoder(Decimal("1")) 1 + ``` """ exponent = dec_value.as_tuple().exponent