From b7ccefd6f42b7e3452c4387c09fec8e1089f4546 Mon Sep 17 00:00:00 2001 From: TiM Date: Tue, 20 Oct 2026 11:15:51 +1300 Subject: [PATCH 1/2] feat!: support Python 3.11-3.15, dropping 3.9 and 3.10 Moves the supported range up to the current CPython releases. 3.15.0 is out and every compiled dependency (cryptography, pydantic-core, pandas, numpy) ships cp315 wheels, so the CI and publish matrices now run 3.11-3.15. With 3.9 gone, the notes that existed only to protect it are removed: the PEP 604 quirk in AGENTS.md, the black-runs-on-3.9 caveats on the dev cap, pre-commit rev and Dependabot ignore, and the single-`with` test comment. ruff, black and mypy now target 3.11. The py311 ruff target enables B905 at the one `zip()` in coerce_rows; it takes strict=False to keep today's behaviour for a row whose cell count differs from the column count. Enabling pyupgrade (UP) is left out: the code still spells Optional/List, and that is a ~250-site rewrite of its own. --- .github/dependabot.yml | 5 ++--- .github/workflows/ci.yml | 2 +- .github/workflows/publish.yml | 2 +- .pre-commit-config.yaml | 3 +-- AGENTS.md | 11 +---------- CONTRIBUTING.md | 2 +- docs/getting-started.md | 2 +- pyproject.toml | 30 ++++++++++++++---------------- snowflake_sql_api/types.py | 5 ++++- tests/test_testing.py | 2 -- 10 files changed, 26 insertions(+), 38 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index cb65dac..e958e0d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -17,9 +17,8 @@ updates: # would match nothing. Security updates are not blocked by it: in # dependabot-core, IgnoreCondition#ignored_versions returns early for # security_updates_only with just the explicit `versions`, and a - # name-only ignore has none. Black's security fixes since 25.9.0 need - # Python 3.10+, so a security PR for it is a decision about the 3.9 - # floor and the hook rev, not a plain merge. + # name-only ignore has none. A security PR for it still has to move + # the hook rev in .pre-commit-config.yaml alongside the cap. - dependency-name: "black" # GitHub Actions used by the CI and publish workflows. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 90986db..71f4064 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 3a57c34..d7188bf 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -36,7 +36,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d9b751c..6756f9e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,8 +11,7 @@ repos: - repo: https://github.com/psf/black # Kept in sync with the [dev] cap in pyproject.toml (<25.10); move both - # together by hand. Stay on a black that runs on Python 3.9 (the project's - # runtime floor): 25.11.0 is the last one, 25.12.0+ require 3.10. + # together by hand. rev: 25.9.0 hooks: - id: black diff --git a/AGENTS.md b/AGENTS.md index 09ea728..b208c14 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -116,14 +116,6 @@ the linked regression test first. (`query_stream`, Phase 8). The hook fires for `query`/`execute`/`submit` today; there is no streaming path yet, so no regression test until the feature lands (this is spike bug #2, intentionally not yet covered). -- **No PEP 604 unions at runtime** (py3.9 floor). ruff's `UP` (pyupgrade) rule is - omitted on purpose: it would rewrite `Optional[...]` / `Union[...]` to - `X | None`, which raises at import time on 3.9 for typing generics (PEP 604 on - generics is 3.10+). Keep `from __future__ import annotations` plus - `Optional`/`Union`. -- **mypy `python_version = "3.10"` vs the 3.9-3.13 matrix.** 3.10 is the lowest - this mypy accepts; true 3.9 runtime compatibility is enforced by the pytest - matrix, which imports every module under 3.9. - **Coverage uses `coverage run`, not `pytest --cov`.** The package ships a pytest plugin via the `pytest11` entry point, so `snowflake_sql_api.testing` (and the whole package) is imported at plugin-load time, before pytest-cov starts @@ -140,7 +132,7 @@ the linked regression test first. fixtures). No respx. See `docs/testing.md`. - Each fixed bug gets a named regression test (`test_regression_*`) so it cannot silently return. -- Target coverage: >= 89%, enforced across Python 3.9-3.13. Run with +- Target coverage: >= 89%, enforced across Python 3.11-3.15. Run with `coverage run -m pytest && coverage report` (see Known Quirks). ## Common Mistakes @@ -153,7 +145,6 @@ the linked regression test first. - Adding a runtime dependency without strong justification. The small install / fast cold start is the whole point; new optional features go behind an extra. -- Rewriting `Optional[...]` to `X | None` (breaks the 3.9 runtime). - Conflating the JWT claim account with the API host (see Known Quirks). - Forgetting the async counterpart of a sync change (sync/async parity). - Forgetting a `test_regression_*` for a fixed bug. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 13c494b..c6667b0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,7 +34,7 @@ All four must pass (and `pre-commit run --all-files` is clean). Use `coverage ru (`snowflake_sql_api.testing`), and `coverage run` starts tracing before that plugin is imported so import-time lines are measured correctly. The coverage gate is enforced (`fail_under = 89` in `pyproject.toml`); `coverage report` exits -non-zero below it. CI runs the same checks across Python 3.9-3.13. +non-zero below it. CI runs the same checks across Python 3.11-3.15. Checklist: diff --git a/docs/getting-started.md b/docs/getting-started.md index 2000980..3ac09f6 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -17,7 +17,7 @@ pip install "snowflake-sql-api[pandas]" # DataFrame output helpers pip install "snowflake-sql-api[pydantic]" # typed-row mapping ``` -Requires Python 3.9 or newer. Core dependencies are `httpx`, `PyJWT`, and +Requires Python 3.11 or newer. Core dependencies are `httpx`, `PyJWT`, and `cryptography`. ## Prerequisites diff --git a/pyproject.toml b/pyproject.toml index ab699d1..0b5427e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "snowflake-sql-api" dynamic = ["version"] description = "Lightweight, pure-Python client for Snowflake's SQL API v2" readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.11" license = { file = "LICENSE" } authors = [{ name = "hampsterx" }] keywords = ["snowflake", "sql", "rest", "api", "httpx", "serverless", "lambda"] @@ -17,11 +17,11 @@ classifiers = [ "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "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", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Database", "Typing :: Typed", @@ -43,9 +43,7 @@ dev = [ "ruff>=0.4", # Capped below 25.10 so CI formats with the same black the pre-commit hook # pins (rev 25.9.0 in .pre-commit-config.yaml). Move the two together by - # hand; Dependabot ignores black (.github/dependabot.yml). Keep the cap at - # or below 25.12 while Python 3.9 is supported: 25.12.0+ require 3.10, so - # a higher cap resolves a different black per interpreter. + # hand; Dependabot ignores black (.github/dependabot.yml). "black>=24.0,<25.10", "mypy>=1.8", "build>=1.0", @@ -89,13 +87,13 @@ include = [ [tool.ruff] line-length = 88 -target-version = "py39" +target-version = "py311" [tool.ruff.lint] select = ["E", "F", "W", "I", "B", "C4", "SIM"] -# E501 (line length) is owned by black. `UP` (pyupgrade) is omitted: it would -# rewrite runtime type aliases like `Optional[Sequence[Any]]` to `X | None`, -# which raises at import time on Python 3.9 (PEP 604 on typing generics is 3.10+). +# E501 (line length) is owned by black. `UP` (pyupgrade) is not enabled yet: the +# code predates the 3.11 floor and still spells `Optional`/`List`, so turning it +# on is a ~250-site annotation rewrite of its own. ignore = ["E501"] [tool.ruff.lint.per-file-ignores] @@ -103,14 +101,14 @@ ignore = ["E501"] [tool.black] line-length = 88 -# Pin to py39 grammar: our code uses no newer syntax, and a single floor target -# keeps black's AST safety check parseable by the interpreter running CI. -target-version = ["py39"] +# Pin to the floor's grammar: a single floor target keeps black's AST safety +# check parseable by the interpreter running CI. +target-version = ["py311"] [tool.mypy] -# Lowest target this mypy accepts; true 3.9 runtime compat is gated by the -# pytest matrix (3.9-3.13), which imports every module under each interpreter. -python_version = "3.10" +# The runtime floor; the pytest matrix (3.11-3.15) imports every module under +# each supported interpreter. +python_version = "3.11" files = ["snowflake_sql_api"] disallow_untyped_defs = true disallow_incomplete_defs = true diff --git a/snowflake_sql_api/types.py b/snowflake_sql_api/types.py index 13f9534..937e11b 100644 --- a/snowflake_sql_api/types.py +++ b/snowflake_sql_api/types.py @@ -178,6 +178,9 @@ def coerce_rows(raw_rows: List[List[Any]], columns: List[ColumnMeta]) -> List[Ro """Coerce a partition of raw rows into a list of name-keyed :data:`Row`.""" names = [c.name for c in columns] return [ - {name: coerce_value(cell, col) for name, col, cell in zip(names, columns, row)} + { + name: coerce_value(cell, col) + for name, col, cell in zip(names, columns, row, strict=False) + } for row in raw_rows ] diff --git a/tests/test_testing.py b/tests/test_testing.py index c15aed0..fa47155 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -137,8 +137,6 @@ def test_register_match_predicate() -> None: def test_unregistered_statement_raises() -> None: - # Single `with` per statement here: a parenthesized multi-context `with` - # is 3.10+ syntax and breaks the 3.9 leg of the test matrix. fake = FakeSnowflake() client = make_client(fake) with pytest.raises(FakeSnowflakeError, match="no result registered"): From 28c5f6c9457244f0608d5230cd92a03d8f6fd1c5 Mon Sep 17 00:00:00 2001 From: TiM Date: Tue, 20 Oct 2026 11:31:20 +1300 Subject: [PATCH 2/2] fix(analysis): move black to 26.5.1 now that the 3.9 floor is gone 25.9.0 was held only because later black needs Python 3.10+. It predates the 26.3.x security fixes, and with Dependabot security updates off for this repo nothing else would raise them. The hook rev and the [dev] cap move together; black 26 reflows one .format() call in a test. --- .github/dependabot.yml | 7 ++++--- .pre-commit-config.yaml | 4 ++-- pyproject.toml | 6 +++--- tests/test_import_footprint.py | 4 +--- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e958e0d..58ae281 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -13,12 +13,13 @@ updates: # .pre-commit-config.yaml, and the two move together by hand. A # Dependabot bump of the cap alone splits CI from the local hook. # Name-only on purpose: an update-types ignore is computed from the first - # X.Y.Z version in the requirement, and `>=24.0,<25.10` has none, so it + # X.Y.Z version in the requirement, and `>=26.5,<26.6` has none, so it # would match nothing. Security updates are not blocked by it: in # dependabot-core, IgnoreCondition#ignored_versions returns early for # security_updates_only with just the explicit `versions`, and a - # name-only ignore has none. A security PR for it still has to move - # the hook rev in .pre-commit-config.yaml alongside the cap. + # name-only ignore has none. Security updates are off for this repo, + # though, so a black advisory has to be caught by hand, and the fix + # moves the hook rev in .pre-commit-config.yaml alongside the cap. - dependency-name: "black" # GitHub Actions used by the CI and publish workflows. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6756f9e..eca73c2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,9 +10,9 @@ repos: - id: ruff-check - repo: https://github.com/psf/black - # Kept in sync with the [dev] cap in pyproject.toml (<25.10); move both + # Kept in sync with the [dev] cap in pyproject.toml (<26.6); move both # together by hand. - rev: 25.9.0 + rev: 26.5.1 hooks: - id: black diff --git a/pyproject.toml b/pyproject.toml index 0b5427e..9883a55 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,10 +41,10 @@ dev = [ "pytest-asyncio>=0.21", "respx>=0.20", "ruff>=0.4", - # Capped below 25.10 so CI formats with the same black the pre-commit hook - # pins (rev 25.9.0 in .pre-commit-config.yaml). Move the two together by + # Held to 26.5.x so CI formats with the same black the pre-commit hook + # pins (rev 26.5.1 in .pre-commit-config.yaml). Move the two together by # hand; Dependabot ignores black (.github/dependabot.yml). - "black>=24.0,<25.10", + "black>=26.5,<26.6", "mypy>=1.8", "build>=1.0", "pre-commit>=3.5", diff --git a/tests/test_import_footprint.py b/tests/test_import_footprint.py index e915985..147fb3a 100644 --- a/tests/test_import_footprint.py +++ b/tests/test_import_footprint.py @@ -64,9 +64,7 @@ def exec_module(self, module): sys.meta_path.insert(0, _Recorder()) import snowflake_sql_api # noqa: F401 print(",".join(sorted(set(attempted)))) -""".format( - forbidden=FORBIDDEN_ON_BARE_IMPORT -) +""".format(forbidden=FORBIDDEN_ON_BARE_IMPORT) _TIMING_SCRIPT = """ import time