From 721c02734c33c8405a80d8583cd4c5ae8b9ed419 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9lio=20Costa?= Date: Thu, 8 Oct 2026 18:42:25 -0300 Subject: [PATCH] Add Ruff lint and format checks to CI CI gains a lint job on Python 3.12 that runs `ruff check .` and `ruff format --check .`. Ruff is pinned to 0.16.9 in the dev extra, which the job installs, so contributors and CI run the same version. The config targets py39 with line length 110, the width the existing code fits best (the smallest formatter diff). It ignores the typing-modernization rules (FA100, UP006, UP035), because rewriting the public annotations would change signatures, and RUF022, because __all__ is grouped on purpose. The README is excluded so its samples keep their aligned comments. Existing code brought in line: three formatter rewraps in millionsend/_client.py and millionsend/contacts.py, which leave the parsed AST unchanged, plus one in tests. The tests also import _client with `from millionsend import _client`. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 13 +++++++++++++ millionsend/_client.py | 7 ++++++- millionsend/contacts.py | 8 ++------ pyproject.toml | 18 +++++++++++++++++- tests/conftest.py | 2 +- tests/test_client.py | 2 +- tests/test_resources.py | 4 +--- 7 files changed, 41 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ea87b39..f492739 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,3 +22,16 @@ jobs: cache: pip - run: python -m pip install -e ".[dev]" - run: pytest + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: "3.12" + cache: pip + # The dev extra pins ruff, so CI and contributors run the same version. + - run: python -m pip install -e ".[dev]" + - run: ruff check . + - run: ruff format --check . diff --git a/millionsend/_client.py b/millionsend/_client.py index a11c132..61f7e26 100644 --- a/millionsend/_client.py +++ b/millionsend/_client.py @@ -169,7 +169,12 @@ def request( try: resp = requests.request( - method, url, headers=headers, data=data, params=query, timeout=_config("timeout") or DEFAULT_TIMEOUT + method, + url, + headers=headers, + data=data, + params=query, + timeout=_config("timeout") or DEFAULT_TIMEOUT, ) except requests.exceptions.RequestException as exc: raise MillionSendError( diff --git a/millionsend/contacts.py b/millionsend/contacts.py index 9685d94..a9e755a 100644 --- a/millionsend/contacts.py +++ b/millionsend/contacts.py @@ -33,9 +33,7 @@ def list( ``params`` is resend-python's pagination dict, forwarded as the query. """ - return request( - "GET", f"/contacts/{_key(contact_id or id, email)}/topics", query=list_query(params) - ) + return request("GET", f"/contacts/{_key(contact_id or id, email)}/topics", query=list_query(params)) @classmethod def update(cls, params: Dict[str, Any]) -> Any: @@ -84,9 +82,7 @@ def create( ) @classmethod - def get( - cls, params: List[Union[str, Dict[str, Any]]], include: Optional[List[str]] = None - ) -> Any: + def get(cls, params: List[Union[str, Dict[str, Any]]], include: Optional[List[str]] = None) -> Any: """POST /contacts/batch/get — 1..1000 contacts by ``{"id": ...}`` or ``{"email": ...}`` (a bare string is an id), returned in request order. diff --git a/pyproject.toml b/pyproject.toml index 905a3a1..251dc2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,10 +18,26 @@ Homepage = "https://github.com/MillionSend/millionsend-python" Repository = "https://github.com/MillionSend/millionsend-python" [project.optional-dependencies] -dev = ["pytest>=7"] +dev = ["pytest>=7", "ruff==0.16.9"] [tool.hatch.build.targets.wheel] packages = ["millionsend"] [tool.pytest.ini_options] testpaths = ["tests"] + +[tool.ruff] +target-version = "py39" +line-length = 110 +# The README's Python samples keep their hand-aligned comments. +extend-exclude = ["*.md"] + +[tool.ruff.lint] +ignore = [ + # Public signatures keep typing.Optional/Dict/List; rewriting the annotations changes them. + "FA100", + "UP006", + "UP035", + # __all__ is grouped by purpose (config, resources, errors), not alphabetized. + "RUF022", +] diff --git a/tests/conftest.py b/tests/conftest.py index 21d6fd3..a3dff3b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,7 @@ import pytest import millionsend -import millionsend._client as _client +from millionsend import _client class FakeResponse: diff --git a/tests/test_client.py b/tests/test_client.py index 6f16e6f..f0f9cc1 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -4,12 +4,12 @@ import requests import millionsend -import millionsend._client as _client from millionsend import ( MillionSendError, MissingApiKeyError, NotFoundError, ValidationError, + _client, ) diff --git a/tests/test_resources.py b/tests/test_resources.py index 17730b5..9d5e5b4 100644 --- a/tests/test_resources.py +++ b/tests/test_resources.py @@ -292,9 +292,7 @@ def test_contacts_topics_list(http): def test_contacts_topics_update_bare_array(http): http.body = {"id": "c1"} - millionsend.Contacts.Topics.update( - {"id": "c1", "topics": [{"id": "t1", "subscription": "opt_out"}]} - ) + millionsend.Contacts.Topics.update({"id": "c1", "topics": [{"id": "t1", "subscription": "opt_out"}]}) assert http.calls[0]["method"] == "PATCH" assert http.calls[0]["path"] == "/contacts/c1/topics" assert http.calls[0]["body"] == [{"id": "t1", "subscription": "opt_out"}]