From 0c6403370900624b12a4043d4fa4553128e2f77b Mon Sep 17 00:00:00 2001 From: Andrii Porokhnavets Date: Wed, 30 Apr 2025 18:16:13 +0300 Subject: [PATCH 1/2] Drop support for Python 3.6-3.8 and add support for 3.12-3.13 Updated `tox.ini`, `pyproject.toml`, and GitHub Actions workflows to reflect the removal of Python 3.6-3.8 and inclusion of 3.12-3.13. Pinned `pre-commit` and `mypy` dependencies to ensure compatibility. Adjusted pre-commit hooks and dependencies to use newer versions for better tooling support. --- .github/workflows/main.yml | 18 +++--------------- .pre-commit-config.yaml | 32 ++++++++++++++++---------------- pyproject.toml | 7 +++---- tox.ini | 6 +++--- 4 files changed, 25 insertions(+), 38 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 65be537..a4e1394 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -9,27 +9,15 @@ jobs: strategy: matrix: os: - - "ubuntu-20.04" + - "ubuntu-latest" - "windows-latest" - "macos-latest" python: - - "3.6" - - "3.7" - - "3.8" - "3.9" - "3.10" - "3.11" - # Workaround from https://github.com/actions/runner-images/issues/9770 - exclude: # Python < v3.8 does not support Apple Silicon ARM64. - - python: "3.6" - os: macos-latest - - python: "3.7" - os: macos-latest - include: # So run those legacy versions on Intel CPUs. - - python: "3.6" - os: macos-13 - - python: "3.7" - os: macos-13 + - "3.12" + - "3.13" steps: - uses: actions/checkout@v4 - name: Setup python diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5a7720f..7eabe39 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/PyCQA/flake8 - rev: 5.0.4 + rev: 7.2.0 hooks: - id: flake8 name: Style Guide Enforcement (flake8) @@ -8,32 +8,32 @@ repos: - '--max-line-length=90' - '--per-file-ignores=__init__.py:F401' - repo: https://github.com/asottile/pyupgrade - rev: v2.31.0 + rev: v3.19.0 hooks: - id: pyupgrade name: Upgrade syntax for newer versions of the language (pyupgrade) args: - - '--py36-plus' -# - repo: https://github.com/pycqa/isort -# rev: 5.10.0 -# hooks: -# - id: isort -# name: 'Reorder Python imports' - - repo: https://github.com/PyCQA/docformatter - rev: v1.5.1 + - '--py39-plus' + - repo: https://github.com/pycqa/isort + rev: 6.0.1 hooks: - - id: docformatter - name: 'Formats docstrings' - args: - - '--in-place' + - id: isort + name: Reorder Python imports +# - repo: https://github.com/PyCQA/docformatter +# rev: v1.7.5 # incompatible with pre-commit > 4.0.0, but should be fixed in the next release +# hooks: +# - id: docformatter +# name: Formats docstrings +# args: +# - '--in-place ' - repo: 'https://github.com/pre-commit/pre-commit-hooks' - rev: v4.1.0 + rev: v5.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer - id: check-toml - repo: https://github.com/python/black - rev: 22.8.0 + rev: 25.1.0 hooks: - id: black name: Uncompromising Code Formatter (black) diff --git a/pyproject.toml b/pyproject.toml index 816fa34..affea48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,14 +10,13 @@ classifiers = [ "Operating System :: OS Independent", "Topic :: Software Development :: Libraries :: Application Frameworks", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] -requires-python = ">=3.6" +requires-python = ">=3.9" dependencies = [ "requests>=2.26.0", ] diff --git a/tox.ini b/tox.ini index b574d42..ee6fdcb 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] isolated_build = true envlist = - py{3.6,37,38,39,310,311} + py{39,310,311,312,313}, pre-commit mypy skip_missing_interpreters = true @@ -15,13 +15,13 @@ commands = pytest -q {posargs} [testenv:pre-commit] -deps = pre-commit +deps = pre-commit==4.2.0 commands = pre-commit run --all-files [testenv:mypy] deps = -r requirements.test.txt - mypy + mypy==1.15.0 types-requests commands = mypy ./mailtrap From 8ca3c574f7ff595c3ff06d36a1faebe2db85d106 Mon Sep 17 00:00:00 2001 From: Andrii Porokhnavets Date: Wed, 30 Apr 2025 18:16:37 +0300 Subject: [PATCH 2/2] Use modern `PEP 585` type hints across the codebase Replaced legacy `typing` constructs like `List` and `Dict` with modern `PEP 585` style type hints (`list`, `dict`, etc.). This improves code readability and aligns with Python 3.9+ standards. --- mailtrap/client.py | 8 +++----- mailtrap/exceptions.py | 7 ++----- mailtrap/mail/address.py | 3 +-- mailtrap/mail/attachment.py | 3 +-- mailtrap/mail/base.py | 20 +++++++++----------- mailtrap/mail/base_entity.py | 5 ++--- mailtrap/mail/from_template.py | 18 ++++++++---------- mailtrap/mail/mail.py | 16 +++++++--------- 8 files changed, 33 insertions(+), 47 deletions(-) diff --git a/mailtrap/client.py b/mailtrap/client.py index 342fc2a..9d33682 100644 --- a/mailtrap/client.py +++ b/mailtrap/client.py @@ -1,5 +1,3 @@ -from typing import Dict -from typing import List from typing import NoReturn from typing import Union @@ -24,12 +22,12 @@ def __init__( self.api_host = api_host self.api_port = api_port - def send(self, mail: BaseMail) -> Dict[str, Union[bool, List[str]]]: + def send(self, mail: BaseMail) -> dict[str, Union[bool, list[str]]]: url = f"{self.base_url}/api/send" response = requests.post(url, headers=self.headers, json=mail.api_data) if response.ok: - data = response.json() # type: Dict[str, Union[bool, List[str]]] + data: dict[str, Union[bool, list[str]]] = response.json() return data self._handle_failed_response(response) @@ -39,7 +37,7 @@ def base_url(self) -> str: return f"https://{self.api_host.rstrip('/')}:{self.api_port}" @property - def headers(self) -> Dict[str, str]: + def headers(self) -> dict[str, str]: return { "Authorization": f"Bearer {self.token}", "Content-Type": "application/json", diff --git a/mailtrap/exceptions.py b/mailtrap/exceptions.py index ee099b6..978f1b8 100644 --- a/mailtrap/exceptions.py +++ b/mailtrap/exceptions.py @@ -1,12 +1,9 @@ -from typing import List - - class MailtrapError(Exception): pass class APIError(MailtrapError): - def __init__(self, status: int, errors: List[str]) -> None: + def __init__(self, status: int, errors: list[str]) -> None: self.status = status self.errors = errors @@ -14,5 +11,5 @@ def __init__(self, status: int, errors: List[str]) -> None: class AuthorizationError(APIError): - def __init__(self, errors: List[str]) -> None: + def __init__(self, errors: list[str]) -> None: super().__init__(status=401, errors=errors) diff --git a/mailtrap/mail/address.py b/mailtrap/mail/address.py index 0d2cc8d..efd59c5 100644 --- a/mailtrap/mail/address.py +++ b/mailtrap/mail/address.py @@ -1,5 +1,4 @@ from typing import Any -from typing import Dict from typing import Optional from mailtrap.mail.base_entity import BaseEntity @@ -11,5 +10,5 @@ def __init__(self, email: str, name: Optional[str] = None) -> None: self.name = name @property - def api_data(self) -> Dict[str, Any]: + def api_data(self) -> dict[str, Any]: return self.omit_none_values({"email": self.email, "name": self.name}) diff --git a/mailtrap/mail/attachment.py b/mailtrap/mail/attachment.py index 5107501..72e6b55 100644 --- a/mailtrap/mail/attachment.py +++ b/mailtrap/mail/attachment.py @@ -1,6 +1,5 @@ from enum import Enum from typing import Any -from typing import Dict from typing import Optional from mailtrap.mail.base_entity import BaseEntity @@ -27,7 +26,7 @@ def __init__( self.content_id = content_id @property - def api_data(self) -> Dict[str, Any]: + def api_data(self) -> dict[str, Any]: return self.omit_none_values( { "content": self.content.decode(), diff --git a/mailtrap/mail/base.py b/mailtrap/mail/base.py index c408240..6032fc4 100644 --- a/mailtrap/mail/base.py +++ b/mailtrap/mail/base.py @@ -1,9 +1,7 @@ from abc import ABCMeta +from collections.abc import Sequence from typing import Any -from typing import Dict -from typing import List from typing import Optional -from typing import Sequence from mailtrap.mail.address import Address from mailtrap.mail.attachment import Attachment @@ -16,12 +14,12 @@ class BaseMail(BaseEntity, metaclass=ABCMeta): def __init__( self, sender: Address, - to: List[Address], - cc: Optional[List[Address]] = None, - bcc: Optional[List[Address]] = None, - attachments: Optional[List[Attachment]] = None, - headers: Optional[Dict[str, str]] = None, - custom_variables: Optional[Dict[str, Any]] = None, + to: list[Address], + cc: Optional[list[Address]] = None, + bcc: Optional[list[Address]] = None, + attachments: Optional[list[Attachment]] = None, + headers: Optional[dict[str, str]] = None, + custom_variables: Optional[dict[str, Any]] = None, ) -> None: self.sender = sender self.to = to @@ -32,7 +30,7 @@ def __init__( self.custom_variables = custom_variables @property - def api_data(self) -> Dict[str, Any]: + def api_data(self) -> dict[str, Any]: return self.omit_none_values( { "from": self.sender.api_data, @@ -48,7 +46,7 @@ def api_data(self) -> Dict[str, Any]: @staticmethod def get_api_data_from_list( items: Optional[Sequence[BaseEntity]], - ) -> Optional[List[Dict[str, Any]]]: + ) -> Optional[list[dict[str, Any]]]: if items is None: return None diff --git a/mailtrap/mail/base_entity.py b/mailtrap/mail/base_entity.py index 389baa2..536f94c 100644 --- a/mailtrap/mail/base_entity.py +++ b/mailtrap/mail/base_entity.py @@ -1,15 +1,14 @@ from abc import ABCMeta from abc import abstractmethod from typing import Any -from typing import Dict class BaseEntity(metaclass=ABCMeta): @property @abstractmethod - def api_data(self) -> Dict[str, Any]: + def api_data(self) -> dict[str, Any]: raise NotImplementedError @staticmethod - def omit_none_values(data: Dict[str, Any]) -> Dict[str, Any]: + def omit_none_values(data: dict[str, Any]) -> dict[str, Any]: return {key: value for key, value in data.items() if value is not None} diff --git a/mailtrap/mail/from_template.py b/mailtrap/mail/from_template.py index ba03b00..24fa786 100644 --- a/mailtrap/mail/from_template.py +++ b/mailtrap/mail/from_template.py @@ -1,6 +1,4 @@ from typing import Any -from typing import Dict -from typing import List from typing import Optional from mailtrap.mail.address import Address @@ -15,14 +13,14 @@ class MailFromTemplate(BaseMail): def __init__( self, sender: Address, - to: List[Address], + to: list[Address], template_uuid: str, - template_variables: Optional[Dict[str, Any]] = None, - cc: Optional[List[Address]] = None, - bcc: Optional[List[Address]] = None, - attachments: Optional[List[Attachment]] = None, - headers: Optional[Dict[str, str]] = None, - custom_variables: Optional[Dict[str, Any]] = None, + template_variables: Optional[dict[str, Any]] = None, + cc: Optional[list[Address]] = None, + bcc: Optional[list[Address]] = None, + attachments: Optional[list[Attachment]] = None, + headers: Optional[dict[str, str]] = None, + custom_variables: Optional[dict[str, Any]] = None, ) -> None: super().__init__( sender=sender, @@ -37,7 +35,7 @@ def __init__( self.template_variables = template_variables @property - def api_data(self) -> Dict[str, Any]: + def api_data(self) -> dict[str, Any]: return self.omit_none_values( { **super().api_data, diff --git a/mailtrap/mail/mail.py b/mailtrap/mail/mail.py index caa0a52..6f81e03 100644 --- a/mailtrap/mail/mail.py +++ b/mailtrap/mail/mail.py @@ -1,6 +1,4 @@ from typing import Any -from typing import Dict -from typing import List from typing import Optional from mailtrap.mail.address import Address @@ -23,16 +21,16 @@ class Mail(BaseMail): def __init__( self, sender: Address, - to: List[Address], + to: list[Address], subject: str, text: Optional[str] = None, html: Optional[str] = None, category: Optional[str] = None, - cc: Optional[List[Address]] = None, - bcc: Optional[List[Address]] = None, - attachments: Optional[List[Attachment]] = None, - headers: Optional[Dict[str, str]] = None, - custom_variables: Optional[Dict[str, Any]] = None, + cc: Optional[list[Address]] = None, + bcc: Optional[list[Address]] = None, + attachments: Optional[list[Attachment]] = None, + headers: Optional[dict[str, str]] = None, + custom_variables: Optional[dict[str, Any]] = None, ) -> None: super().__init__( sender=sender, @@ -49,7 +47,7 @@ def __init__( self.category = category @property - def api_data(self) -> Dict[str, Any]: + def api_data(self) -> dict[str, Any]: return self.omit_none_values( { **super().api_data,