diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f2903be..4da29b5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -18,15 +18,13 @@ permissions: jobs: # --------------------------------------------------------------------------- - # Lint. rustfmt and mypy --strict are enforced (blocking); clippy and ruff - # stay advisory (continue-on-error) until their debt clears, and each runs + # Lint. rustfmt, mypy --strict, and ruff are enforced (blocking); clippy + # stays advisory (continue-on-error) until its debt clears, and each runs # regardless of the others: # - rustfmt -> blocking (source is rustfmt-clean). # - clippy -> flip to blocking after the str->bytes fix (#19). # - mypy -> blocking: `mypy --strict fast_mail_parser/` is clean (#42). - # - ruff -> advisory: `ruff check .` reports issues in the package - # stubs (UP006/UP007) and in test code (I001/E501/UP*). Fixing - # those is out of scope here; flip to blocking once clean (#44). + # - ruff -> blocking: `ruff check .` is clean (config in ruff.toml). # --------------------------------------------------------------------------- lint: name: Lint @@ -58,15 +56,12 @@ jobs: python -m pip install --upgrade pip mypy mypy --strict fast_mail_parser/ - # Advisory: `ruff check .` currently reports issues in the package stubs - # (UP006/UP007) and in test code (I001/E501/UP015). Fixing those is out - # of scope; drop continue-on-error once they are resolved (#44). + # Blocking: `ruff check .` is clean (config in ruff.toml). - name: ruff check if: always() run: | python -m pip install ruff ruff check . - continue-on-error: true # --------------------------------------------------------------------------- # Supply-chain audit. Blocking (no continue-on-error): the dependency stack is diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b98c6b5..acf03af 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -129,28 +129,31 @@ make benchmark The following checks are run in CI. Run them locally before opening a PR: ```bash -cargo fmt --all -- --check # Rust formatting (enforced / blocking in CI) +cargo fmt --all -- --check # Rust formatting (blocking in CI) cargo clippy --all-targets -- -D warnings -W clippy::cast_possible_truncation -mypy fast_mail_parser/ # type-stub checking (advisory in CI) -ruff check . # Python linting (advisory) +mypy --strict fast_mail_parser/ # type-stub checking (blocking in CI) +ruff check --fix . # Python lint + autofix (blocking in CI; config in ruff.toml) ``` +Run **`ruff check --fix .`** before opening a PR — it auto-fixes most findings +(import order, modern typing, etc.); fix any remainder by hand so `ruff check .` +is clean. + In CI: -- **`cargo fmt --check`** is **blocking** — the source is rustfmt-clean and must - stay that way. -- **`cargo clippy`** and **`mypy`** are currently **advisory** - (`continue-on-error`) while their remaining debt clears. Please keep new code - clean under both even though they do not yet fail the build. -- `ruff` is advisory; keep Python code clean under it. +- **`cargo fmt --check`**, **`mypy --strict`**, and **`ruff check`** are + **blocking** — keep the source clean under all three. +- **`cargo clippy`** is currently **advisory** (`continue-on-error`) while its + remaining debt clears; please keep new code clean under it even though it does + not yet fail the build. ## Continuous integration The [`Test`](.github/workflows/test.yml) workflow gates every pull request. It consists of: -- **Lint** — `cargo fmt --check` (blocking), plus `cargo clippy` and `mypy` - (advisory). +- **Lint** — `cargo fmt --check`, `mypy --strict`, and `ruff check` (all + blocking), plus `cargo clippy` (advisory). - **cargo audit** — a **blocking** supply-chain audit of the Rust dependency stack (PyO3 0.29) against the RustSec advisory database. A new advisory against any dependency fails the build. diff --git a/Cargo.lock b/Cargo.lock index a181ddb..3c39692 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,7 +41,7 @@ dependencies = [ [[package]] name = "fast_mail_parser" -version = "0.3.0" +version = "0.4.0" dependencies = [ "mailparse", "pyo3", diff --git a/fast_mail_parser/__init__.py b/fast_mail_parser/__init__.py index 240b07b..de65edd 100644 --- a/fast_mail_parser/__init__.py +++ b/fast_mail_parser/__init__.py @@ -1,4 +1,4 @@ -from .fast_mail_parser import parse_email, ParseError, PyMail, PyAttachment +from .fast_mail_parser import ParseError, PyAttachment, PyMail, parse_email __all__ = [ "parse_email", diff --git a/fast_mail_parser/__init__.pyi b/fast_mail_parser/__init__.pyi index 478ee23..c14febc 100644 --- a/fast_mail_parser/__init__.pyi +++ b/fast_mail_parser/__init__.pyi @@ -1,5 +1,3 @@ -import typing as t - __all__ = [ "parse_email", "PyMail", @@ -18,11 +16,11 @@ class PyMail: def __init__( self, subject: str, - text_plain: t.List[str], - text_html: t.List[str], + text_plain: list[str], + text_html: list[str], date: str, - attachments: t.List[PyAttachment], - headers: t.Dict[str, str], + attachments: list[PyAttachment], + headers: dict[str, str], ) -> None: self.subject = subject self.text_plain = text_plain @@ -36,7 +34,7 @@ class ParseError(Exception): """Error happened during parsing email.""" -def parse_email(payload: t.Union[str, bytes]) -> PyMail: +def parse_email(payload: str | bytes) -> PyMail: """Parse raw content of email and return structured datatype. A missing ``Subject`` or ``Date`` header yields the empty string ``""`` diff --git a/tests/conftest.py b/tests/conftest.py index bd8bd87..63f8ca4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,8 @@ -import pytest import sys from typing import Callable +import pytest + from fast_mail_parser import PyMail, parse_email sys.path.pop(0) @@ -10,7 +11,7 @@ @pytest.fixture(scope='module') def read_mail() -> Callable: def wrap(path: str): - with open(path, 'r') as f: + with open(path) as f: return f.read() return wrap diff --git a/tests/generate_rfc_corpus.py b/tests/generate_rfc_corpus.py index cb4969e..e795c04 100644 --- a/tests/generate_rfc_corpus.py +++ b/tests/generate_rfc_corpus.py @@ -16,8 +16,8 @@ """ import os -from email.message import EmailMessage from email import policy +from email.message import EmailMessage OUT_DIR = os.path.join(os.path.dirname(__file__), "data", "rfc") diff --git a/tests/test_attachments.py b/tests/test_attachments.py index 3fdc701..32e5d5f 100644 --- a/tests/test_attachments.py +++ b/tests/test_attachments.py @@ -1,4 +1,3 @@ -import typing as t from fast_mail_parser import PyMail @@ -7,13 +6,15 @@ def test__attachments_are_available(attachment_mail: PyMail): def test__base64_content_is_decoded(attachment_mail: PyMail): - attachment = list(filter(lambda a: a.mimetype == 'image/png', attachment_mail.attachments)).pop() + attachment = list( + filter(lambda a: a.mimetype == 'image/png', attachment_mail.attachments) + ).pop() assert attachment.content == b'PNG here' def test__expected_attachments_are_present(large_mail: PyMail): - expected_attachment_names: t.Set[str] = {'Lorem Ipsum - All the facts.pdf', 'Kitty Dark.png'} + expected_attachment_names: set[str] = {'Lorem Ipsum - All the facts.pdf', 'Kitty Dark.png'} attachments = [a for a in large_mail.attachments if a.filename in expected_attachment_names] assert len(attachments) == 2 diff --git a/tests/test_contract.py b/tests/test_contract.py index 8d54b91..414bbf3 100644 --- a/tests/test_contract.py +++ b/tests/test_contract.py @@ -11,7 +11,6 @@ pass. """ -import typing as t import pytest @@ -34,7 +33,7 @@ MALFORMED_MESSAGE = " unexpected continuation\r\n\r\nbody" -def _public_attrs(obj: object) -> t.Set[str]: +def _public_attrs(obj: object) -> set[str]: return {name for name in dir(obj) if not name.startswith("_")}