From 856c030cf3f91ac134832d37de4403c1da0d6875 Mon Sep 17 00:00:00 2001 From: Mike Date: Sun, 7 Jun 2026 17:21:21 +0100 Subject: [PATCH 1/2] chore: require Python 3.10 and modernise type hints - raise the floor to Python 3.10 (setup.py, CI matrices, black target, CONTRIBUTING); 3.9 is end-of-life - replace Optional[X]/Union[...] with X | None / X | Y throughout - use builtin generics (list/dict/tuple/type) instead of typing.List etc - drop the Optional* wrapper aliases from utils.typing and inline at use sites; keep the domain aliases (JsonType, BasicAuthType, XmlType, ResponseType), modernised - fix retry_if_api_request_error status_codes default to list[int] | None --- .github/workflows/test.yml | 2 +- .github/workflows/test_and_deploy.yml | 2 +- CONTRIBUTING.md | 2 +- apiclient/authentication_methods.py | 14 +++++----- apiclient/client.py | 38 +++++++++++++-------------- apiclient/exceptions.py | 7 ++--- apiclient/request_formatters.py | 8 +++--- apiclient/request_strategies.py | 32 +++++++++++----------- apiclient/response_handlers.py | 5 ++-- apiclient/retrying.py | 3 +-- apiclient/utils/typing.py | 11 +++----- pyproject.toml | 2 +- setup.py | 3 +-- 13 files changed, 59 insertions(+), 70 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 75fd18e..dc47c85 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,7 +29,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v6 diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index 2f6203d..6828618 100644 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -12,7 +12,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] steps: - uses: actions/checkout@v6 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f6e146e..5ab50b8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ ## Development setup -The library supports Python 3.9 to 3.14. Create a virtual environment and +The library supports Python 3.10 to 3.14. Create a virtual environment and install the package with its development dependencies: ```bash diff --git a/apiclient/authentication_methods.py b/apiclient/authentication_methods.py index f7a9159..cb0bad0 100644 --- a/apiclient/authentication_methods.py +++ b/apiclient/authentication_methods.py @@ -1,7 +1,7 @@ import http.cookiejar -from typing import TYPE_CHECKING, Dict, Optional, Union +from typing import TYPE_CHECKING -from apiclient.utils.typing import BasicAuthType, OptionalStr +from apiclient.utils.typing import BasicAuthType if TYPE_CHECKING: # pragma: no cover # Stupid way of getting around cyclic imports when @@ -16,7 +16,7 @@ def get_headers(self) -> dict: def get_query_params(self) -> dict: return {} - def get_username_password_authentication(self) -> Optional[BasicAuthType]: + def get_username_password_authentication(self) -> BasicAuthType | None: return None def perform_initial_auth(self, client: "APIClient"): @@ -51,15 +51,15 @@ def __init__( self, token: str, parameter: str = "Authorization", - scheme: OptionalStr = "Bearer", - extra: Optional[Dict[str, str]] = None, + scheme: str | None = "Bearer", + extra: dict[str, str] | None = None, ): self._token = token self._parameter = parameter self._scheme = scheme self._extra = extra - def get_headers(self) -> Dict[str, str]: + def get_headers(self) -> dict[str, str]: if self._scheme: headers = {self._parameter: f"{self._scheme} {self._token}"} else: @@ -86,7 +86,7 @@ class CookieAuthentication(BaseAuthenticationMethod): def __init__( self, auth_url: str, - authentication: Union[HeaderAuthentication, QueryParameterAuthentication, BasicAuthentication], + authentication: HeaderAuthentication | QueryParameterAuthentication | BasicAuthentication, ): self._auth_url = auth_url self._authentication = authentication diff --git a/apiclient/client.py b/apiclient/client.py index bdb8609..b051263 100644 --- a/apiclient/client.py +++ b/apiclient/client.py @@ -1,13 +1,13 @@ import logging from copy import copy -from typing import Any, Optional, Type +from typing import Any from apiclient.authentication_methods import BaseAuthenticationMethod, NoAuthentication from apiclient.error_handlers import BaseErrorHandler, ErrorHandler from apiclient.request_formatters import BaseRequestFormatter, NoOpRequestFormatter from apiclient.request_strategies import BaseRequestStrategy, RequestStrategy from apiclient.response_handlers import BaseResponseHandler, RequestsResponseHandler -from apiclient.utils.typing import JsonType, OptionalDict +from apiclient.utils.typing import JsonType LOG = logging.getLogger(__name__) @@ -18,11 +18,11 @@ class APIClient: def __init__( self, - authentication_method: Optional[BaseAuthenticationMethod] = None, - response_handler: Type[BaseResponseHandler] = RequestsResponseHandler, - request_formatter: Type[BaseRequestFormatter] = NoOpRequestFormatter, - error_handler: Type[BaseErrorHandler] = ErrorHandler, - request_strategy: Optional[BaseRequestStrategy] = None, + authentication_method: BaseAuthenticationMethod | None = None, + response_handler: type[BaseResponseHandler] = RequestsResponseHandler, + request_formatter: type[BaseRequestFormatter] = NoOpRequestFormatter, + error_handler: type[BaseErrorHandler] = ErrorHandler, + request_strategy: BaseRequestStrategy | None = None, ): # Set default values self._default_headers = {} @@ -58,26 +58,26 @@ def set_authentication_method(self, authentication_method: BaseAuthenticationMet def get_authentication_method(self) -> BaseAuthenticationMethod: return self._authentication_method - def get_response_handler(self) -> Type[BaseResponseHandler]: + def get_response_handler(self) -> type[BaseResponseHandler]: return self._response_handler - def set_response_handler(self, response_handler: Type[BaseResponseHandler]): + def set_response_handler(self, response_handler: type[BaseResponseHandler]): if not (response_handler and issubclass(response_handler, BaseResponseHandler)): raise RuntimeError("provided response_handler must be a subclass of BaseResponseHandler.") self._response_handler = response_handler - def get_error_handler(self) -> Type[BaseErrorHandler]: + def get_error_handler(self) -> type[BaseErrorHandler]: return self._error_handler - def set_error_handler(self, error_handler: Type[BaseErrorHandler]): + def set_error_handler(self, error_handler: type[BaseErrorHandler]): if not (error_handler and issubclass(error_handler, BaseErrorHandler)): raise RuntimeError("provided error_handler must be a subclass of BaseErrorHandler.") self._error_handler = error_handler - def get_request_formatter(self) -> Type[BaseRequestFormatter]: + def get_request_formatter(self) -> type[BaseRequestFormatter]: return self._request_formatter - def set_request_formatter(self, request_formatter: Type[BaseRequestFormatter]): + def set_request_formatter(self, request_formatter: type[BaseRequestFormatter]): if not (request_formatter and issubclass(request_formatter, BaseRequestFormatter)): raise RuntimeError("provided request_formatter must be a subclass of BaseRequestFormatter.") self._request_formatter = request_formatter @@ -100,7 +100,7 @@ def get_default_headers(self) -> dict: def get_default_query_params(self) -> dict: return self._authentication_method.get_query_params() - def get_default_username_password_authentication(self) -> Optional[tuple]: + def get_default_username_password_authentication(self) -> tuple | None: return self._authentication_method.get_username_password_authentication() def get_request_timeout(self) -> float: @@ -111,27 +111,27 @@ def clone(self): """Enable Prototype pattern on client.""" return copy(self) - def post(self, endpoint: str, data: JsonType, params: OptionalDict = None, **kwargs): + def post(self, endpoint: str, data: JsonType, params: dict | None = None, **kwargs): """Send data and return response data from POST endpoint.""" LOG.debug("POST %s with %s", endpoint, data) return self.get_request_strategy().post(endpoint, data=data, params=params, **kwargs) - def get(self, endpoint: str, params: OptionalDict = None, **kwargs): + def get(self, endpoint: str, params: dict | None = None, **kwargs): """Return response data from GET endpoint.""" LOG.debug("GET %s", endpoint) return self.get_request_strategy().get(endpoint, params=params, **kwargs) - def put(self, endpoint: str, data: JsonType, params: OptionalDict = None, **kwargs): + def put(self, endpoint: str, data: JsonType, params: dict | None = None, **kwargs): """Send data to overwrite resource and return response data from PUT endpoint.""" LOG.debug("PUT %s with %s", endpoint, data) return self.get_request_strategy().put(endpoint, data=data, params=params, **kwargs) - def patch(self, endpoint: str, data: JsonType, params: OptionalDict = None, **kwargs): + def patch(self, endpoint: str, data: JsonType, params: dict | None = None, **kwargs): """Send data to update resource and return response data from PATCH endpoint.""" LOG.debug("PATCH %s with %s", endpoint, data) return self.get_request_strategy().patch(endpoint, data=data, params=params, **kwargs) - def delete(self, endpoint: str, params: OptionalDict = None, **kwargs): + def delete(self, endpoint: str, params: dict | None = None, **kwargs): """Remove resource with DELETE endpoint.""" LOG.debug("DELETE %s", endpoint) return self.get_request_strategy().delete(endpoint, params=params, **kwargs) diff --git a/apiclient/exceptions.py b/apiclient/exceptions.py index 6f98b70..3303376 100644 --- a/apiclient/exceptions.py +++ b/apiclient/exceptions.py @@ -1,6 +1,3 @@ -from apiclient.utils.typing import OptionalInt - - class APIClientError(Exception): """General exception to denote that something went wrong when using the client. @@ -19,10 +16,10 @@ class APIRequestError(APIClientError): """Exception to denote that something went wrong when making the request.""" message: str = "" - status_code: OptionalInt = None + status_code: int | None = None info: str = "" - def __init__(self, message: str = "", status_code: OptionalInt = None, info: str = ""): + def __init__(self, message: str = "", status_code: int | None = None, info: str = ""): self.message = self.message or message self.status_code = self.status_code or status_code self.info = self.info or info diff --git a/apiclient/request_formatters.py b/apiclient/request_formatters.py index 1c2e34a..72ea087 100644 --- a/apiclient/request_formatters.py +++ b/apiclient/request_formatters.py @@ -1,6 +1,6 @@ import json -from apiclient.utils.typing import OptionalJsonType, OptionalStr +from apiclient.utils.typing import JsonType class BaseRequestFormatter: @@ -16,7 +16,7 @@ def get_headers(cls) -> dict: return {} @classmethod - def format(cls, data: OptionalJsonType): + def format(cls, data: JsonType | None): raise NotImplementedError @@ -24,7 +24,7 @@ class NoOpRequestFormatter(BaseRequestFormatter): """No action request formatter.""" @classmethod - def format(cls, data: OptionalJsonType) -> OptionalJsonType: + def format(cls, data: JsonType | None) -> JsonType | None: return data @@ -34,6 +34,6 @@ class JsonRequestFormatter(BaseRequestFormatter): content_type = "application/json" @classmethod - def format(cls, data: OptionalJsonType) -> OptionalStr: + def format(cls, data: JsonType | None) -> str | None: if data: return json.dumps(data) diff --git a/apiclient/request_strategies.py b/apiclient/request_strategies.py index 88ee760..9433d25 100644 --- a/apiclient/request_strategies.py +++ b/apiclient/request_strategies.py @@ -5,7 +5,7 @@ from apiclient.exceptions import UnexpectedError from apiclient.response import RequestsResponse, Response -from apiclient.utils.typing import JsonType, OptionalDict, OptionalJsonType +from apiclient.utils.typing import JsonType if TYPE_CHECKING: # pragma: no cover # Stupid way of getting around cyclic imports when @@ -51,23 +51,23 @@ def get_session(self): def set_session(self, session: requests.Session): self.get_client().set_session(session) - def post(self, endpoint: str, data: JsonType, params: OptionalDict = None, **kwargs): + def post(self, endpoint: str, data: JsonType, params: dict | None = None, **kwargs): """Send data and return response data from POST endpoint.""" return self._make_request(self.get_session().post, endpoint, data=data, params=params, **kwargs) - def get(self, endpoint: str, params: OptionalDict = None, **kwargs): + def get(self, endpoint: str, params: dict | None = None, **kwargs): """Return response data from GET endpoint.""" return self._make_request(self.get_session().get, endpoint, params=params, **kwargs) - def put(self, endpoint: str, data: JsonType, params: OptionalDict = None, **kwargs): + def put(self, endpoint: str, data: JsonType, params: dict | None = None, **kwargs): """Send data to overwrite resource and return response data from PUT endpoint.""" return self._make_request(self.get_session().put, endpoint, data=data, params=params, **kwargs) - def patch(self, endpoint: str, data: JsonType, params: OptionalDict = None, **kwargs): + def patch(self, endpoint: str, data: JsonType, params: dict | None = None, **kwargs): """Send data to update resource and return response data from PATCH endpoint.""" return self._make_request(self.get_session().patch, endpoint, data=data, params=params, **kwargs) - def delete(self, endpoint: str, params: OptionalDict = None, **kwargs): + def delete(self, endpoint: str, params: dict | None = None, **kwargs): """Remove resource with DELETE endpoint.""" return self._make_request(self.get_session().delete, endpoint, params=params, **kwargs) @@ -75,9 +75,9 @@ def _make_request( self, request_method: Callable, endpoint: str, - params: OptionalDict = None, - headers: OptionalDict = None, - data: OptionalJsonType = None, + params: dict | None = None, + headers: dict | None = None, + data: JsonType | None = None, **kwargs, ) -> Response: """Make the request with the given method. @@ -102,14 +102,14 @@ def _make_request( self._check_response(response) return self._decode_response_data(response) - def _get_request_params(self, params: OptionalDict) -> dict: + def _get_request_params(self, params: dict | None) -> dict: """Return dictionary with any additional authentication query parameters.""" if params is None: params = {} params.update(self.get_client().get_default_query_params()) return params - def _get_request_headers(self, headers: OptionalDict) -> dict: + def _get_request_headers(self, headers: dict | None) -> dict: """Return dictionary with any additional authentication headers.""" if headers is None: headers = {} @@ -119,7 +119,7 @@ def _get_request_headers(self, headers: OptionalDict) -> dict: def _get_username_password_authentication(self): return self.get_client().get_default_username_password_authentication() - def _get_formatted_data(self, data: OptionalDict): + def _get_formatted_data(self, data: JsonType | None): return self.get_client().get_request_formatter().format(data) def _get_request_timeout(self) -> float: @@ -146,7 +146,7 @@ class QueryParamPaginatedRequestStrategy(RequestStrategy): def __init__(self, next_page: Callable): self._next_page = next_page - def get(self, endpoint: str, params: OptionalDict = None, **kwargs): + def get(self, endpoint: str, params: dict | None = None, **kwargs): if params is None: params = {} @@ -168,7 +168,7 @@ def get(self, endpoint: str, params: OptionalDict = None, **kwargs): return pages - def get_next_page_params(self, response, previous_page_params: dict) -> OptionalDict: + def get_next_page_params(self, response, previous_page_params: dict) -> dict | None: return self._next_page(response, previous_page_params) @@ -178,7 +178,7 @@ class UrlPaginatedRequestStrategy(RequestStrategy): def __init__(self, next_page: Callable): self._next_page = next_page - def get(self, endpoint: str, params: OptionalDict = None, **kwargs): + def get(self, endpoint: str, params: dict | None = None, **kwargs): pages = [] while endpoint: response = super().get(endpoint, params=params, **kwargs) @@ -190,5 +190,5 @@ def get(self, endpoint: str, params: OptionalDict = None, **kwargs): return pages - def get_next_page_url(self, response, previous_page_url: str) -> OptionalDict: + def get_next_page_url(self, response, previous_page_url: str) -> dict | None: return self._next_page(response, previous_page_url) diff --git a/apiclient/response_handlers.py b/apiclient/response_handlers.py index de2cdaf..a5bc35b 100644 --- a/apiclient/response_handlers.py +++ b/apiclient/response_handlers.py @@ -1,5 +1,4 @@ from json import JSONDecodeError -from typing import Optional from xml.etree import ElementTree import requests @@ -29,7 +28,7 @@ class JsonResponseHandler(BaseResponseHandler): """Attempt to return the decoded response data as json.""" @staticmethod - def get_request_data(response: Response) -> Optional[JsonType]: + def get_request_data(response: Response) -> JsonType | None: if response.get_raw_data() == "": return None @@ -46,7 +45,7 @@ class XmlResponseHandler(BaseResponseHandler): """Attempt to return the decoded response to an xml Element.""" @staticmethod - def get_request_data(response: Response) -> Optional[XmlType]: + def get_request_data(response: Response) -> XmlType | None: if response.get_raw_data() == "": return None diff --git a/apiclient/retrying.py b/apiclient/retrying.py index 6efbec4..7fb4081 100644 --- a/apiclient/retrying.py +++ b/apiclient/retrying.py @@ -1,5 +1,4 @@ import random -from typing import List import tenacity @@ -18,7 +17,7 @@ class retry_if_api_request_error(tenacity.retry_if_exception): * status codes >= 500 codes will be retried. """ - def __init__(self, status_codes: List[int] = None): + def __init__(self, status_codes: list[int] | None = None): self._status_codes = status_codes super().__init__(self._retry_if) diff --git a/apiclient/utils/typing.py b/apiclient/utils/typing.py index ddcc385..6e7e706 100644 --- a/apiclient/utils/typing.py +++ b/apiclient/utils/typing.py @@ -1,13 +1,8 @@ -from typing import Optional, Tuple, Union from xml.etree import ElementTree from requests import Response -OptionalDict = Optional[dict] -OptionalStr = Optional[str] -OptionalInt = Optional[int] -BasicAuthType = Tuple[str, str] -JsonType = Union[str, list, dict] -OptionalJsonType = Optional[JsonType] +BasicAuthType = tuple[str, str] +JsonType = str | list | dict XmlType = ElementTree.Element -ResponseType = Union[JsonType, XmlType, Response] +ResponseType = JsonType | XmlType | Response diff --git a/pyproject.toml b/pyproject.toml index d6ba3f1..d6728c3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,4 +4,4 @@ build-backend = "setuptools.build_meta" [tool.black] line-length=109 -target-version=['py39', 'py310', 'py311', 'py312', 'py313', 'py314'] +target-version=['py310', 'py311', 'py312', 'py313', 'py314'] diff --git a/setup.py b/setup.py index 00d6d13..66b836b 100644 --- a/setup.py +++ b/setup.py @@ -29,11 +29,10 @@ author="Mike Wooster", author_email="", url="https://github.com/MikeWooster/api-client", - python_requires=">=3.9", + python_requires=">=3.10", packages=["apiclient"], classifiers=[ "Development Status :: 5 - Production/Stable", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", From 685d8e6d797e01c582d58b5c600c5cc945144b1a Mon Sep 17 00:00:00 2001 From: Mike Date: Sun, 7 Jun 2026 18:28:39 +0100 Subject: [PATCH 2/2] refactor: defer type-only imports into TYPE_CHECKING blocks - add 'from __future__ import annotations' so annotations are lazy, then move every type-only import (BasicAuthType, JsonType, XmlType, Callable, Any, requests, and cross-module classes used only in hints) under TYPE_CHECKING - omit the now runtime-dead utils/typing.py (pure type-alias module, imported only under TYPE_CHECKING) from coverage - make paginated()'s callable args optional (Callable | None = None) --- apiclient/authentication_methods.py | 6 ++---- apiclient/client.py | 9 +++++++-- apiclient/error_handlers.py | 7 ++++++- apiclient/paginators.py | 18 ++++++++++-------- apiclient/request_formatters.py | 5 ++++- apiclient/request_strategies.py | 12 +++++++----- apiclient/response.py | 10 +++++++--- apiclient/response_handlers.py | 12 ++++++++---- setup.cfg | 4 ++++ 9 files changed, 55 insertions(+), 28 deletions(-) diff --git a/apiclient/authentication_methods.py b/apiclient/authentication_methods.py index cb0bad0..69c285c 100644 --- a/apiclient/authentication_methods.py +++ b/apiclient/authentication_methods.py @@ -1,12 +1,10 @@ +from __future__ import annotations import http.cookiejar from typing import TYPE_CHECKING -from apiclient.utils.typing import BasicAuthType - if TYPE_CHECKING: # pragma: no cover - # Stupid way of getting around cyclic imports when - # using typehinting. from apiclient import APIClient + from apiclient.utils.typing import BasicAuthType class BaseAuthenticationMethod: diff --git a/apiclient/client.py b/apiclient/client.py index b051263..2b8b94f 100644 --- a/apiclient/client.py +++ b/apiclient/client.py @@ -1,13 +1,18 @@ +from __future__ import annotations import logging from copy import copy -from typing import Any +from typing import TYPE_CHECKING from apiclient.authentication_methods import BaseAuthenticationMethod, NoAuthentication from apiclient.error_handlers import BaseErrorHandler, ErrorHandler from apiclient.request_formatters import BaseRequestFormatter, NoOpRequestFormatter from apiclient.request_strategies import BaseRequestStrategy, RequestStrategy from apiclient.response_handlers import BaseResponseHandler, RequestsResponseHandler -from apiclient.utils.typing import JsonType + +if TYPE_CHECKING: # pragma: no cover + from typing import Any + + from apiclient.utils.typing import JsonType LOG = logging.getLogger(__name__) diff --git a/apiclient/error_handlers.py b/apiclient/error_handlers.py index 6c287cb..8f9f00b 100644 --- a/apiclient/error_handlers.py +++ b/apiclient/error_handlers.py @@ -1,5 +1,10 @@ +from __future__ import annotations +from typing import TYPE_CHECKING + from apiclient import exceptions -from apiclient.response import Response + +if TYPE_CHECKING: # pragma: no cover + from apiclient.response import Response class BaseErrorHandler: diff --git a/apiclient/paginators.py b/apiclient/paginators.py index db550b3..3e635b2 100644 --- a/apiclient/paginators.py +++ b/apiclient/paginators.py @@ -1,13 +1,15 @@ +from __future__ import annotations from contextlib import contextmanager from functools import wraps -from typing import Callable +from typing import TYPE_CHECKING -from apiclient.client import APIClient -from apiclient.request_strategies import ( - BaseRequestStrategy, - QueryParamPaginatedRequestStrategy, - UrlPaginatedRequestStrategy, -) +from apiclient.request_strategies import QueryParamPaginatedRequestStrategy, UrlPaginatedRequestStrategy + +if TYPE_CHECKING: # pragma: no cover + from typing import Callable + + from apiclient.client import APIClient + from apiclient.request_strategies import BaseRequestStrategy @contextmanager @@ -21,7 +23,7 @@ def set_strategy(client: APIClient, strategy: BaseRequestStrategy): del temporary_client -def paginated(by_query_params: Callable = None, by_url: Callable = None): +def paginated(by_query_params: Callable | None = None, by_url: Callable | None = None): """Decorator to signal that the page is paginated.""" if by_query_params: strategy = QueryParamPaginatedRequestStrategy(by_query_params) diff --git a/apiclient/request_formatters.py b/apiclient/request_formatters.py index 72ea087..d01d1b7 100644 --- a/apiclient/request_formatters.py +++ b/apiclient/request_formatters.py @@ -1,6 +1,9 @@ +from __future__ import annotations import json +from typing import TYPE_CHECKING -from apiclient.utils.typing import JsonType +if TYPE_CHECKING: # pragma: no cover + from apiclient.utils.typing import JsonType class BaseRequestFormatter: diff --git a/apiclient/request_strategies.py b/apiclient/request_strategies.py index 9433d25..98dfb15 100644 --- a/apiclient/request_strategies.py +++ b/apiclient/request_strategies.py @@ -1,16 +1,18 @@ +from __future__ import annotations from copy import deepcopy -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING import requests from apiclient.exceptions import UnexpectedError -from apiclient.response import RequestsResponse, Response -from apiclient.utils.typing import JsonType +from apiclient.response import RequestsResponse if TYPE_CHECKING: # pragma: no cover - # Stupid way of getting around cyclic imports when - # using typehinting. + from typing import Callable + from apiclient import APIClient + from apiclient.response import Response + from apiclient.utils.typing import JsonType class BaseRequestStrategy: diff --git a/apiclient/response.py b/apiclient/response.py index f690f25..63c82c7 100644 --- a/apiclient/response.py +++ b/apiclient/response.py @@ -1,8 +1,12 @@ -from typing import Any +from __future__ import annotations +from typing import TYPE_CHECKING -import requests +if TYPE_CHECKING: # pragma: no cover + from typing import Any -from apiclient.utils.typing import JsonType + import requests + + from apiclient.utils.typing import JsonType class Response: diff --git a/apiclient/response_handlers.py b/apiclient/response_handlers.py index a5bc35b..8c778af 100644 --- a/apiclient/response_handlers.py +++ b/apiclient/response_handlers.py @@ -1,11 +1,15 @@ +from __future__ import annotations from json import JSONDecodeError +from typing import TYPE_CHECKING from xml.etree import ElementTree -import requests - from apiclient.exceptions import ResponseParseError -from apiclient.response import Response -from apiclient.utils.typing import JsonType, XmlType + +if TYPE_CHECKING: # pragma: no cover + import requests + + from apiclient.response import Response + from apiclient.utils.typing import JsonType, XmlType class BaseResponseHandler: diff --git a/setup.cfg b/setup.cfg index 114a27e..01a9557 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,6 +3,10 @@ addopts = --cov=apiclient/ --cov-fail-under=100 --cov-report html env = ENDPOINT_BASE_URL=http://environment.com +[coverage:run] +# Pure type-alias module; only imported under TYPE_CHECKING, never at runtime. +omit = apiclient/utils/typing.py + [coverage:report] fail_under = 100 skip_covered = True