Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/actions/pysen-test-post-313/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ runs:
- '-c'
# Changing the ownership is required. See the following link for more details:
# https://github.blog/2022-04-12-git-security-vulnerability-announced/
- "chown -R $(id -u):$(id -g) . && tox --parallel -e 'py{313,314}-post313'"
- "chown -R $(id -u):$(id -g) . && tox --parallel -e 'py{313,314}-post313, py{313,314}-mypy2'"
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ lint = [
"flake8>=3.7,<5",
"flake8-bugbear", # flake8 doesn't have a dependency for bugbear plugin
"isort>=4.3,<5.2.0",
"mypy>=0.770,<0.800",
"mypy>=0.770,<3",
]

[project.scripts]
Expand Down
20 changes: 17 additions & 3 deletions src/pysen/ext/mypy_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from pysen.command import check_command_installed
from pysen.dist_version import get_version
from pysen.error_lines import parse_error_lines
from pysen.exceptions import IncompatibleVersionError
from pysen.exceptions import IncompatibleVersionError, PysenError
from pysen.path import PathLikeType, change_dir, get_relative_path, resolve_path
from pysen.py_version import PythonVersion
from pysen.reporter import Reporter
Expand Down Expand Up @@ -191,11 +191,25 @@ class MypyTarget:
@functools.lru_cache(1)
def _check_mypy_version() -> None:
version = get_version("mypy")
if version.major >= 2 or version.major == 0 and version.minor < 770:
if version.major >= 3 or version.major == 0 and version.minor < 770:
raise IncompatibleVersionError(
f"pysen only supports mypy version >=0.770, <2. "
f"pysen only supports mypy version >=0.770, <3. "
f"version {version} is not supported."
)
if version.major >= 2:
# mypy >=2 uses a sqlite-backed cache by default, which requires the
# stdlib sqlite3 module (and the underlying libsqlite3 shared library)
# to be importable. Fail early with an actionable message instead of
# letting mypy crash with an INTERNAL ERROR.
try:
import sqlite3 # noqa: F401
except ImportError as e:
raise PysenError(
f"mypy {version} requires the sqlite3 module for its cache, "
f"but it could not be imported ({e}). Please install sqlite "
f"(e.g. the libsqlite3 / sqlite-libs system package) and "
f"rebuild Python with sqlite3 support."
) from e

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mypy2 でsqlite が必要になるので入っていなければエラーを出すのが良さそうです

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

これってmypy側でハンドリングしてくれないんでしょうか?
pysen側で各種ツールの依存チェックをしてあげる必要がありますかね?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

python/mypy#21440
mypy のIssueにはなっているようです



def run(
Expand Down
50 changes: 49 additions & 1 deletion tests/test_mypy_wrapper.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import pathlib
import sys
from unittest import mock

import pytest

from pysen.ext.mypy_wrapper import MypyPlugin, MypyTarget, run
from pysen.exceptions import IncompatibleVersionError, PysenError
from pysen.ext.mypy_wrapper import MypyPlugin, MypyTarget, _check_mypy_version, run
from pysen.reporter import Reporter

BASE_DIR = pathlib.Path(__file__).resolve().parent
Expand All @@ -13,6 +16,51 @@ def test_run_zero_source() -> None:
assert run(reporter, BASE_DIR, BASE_DIR, MypyTarget([]), True) == 0


def test__check_mypy_version() -> None:
def check(version: str) -> None:
_check_mypy_version.cache_clear()
with mock.patch(
"pysen.dist_version.distribution",
return_value=mock.Mock(version=version),
):
# decouple the version-logic check from the host's sqlite3
# availability; the sqlite3 requirement for mypy >=2 is covered by
# test__check_mypy_version_requires_sqlite_for_mypy2
with mock.patch.dict(sys.modules, {"sqlite3": mock.Mock()}):
_check_mypy_version()

# supported: >=0.770, <3
check("0.770")
check("0.991")
check("1.19.0")
check("2.0.0")
check("2.1.0")

# unsupported
for version in ("0.760", "3.0.0"):
with pytest.raises(IncompatibleVersionError):
check(version)


def test__check_mypy_version_requires_sqlite_for_mypy2() -> None:
def check(version: str) -> None:
_check_mypy_version.cache_clear()
with mock.patch(
"pysen.dist_version.distribution",
return_value=mock.Mock(version=version),
):
# setting the entry to None makes `import sqlite3` raise ImportError
with mock.patch.dict(sys.modules, {"sqlite3": None}):
_check_mypy_version()

# mypy >=2 requires sqlite3
with pytest.raises(PysenError, match="sqlite3"):
check("2.0.0")

# mypy <2 does not need sqlite3
check("1.19.0")


def test_mypy_plugin() -> None:
script_plugin = MypyPlugin(script=pathlib.Path("/foo/bar/baz"))
script_plugin2 = MypyPlugin(script=pathlib.Path("./bar/baz"))
Expand Down
30 changes: 29 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tox]
envlist = py{310}-dacite150-isort51-black{20,22}-mypy078, py{311,312}-dacite150-isort51-black22-mypy099, development, latest, py{313,314}-post313
envlist = py{310}-dacite150-isort51-black{20,22}-mypy078, py{311,312}-dacite150-isort51-black22-mypy099, development, latest, py{313,314}-post313, py{313,314}-mypy2
isolated_build = True

[testenv]
Expand All @@ -26,7 +26,14 @@ setenv =
basepython = python3.10
deps =
pipenv==2022.9.8
# pipenv 2022.9.8 imports pkg_resources, which was removed in setuptools
# >=81; current virtualenv seeds a newer setuptools, so pin it back.
setuptools<81
extras = lint
setenv =
LANG = C
# isolate pipenv's cache per env; the shared default races under --parallel
PIPENV_CACHE_DIR = {envdir}/.pipenv-cache
commands =
pipenv sync --system
pip install .
Expand All @@ -37,7 +44,14 @@ commands =
basepython = python3.10
deps =
pipenv==2022.9.8
# pipenv 2022.9.8 imports pkg_resources, which was removed in setuptools
# >=81; current virtualenv seeds a newer setuptools, so pin it back.
setuptools<81
extras = lint
setenv =
LANG = C
# isolate pipenv's cache per env; the shared default races under --parallel
PIPENV_CACHE_DIR = {envdir}/.pipenv-cache
commands =
pipenv sync --system
pip install .
Expand All @@ -57,3 +71,17 @@ changedir = version_testing/post_313
commands =
pip install ../../
pysen run lint

[testenv:py{313,314}-mypy2]
deps =
pipenv==2026.0.2
mypy>=2.0,<3
isort==7.0.0
black==25.12.0
flake8==7.3.0
flake8-bugbear==25.11.29
importlib-metadata==8.7.0
changedir = version_testing/post_mypy2
commands =
pip install ../../
pysen run lint
18 changes: 17 additions & 1 deletion version_testing/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,20 @@
```bash
$ docker build -t quay.io/pysen/pysen-test .
$ docker push quay.io/pysen/pysen-test
```
```

## Directories

- `post_310` / `post_313`: smoke-test projects exercised by the corresponding
`tox` environments for newer Python interpreters.
- `post_mypy2`: smoke-test project for `mypy >=2` (`py{313,314}-mypy2` tox env).

### Note on mypy >=2 and `py_version`

mypy `>=2` only supports type-checking against `python_version >= 3.10`. When a
pysen config sets an older `py_version` (e.g. `py27`, `py38`, `py39`), pysen
still emits `python_version = 3.x` into the mypy config. mypy `>=2` prints a
warning (`python_version: Python 3.8 is not supported (must be 3.10 or higher)`)
and silently falls back to its minimum supported version, so the effective
type-checking semantics differ from what the user requested. The `post_mypy2`
example therefore uses `py_version = "py313"`.
4 changes: 3 additions & 1 deletion version_testing/post_313/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ FROM alpine:3.23
COPY --from=python:3.13.11-alpine3.23 /usr/local/ /usr/local/
COPY --from=python:3.14.2-alpine3.23 /usr/local/ /usr/local/

RUN apk add --no-cache bash git \
# sqlite-libs is required by mypy >=2, whose default cache backend uses sqlite3
# (mypy <2 used a filesystem metastore and did not need it).
RUN apk add --no-cache bash git sqlite-libs \
&& pip3.14 install --no-cache-dir tox==3.28.0
ENV TOX_PARALLEL_NO_SPINNER=1
20 changes: 20 additions & 0 deletions version_testing/post_mypy2/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[project]
name = "example_post_mypy2"
requires-python = ">=3.13"
description = "example project for mypy version >=2"

[tool.pysen]
version = "0.12"

[tool.pysen.lint]
enable_black = true
enable_flake8 = true
enable_isort = true
enable_mypy = true
mypy_preset = "very_strict"
# NOTE: mypy >=2 only supports checking python_version >=3.10. Older py_version
# values (py27/py38/py39) are rejected by mypy and silently fall back to its
# minimum supported version. See version_testing/README.md.
py_version = "py313"
[[tool.pysen.lint.mypy_targets]]
paths = ["src"]
2 changes: 2 additions & 0 deletions version_testing/post_mypy2/src/example_code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def main(arg1: str, arg2: int) -> bool:
return arg1.isdigit() and arg2 > 0
Loading