diff --git a/pyoverkiz/_case.py b/pyoverkiz/_case.py index f87448b7..2170989f 100644 --- a/pyoverkiz/_case.py +++ b/pyoverkiz/_case.py @@ -4,8 +4,10 @@ import functools import re -from collections.abc import Callable -from typing import Any +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from collections.abc import Callable _CAMEL_RE = re.compile(r"([A-Z]+)([A-Z][a-z])|([a-z\d])([A-Z])") diff --git a/pyoverkiz/action_queue.py b/pyoverkiz/action_queue.py index 104809a8..2b5e3523 100644 --- a/pyoverkiz/action_queue.py +++ b/pyoverkiz/action_queue.py @@ -4,13 +4,14 @@ import asyncio import contextlib -from collections.abc import Callable, Coroutine, Generator from dataclasses import dataclass from typing import TYPE_CHECKING, Any from pyoverkiz.models import Action if TYPE_CHECKING: + from collections.abc import Callable, Coroutine, Generator + from pyoverkiz.enums import ExecutionMode diff --git a/pyoverkiz/auth/base.py b/pyoverkiz/auth/base.py index 6131c3bc..19bac2ba 100644 --- a/pyoverkiz/auth/base.py +++ b/pyoverkiz/auth/base.py @@ -3,9 +3,11 @@ from __future__ import annotations import datetime -from collections.abc import Mapping from dataclasses import dataclass, field -from typing import Any, Protocol +from typing import TYPE_CHECKING, Any, Protocol + +if TYPE_CHECKING: + from collections.abc import Mapping @dataclass(slots=True) diff --git a/pyoverkiz/auth/factory.py b/pyoverkiz/auth/factory.py index 4ee91425..2c2fda4e 100644 --- a/pyoverkiz/auth/factory.py +++ b/pyoverkiz/auth/factory.py @@ -2,9 +2,7 @@ from __future__ import annotations -import ssl - -from aiohttp import ClientSession +from typing import TYPE_CHECKING from pyoverkiz.auth.credentials import ( Credentials, @@ -24,7 +22,13 @@ SomfyAuthStrategy, ) from pyoverkiz.enums import APIType, Server -from pyoverkiz.models import ServerConfig + +if TYPE_CHECKING: + from ssl import SSLContext + + from aiohttp import ClientSession + + from pyoverkiz.models import ServerConfig def build_auth_strategy( @@ -32,7 +36,7 @@ def build_auth_strategy( server_config: ServerConfig, credentials: Credentials, session: ClientSession, - ssl_context: ssl.SSLContext | bool, + ssl_context: SSLContext | bool, ) -> AuthStrategy: """Build the correct auth strategy for the given server and credentials.""" server: Server | None = server_config.server diff --git a/pyoverkiz/auth/strategies.py b/pyoverkiz/auth/strategies.py index b5a0e3c7..d7d85d87 100644 --- a/pyoverkiz/auth/strategies.py +++ b/pyoverkiz/auth/strategies.py @@ -6,23 +6,26 @@ import base64 import binascii import json -import ssl -from collections.abc import Mapping from http import HTTPStatus from typing import TYPE_CHECKING, Any, cast if TYPE_CHECKING: + from collections.abc import Mapping + from ssl import SSLContext + from botocore.client import BaseClient + from pyoverkiz.auth.credentials import ( + LocalTokenCredentials, + RexelOAuthCodeCredentials, + TokenCredentials, + UsernamePasswordCredentials, + ) + from pyoverkiz.models import ServerConfig + from aiohttp import ClientSession, FormData from pyoverkiz.auth.base import AuthContext, AuthStrategy -from pyoverkiz.auth.credentials import ( - LocalTokenCredentials, - RexelOAuthCodeCredentials, - TokenCredentials, - UsernamePasswordCredentials, -) from pyoverkiz.const import ( COZYTOUCH_ATLANTIC_API, COZYTOUCH_CLIENT_ID, @@ -48,7 +51,6 @@ SomfyBadCredentialsError, SomfyServiceError, ) -from pyoverkiz.models import ServerConfig MIN_JWT_SEGMENTS = 2 @@ -60,7 +62,7 @@ def __init__( self, session: ClientSession, server: ServerConfig, - ssl_context: ssl.SSLContext | bool, + ssl_context: SSLContext | bool, ) -> None: """Store shared auth context for Overkiz API interactions.""" self.session = session @@ -92,7 +94,7 @@ def __init__( credentials: UsernamePasswordCredentials, session: ClientSession, server: ServerConfig, - ssl_context: ssl.SSLContext | bool, + ssl_context: SSLContext | bool, ) -> None: """Create a session-login strategy bound to the given credentials.""" super().__init__(session, server, ssl_context) @@ -135,7 +137,7 @@ def __init__( credentials: UsernamePasswordCredentials, session: ClientSession, server: ServerConfig, - ssl_context: ssl.SSLContext | bool, + ssl_context: SSLContext | bool, ) -> None: """Create a Somfy OAuth2 strategy with a fresh auth context.""" super().__init__(session, server, ssl_context) @@ -298,7 +300,7 @@ def __init__( credentials: LocalTokenCredentials, session: ClientSession, server: ServerConfig, - ssl_context: ssl.SSLContext | bool, + ssl_context: SSLContext | bool, ) -> None: """Create a local-token strategy bound to the given credentials.""" super().__init__(session, server, ssl_context) @@ -322,7 +324,7 @@ def __init__( credentials: RexelOAuthCodeCredentials, session: ClientSession, server: ServerConfig, - ssl_context: ssl.SSLContext | bool, + ssl_context: SSLContext | bool, ) -> None: """Create a Rexel OAuth2 strategy with a fresh auth context.""" super().__init__(session, server, ssl_context) @@ -406,7 +408,7 @@ def __init__( credentials: TokenCredentials, session: ClientSession, server: ServerConfig, - ssl_context: ssl.SSLContext | bool, + ssl_context: SSLContext | bool, ) -> None: """Create a bearer-token strategy bound to the given credentials.""" super().__init__(session, server, ssl_context) diff --git a/pyoverkiz/client.py b/pyoverkiz/client.py index 3020daf2..39d99463 100644 --- a/pyoverkiz/client.py +++ b/pyoverkiz/client.py @@ -7,8 +7,7 @@ import urllib.parse from http import HTTPStatus from pathlib import Path -from types import TracebackType -from typing import Any, Self, cast +from typing import TYPE_CHECKING, Any, Self, cast import backoff from aiohttp import ( @@ -17,7 +16,6 @@ ClientSession, ServerDisconnectedError, ) -from backoff.types import Details from pyoverkiz._case import decamelize from pyoverkiz.action_queue import ActionQueue, ActionQueueSettings @@ -56,7 +54,13 @@ from pyoverkiz.obfuscate import obfuscate_sensitive_data from pyoverkiz.response_handler import check_response from pyoverkiz.serializers import prepare_payload -from pyoverkiz.types import JSON + +if TYPE_CHECKING: + from types import TracebackType + + from backoff.types import Details + + from pyoverkiz.types import JSON _LOGGER = logging.getLogger(__name__) diff --git a/pyoverkiz/enums/base.py b/pyoverkiz/enums/base.py index 83c075bc..3d2fc01c 100644 --- a/pyoverkiz/enums/base.py +++ b/pyoverkiz/enums/base.py @@ -36,4 +36,4 @@ def _missing_(cls, value: object) -> Self: # type: ignore[override] message = cls.__missing_message__ logging.getLogger(cls.__module__).warning(message, value, cls) # Type checker cannot infer UNKNOWN exists on Self, but all subclasses define it - return cast(Self, cls.UNKNOWN) # type: ignore[attr-defined] + return cast(Self, cls.UNKNOWN) # type: ignore[attr-defined] # ty: ignore[unresolved-attribute] diff --git a/pyoverkiz/models.py b/pyoverkiz/models.py index 0d9a38be..0d3d7f1a 100644 --- a/pyoverkiz/models.py +++ b/pyoverkiz/models.py @@ -4,8 +4,7 @@ import json import re -from collections.abc import Iterator -from typing import Any, cast +from typing import TYPE_CHECKING, Any, cast from attr import define, field @@ -24,12 +23,15 @@ UpdateBoxStatus, UpdateCriticityLevel, ) -from pyoverkiz.enums.command import OverkizCommand, OverkizCommandParam +from pyoverkiz.enums.command import OverkizCommand, OverkizCommandParam # noqa: TC001 from pyoverkiz.enums.protocol import Protocol -from pyoverkiz.enums.server import APIType, Server +from pyoverkiz.enums.server import APIType, Server # noqa: TC001 from pyoverkiz.obfuscate import obfuscate_email, obfuscate_id, obfuscate_string from pyoverkiz.types import DATA_TYPE_TO_PYTHON, StateType +if TYPE_CHECKING: + from collections.abc import Iterator + # --------------------------------------------------------------------------- # State & command primitives # --------------------------------------------------------------------------- diff --git a/pyoverkiz/obfuscate.py b/pyoverkiz/obfuscate.py index ca26abd6..0831bad8 100644 --- a/pyoverkiz/obfuscate.py +++ b/pyoverkiz/obfuscate.py @@ -3,9 +3,10 @@ from __future__ import annotations import re -from typing import Any +from typing import TYPE_CHECKING, Any -from pyoverkiz.types import JSON +if TYPE_CHECKING: + from pyoverkiz.types import JSON def obfuscate_id(id: str | None) -> str: diff --git a/pyoverkiz/response_handler.py b/pyoverkiz/response_handler.py index dbeb918c..66ea0952 100644 --- a/pyoverkiz/response_handler.py +++ b/pyoverkiz/response_handler.py @@ -4,8 +4,7 @@ from http import HTTPStatus from json import JSONDecodeError - -from aiohttp import ClientResponse +from typing import TYPE_CHECKING from pyoverkiz.exceptions import ( AccessDeniedToGatewayError, @@ -40,6 +39,9 @@ UnsupportedOperationError, ) +if TYPE_CHECKING: + from aiohttp import ClientResponse + # Primary dispatch: (errorCode, message_substring) -> error class. # Checked in order; first match wins. Use errorCode as the primary key to # reduce brittleness across cloud vs. local API variants. diff --git a/pyoverkiz/types.py b/pyoverkiz/types.py index 167f5951..fdd47498 100644 --- a/pyoverkiz/types.py +++ b/pyoverkiz/types.py @@ -3,11 +3,13 @@ from __future__ import annotations import json -from collections.abc import Callable -from typing import Any +from typing import TYPE_CHECKING, Any from pyoverkiz.enums import DataType +if TYPE_CHECKING: + from collections.abc import Callable + StateType = str | int | float | bool | dict[str, Any] | list[Any] | None diff --git a/pyproject.toml b/pyproject.toml index bcc26ad5..24bfaed4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -125,6 +125,8 @@ select = [ "A", # pylint "PL", + # flake8-type-checking + "TC", ] ignore = [ "E501", # Line too long @@ -133,6 +135,7 @@ ignore = [ "PLR0913", # Too many arguments — expected for API model constructors "PLC0415", # Import not at top level — intentional lazy imports "PLR0911", # Too many return statements — acceptable in factory functions + "TC006", # Quoting cast() types hurts IDE autocomplete for no real benefit ] [tool.ruff.lint.per-file-ignores] diff --git a/utils/generate_enums.py b/utils/generate_enums.py index 3b88d9be..3e74b0e4 100644 --- a/utils/generate_enums.py +++ b/utils/generate_enums.py @@ -12,13 +12,15 @@ import re import subprocess from pathlib import Path -from typing import cast +from typing import TYPE_CHECKING from pyoverkiz.auth.credentials import UsernamePasswordCredentials from pyoverkiz.client import OverkizClient from pyoverkiz.enums import Server from pyoverkiz.exceptions import OverkizError -from pyoverkiz.models import UIProfileDefinition, ValuePrototype + +if TYPE_CHECKING: + from pyoverkiz.models import UIProfileDefinition, ValuePrototype # Hardcoded protocols that may not be available on all servers # Each tuple contains: name, prefix, id, label @@ -129,8 +131,8 @@ async def generate_ui_enums(server: Server) -> None: ) as client: await client.login() - ui_classes = cast(list[str], await client.get_reference_ui_classes()) - ui_widgets = cast(list[str], await client.get_reference_ui_widgets()) + ui_classes = await client.get_reference_ui_classes() + ui_widgets = await client.get_reference_ui_widgets() # Convert camelCase to SCREAMING_SNAKE_CASE for enum names def to_enum_name(value: str) -> str: @@ -210,7 +212,7 @@ def to_enum_name(value: str) -> str: lines.append("") # End with newline # Fetch and add UI classifiers - ui_classifiers = cast(list[str], await client.get_reference_ui_classifiers()) + ui_classifiers = await client.get_reference_ui_classifiers() lines.append("") lines.append("@unique")