Skip to content

Commit 69b3a44

Browse files
committed
Switch to pyright
2 parents 31679f2 + b26b35e commit 69b3a44

File tree

8 files changed

+29
-65
lines changed

8 files changed

+29
-65
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Happy typing!
1313
This project use [PDM](https://pdm.fming.dev/latest/), a modern Python package and dependency manager, to manage dependencies. After you [have installed PDM](https://pdm.fming.dev/latest/#installation), you can run this project locally based on the following steps:
1414

1515
```bash
16+
pdm plugin add pdm-autoexport
1617
pdm install
1718
pdm run devserver
1819
```

docs/Contribute.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,6 @@
3030

3131
`solution.py` contains the right solution, with everything else unchanged.
3232

33-
5. Test with [`mypy`](https://mypy.readthedocs.io/) to make sure your new challenge works as expected.
33+
5. Test with [`pyright`](https://microsoft.github.io/pyright/#/installation?id=command-line) to make sure your new challenge works as expected.
3434

3535
6. Create a Pull Request.

pdm.lock

+1-30
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ authors = [
66
{name = "laike9m", email = "[email protected]"},
77
]
88
dependencies = [
9-
"mypy>=1.6.1",
109
"flask>=3.0.0",
1110
"typing-extensions>=4.8.0",
1211
"pyright>=1.1.334",
@@ -35,7 +34,3 @@ without-hashes = true
3534
venvPath = "."
3635
venv = ".venv"
3736
pythonVersion = "3.12"
38-
39-
[tool.mypy]
40-
check_untyped_defs = true
41-
warn_no_return = false

requirements.txt

-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ flask==3.0.0
77
itsdangerous==2.1.2
88
Jinja2==3.1.2
99
MarkupSafe==2.1.3
10-
mypy==1.6.1
11-
mypy-extensions==1.0.0
1210
nodeenv==1.8.0
1311
pyright==1.1.334
1412
setuptools==68.2.2

tests/test_utils.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from views.utils import ChallengeManager
22

33

4-
def test_run_type_check_with_mypy():
4+
def test_run_type_check_with_pyright():
55
code = """\
66
a: int = 1
77
b: str = "2" # expect-type-error
@@ -12,7 +12,7 @@ def test_run_type_check_with_mypy():
1212
for i in range(10)
1313
]
1414
"""
15-
result = ChallengeManager._type_check_with_mypy(code)
15+
result = ChallengeManager._type_check_with_pyright(code)
1616
assert result.passed is False
1717
errors = [line for line in result.stdout.splitlines() if line]
1818
assert len(errors) == 3

views/utils.py

+23-15
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,14 @@
22
import io
33
import os
44
import re
5+
import subprocess
6+
import tempfile
57
import tokenize
68
from dataclasses import dataclass, field
79
from pathlib import Path
810
from typing import ClassVar, TypeAlias
911

10-
from mypy import api
11-
1212
ROOT_DIR = Path(__file__).parent.parent
13-
MYPY_CONFIG = ROOT_DIR / "pyproject.toml"
1413

1514

1615
ChallengeName: TypeAlias = str
@@ -47,12 +46,6 @@ class TypeCheckResult:
4746

4847

4948
class ChallengeManager:
50-
EXPECT_ERROR_COMMENT = "expect-type-error"
51-
52-
# Each mypy error should look like: `<filename>:<line_number>: <error|note>: <message>`
53-
# Here we only capture the error messages and line numbers
54-
MYPY_MESSAGE_REGEX = r"^(?:.+?):(\d+):(\s*error:.+)$"
55-
5649
def __init__(self):
5750
self.challenges = self._load_challenges()
5851
self.challenge_names = [
@@ -69,7 +62,7 @@ def get_challenge(self, name: str) -> Challenge:
6962
def run_challenge(self, name: str, user_code: str) -> TypeCheckResult:
7063
challenge = self.get_challenge(name)
7164
code = f"{user_code}\n{challenge.test_code}"
72-
return self._type_check_with_mypy(code)
65+
return self._type_check_with_pyright(code)
7366

7467
@staticmethod
7568
def _load_challenges() -> dict[ChallengeName, Challenge]:
@@ -87,8 +80,15 @@ def _load_challenges() -> dict[ChallengeName, Challenge]:
8780

8881
return challenges
8982

83+
EXPECT_ERROR_COMMENT = "expect-type-error"
84+
85+
# Pyright error messages look like:
86+
# `<filename>:<line_no>:<col_no> - <error|warning|information>: <message>`
87+
# Here we only capture the error messages and line numbers
88+
PYRIGHT_MESSAGE_REGEX = r"^(?:.+?):(\d+):[\s\-\d]+(error:.+)$"
89+
9090
@classmethod
91-
def _type_check_with_mypy(cls, code: str) -> TypeCheckResult:
91+
def _type_check_with_pyright(cls, code: str) -> TypeCheckResult:
9292
buffer = io.StringIO(code)
9393

9494
# This produces a stream of TokenInfos, example:
@@ -104,11 +104,19 @@ def _type_check_with_mypy(cls, code: str) -> TypeCheckResult:
104104
if token.type == tokenize.COMMENT
105105
and token.string[1:].strip() == cls.EXPECT_ERROR_COMMENT
106106
]
107-
raw_result = api.run(["--config-file", str(MYPY_CONFIG), "-c", code])
107+
108+
with tempfile.NamedTemporaryFile(suffix=".py") as temp:
109+
temp.write(code.encode())
110+
temp.flush()
111+
# TODO: switch to json output to simplify output parsing.
112+
# https://microsoft.github.io/pyright/#/command-line?id=json-output
113+
raw_result = subprocess.run(
114+
["pyright", temp.name], capture_output=True, text=True
115+
).stdout
108116
error_lines: list[str] = []
109117

110-
for line in raw_result[0].splitlines():
111-
m = re.match(cls.MYPY_MESSAGE_REGEX, line)
118+
for line in raw_result.splitlines():
119+
m = re.match(cls.PYRIGHT_MESSAGE_REGEX, line)
112120
if m is None:
113121
continue
114122
line_number, message = int(m.group(1)), m.group(2)
@@ -119,7 +127,7 @@ def _type_check_with_mypy(cls, code: str) -> TypeCheckResult:
119127
continue
120128
error_lines.append(f"{line_number}:{message}")
121129

122-
# If there are any lines that are expected to fail but not reported by mypy,
130+
# If there are any lines that are expected to fail but not reported by pyright,
123131
# they should be considered as errors.
124132
for line_number in expect_error_line_numbers:
125133
error_lines.append(f"{line_number}: error: Expected type error")

views/views.py

+1-10
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,4 @@ def run_challenge(name):
4141

4242
error_message = "<h2>❌ Challenge failed 😢\n\n</h2>"
4343
error_message += f"\nError:\n{result.stdout}{result.stderr}\n\n"
44-
45-
response = make_response(error_message)
46-
47-
# See https://twitter.com/Manjusaka_Lee/status/1720506781577937304
48-
# Call gc after returning the response, so that it's off the critical path.
49-
@response.call_on_close
50-
def cleanup_mypy_objects():
51-
gc.collect()
52-
53-
return response
44+
return error_message

0 commit comments

Comments
 (0)