From 33a91598f2c92f3d6baba026f6f29b4fadbf753b Mon Sep 17 00:00:00 2001 From: Vlada Dusek Date: Thu, 11 Jun 2026 14:59:02 +0200 Subject: [PATCH] fix: Preserve decorated symbol types in docs_group and docs_name --- src/apify/_utils.py | 8 ++++---- tests/unit/test_utils.py | 30 +++++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/apify/_utils.py b/src/apify/_utils.py index 23a75e67..ca099f70 100644 --- a/src/apify/_utils.py +++ b/src/apify/_utils.py @@ -85,7 +85,7 @@ def is_running_in_ipython() -> bool: ] -def docs_group(group_name: GroupName) -> Callable: # noqa: ARG001 +def docs_group(group_name: GroupName) -> Callable[[T], T]: # noqa: ARG001 """Mark a symbol for rendering and grouping in documentation. This decorator is used solely for documentation purposes and does not modify the behavior @@ -98,13 +98,13 @@ def docs_group(group_name: GroupName) -> Callable: # noqa: ARG001 The original callable without modification. """ - def wrapper(func: Callable) -> Callable: + def wrapper(func: T) -> T: return func return wrapper -def docs_name(symbol_name: str) -> Callable: # noqa: ARG001 +def docs_name(symbol_name: str) -> Callable[[T], T]: # noqa: ARG001 """Rename a symbol for documentation rendering. This decorator modifies only the displayed name of the symbol in the generated documentation @@ -117,7 +117,7 @@ def docs_name(symbol_name: str) -> Callable: # noqa: ARG001 The original callable without modification. """ - def wrapper(func: Callable) -> Callable: + def wrapper(func: T) -> T: return func return wrapper diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 5b823bb2..526fd5d2 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -1,11 +1,12 @@ from __future__ import annotations from enum import Enum +from typing import TYPE_CHECKING from unittest.mock import patch import pytest -from apify._utils import get_system_info, is_running_in_ipython, maybe_extract_enum_member_value +from apify._utils import docs_group, docs_name, get_system_info, is_running_in_ipython, maybe_extract_enum_member_value def test_ipython_detection_when_active() -> None: @@ -54,3 +55,30 @@ def test_maybe_extract_enum_member_value_with_non_enum() -> None: assert maybe_extract_enum_member_value('hello') == 'hello' assert maybe_extract_enum_member_value(42) == 42 assert maybe_extract_enum_member_value(None) is None + + +if TYPE_CHECKING: + # Regression guard: if `docs_group`/`docs_name` stop being identity-typed (`Callable[[T], T]`), + # the decorated classes degrade to `Unknown`, the accesses below stop erroring, and the + # then-unused suppressions fail the type check (`unused-ignore-comment = "error"`). + + @docs_group('Actor') + class _DocsGroupDecorated: + pass + + @docs_name('Renamed') + class _DocsNameDecorated: + pass + + _ = _DocsGroupDecorated.nonexistent # ty: ignore[unresolved-attribute] + _ = _DocsNameDecorated.nonexistent # ty: ignore[unresolved-attribute] + + +def test_docs_decorators_return_original_object() -> None: + """Test that `docs_group` and `docs_name` return the decorated object unchanged.""" + + class Sample: + pass + + assert docs_group('Actor')(Sample) is Sample + assert docs_name('Renamed')(Sample) is Sample