-
Notifications
You must be signed in to change notification settings - Fork 0
Call mypy manually and speedup tests #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
124fac0
dont use the mypy checking system and create a custom one instead.
timrid 5577c0d
stop JVM after usage
timrid b21f6dd
use ubuntu-lastest
timrid 8a62b1a
add colored pytest output
timrid 07d7ed4
Simplify the output by using markers in the code instead of the line …
timrid 9c26d9d
Update to mypy v1.19.1
timrid be6f145
apply suggestions from code review
timrid File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,25 +1,69 @@ | ||
| # Note that this conftest file exists at the package level as it is needed to configure | ||
| # the JVM for docstrings at the package level. | ||
| import os | ||
| import pathlib | ||
| import logging | ||
| import tempfile | ||
| import textwrap | ||
| from collections.abc import Generator | ||
| from pathlib import Path | ||
|
|
||
| import pytest | ||
|
|
||
| import chaquopy_stubgen | ||
|
|
||
| pytest_plugins = [ | ||
| "mypy.test.data", | ||
| ] | ||
| os.environ["MYPY_TEST_PREFIX"] = str(pathlib.Path(__file__).parent / "stubtest") | ||
| logger = logging.getLogger(__name__) | ||
|
|
||
| ANDROID_JAR = pathlib.Path(__file__).parent / "android-35.jar" | ||
|
|
||
| ANDROID_JAR = Path(__file__).parent / "android-35.jar" | ||
|
|
||
| @pytest.fixture(autouse=True, scope="session") | ||
|
|
||
| @pytest.fixture(scope="session") | ||
| def jvm(): | ||
| import jpype # type: ignore | ||
|
|
||
| if not jpype.isJVMStarted(): | ||
| jpype.startJVM(None, classpath=[ANDROID_JAR], convertStrings=True) # type: ignore | ||
| jpype.startJVM(None, classpath=[ANDROID_JAR], convertStrings=True) # type: ignore | ||
|
timrid marked this conversation as resolved.
|
||
| import jpype.imports # type: ignore | ||
|
|
||
| yield jpype | ||
| try: | ||
| yield jpype | ||
| finally: | ||
| jpype.shutdownJVM() | ||
|
|
||
|
timrid marked this conversation as resolved.
|
||
|
|
||
| @pytest.fixture(scope="session") | ||
| def stub_dir(jvm) -> Generator[Path, None, None]: | ||
| # logging.basicConfig(level="DEBUG") | ||
| with tempfile.TemporaryDirectory() as tmpdir: | ||
| tmpdir = Path(tmpdir) | ||
| import java # type: ignore | ||
|
|
||
| logger.debug(f"Generating stubs in {tmpdir}...") | ||
|
|
||
| chaquopy_stubgen.generate_java_stubs( | ||
| [java], # type: ignore | ||
| output_dir=tmpdir, | ||
| ) | ||
|
|
||
| # Rename "java-stubs" to "java" | ||
| (tmpdir / "java-stubs").rename(tmpdir / "java") | ||
|
|
||
| yield tmpdir | ||
|
|
||
|
|
||
| @pytest.fixture(scope="session") | ||
| def mypy_project_dir(stub_dir: Path) -> Generator[Path, None, None]: | ||
| with tempfile.TemporaryDirectory() as tmp: | ||
| project_dir = Path(tmp) | ||
| with project_dir.joinpath("pyproject.toml").open("w") as f: | ||
| f.write( | ||
| textwrap.dedent( | ||
| f"""\ | ||
| [tool.mypy] | ||
| mypy_path = "{stub_dir.absolute()}" | ||
|
|
||
| [[tool.mypy.overrides]] | ||
| module = "java.*" | ||
| ignore_errors = true | ||
| """ | ||
| ) | ||
| ) | ||
| yield project_dir | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,159 @@ | ||
| import io | ||
| import json | ||
| import logging | ||
| import os | ||
| import re | ||
| import tokenize | ||
| from collections.abc import Generator | ||
| from contextlib import contextmanager | ||
| from pathlib import Path | ||
| from typing import Literal, Optional, TypedDict | ||
|
|
||
| import mypy.api | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| class MypyMessage(TypedDict): | ||
| file: str | ||
| line: int | ||
| column: int | ||
| message: str | ||
| hint: Optional[str] | ||
| code: Optional[str] | ||
| severity: Literal["error", "note", "warning"] | ||
|
|
||
|
|
||
| @contextmanager | ||
| def change_dir(path: Path) -> Generator[None, None, None]: | ||
| """Temporary change the current working directory.""" | ||
| old = Path.cwd() | ||
| try: | ||
| os.chdir(path) | ||
| yield | ||
| finally: | ||
| os.chdir(old) | ||
|
|
||
|
|
||
| def _parse_mypy_jsonl_output(mypy_stdout: str) -> list[MypyMessage]: | ||
| """Parse mypy's JSONL output into a list of MypyMessage objects.""" | ||
| messages = [] | ||
| for line in mypy_stdout.strip().split("\n"): | ||
| if line: # Skip empty lines | ||
| messages.append(json.loads(line)) | ||
| return messages | ||
|
|
||
|
|
||
| def run_mypy( | ||
| project_dir: Path, | ||
| test_code: str, | ||
| ) -> str: | ||
| """Run mypy on the given test code and return the mypy stdout.""" | ||
| testfile_name = "testfile.py" | ||
| with project_dir.joinpath(testfile_name).open("w") as f: | ||
| f.write(test_code) | ||
|
|
||
| with change_dir(project_dir): | ||
| result = mypy.api.run([testfile_name, "--output", "json"]) | ||
|
|
||
| mypy_stdout, mypy_stderr, mypy_returncode = result | ||
| logger.debug(f"mypy stdout: {mypy_stdout}") | ||
| logger.debug(f"mypy stderr: {mypy_stderr}") | ||
| logger.debug(f"mypy returncode: {mypy_returncode}") | ||
|
|
||
| return mypy_stdout | ||
|
|
||
|
|
||
| def run_and_assert_mypy( | ||
| project_dir: Path, | ||
| test_code: str, | ||
| expected_output: dict[str, str] | str, | ||
| ): | ||
| """ | ||
| Run mypy on the given test code and assert that the output matches the expected output. | ||
|
|
||
| The expected_output can be either: | ||
| - A dictionary mapping markers like "*1" to expected mypy messages. In this case, | ||
| the function will parse the mypy JSON output and compare the messages for each marked line. | ||
| - A raw string containing the expected mypy output. In this case, the function will compare | ||
| the raw mypy stdout with the expected string. | ||
| """ | ||
| mypy_stdout = run_mypy(project_dir, test_code) | ||
| if isinstance(expected_output, dict): | ||
| mypy_output = _parse_mypy_jsonl_output(mypy_stdout) | ||
| assert_mypy_json_output(test_code, mypy_output, expected_output) | ||
| else: | ||
| # some errors like syntax errors are not returned as json message. In | ||
| # this case, we just check the raw stdout for the expected error message. | ||
| assert expected_output.strip() == mypy_stdout.strip() | ||
|
|
||
|
|
||
| def _format_mypy_msg(msg: MypyMessage) -> str: | ||
| """Format a mypy message into a human-readable string, including hints if present.""" | ||
| formatted_msg = f"{msg['severity']}: {msg['message']}" | ||
| if hints := msg.get("hint"): | ||
| for hint in hints.splitlines(): | ||
| formatted_msg += f"\nhint: {hint}" | ||
| return formatted_msg | ||
|
|
||
|
|
||
| def _parse_marked_lines_with_tokenize(code: str) -> dict[str, int]: | ||
| """Parse the code to find all lines with markers like *1, *2, etc. in comments.""" | ||
| marked_lines = {} | ||
|
|
||
| # Tokenize the code | ||
| tokens = tokenize.generate_tokens(io.StringIO(code).readline) | ||
|
|
||
| for token in tokens: | ||
| if token.type == tokenize.COMMENT: | ||
| # token.string contains the comment including the # | ||
| match = re.search(r"#\s*\*(\d+)", token.string) | ||
| if match: | ||
| marker = f"*{match.group(1)}" | ||
| marked_lines[marker] = token.start[0] # Line number | ||
|
|
||
| return marked_lines | ||
|
|
||
|
|
||
| def assert_mypy_json_output( | ||
| code: str, | ||
| mypy_output: list[MypyMessage], | ||
| expected_output: dict[str, str], | ||
| ): | ||
| """Assert that the mypy output matches the expected output for the given code.""" | ||
|
|
||
| # Parse the code to find the marked lines | ||
| marked_lines: dict[str, int] = _parse_marked_lines_with_tokenize(code) | ||
|
|
||
| # Validate that all expected markers are present in the code | ||
| missing_markers = set(expected_output.keys()) - set(marked_lines.keys()) | ||
| if missing_markers: | ||
| assert False, f"Expected markers not found in code: {', '.join(sorted(missing_markers))}" | ||
|
|
||
| # Group messages by line number | ||
| result_by_line: dict[int, list[MypyMessage]] = {} | ||
| for msg in mypy_output: | ||
| line = msg["line"] | ||
| if line not in result_by_line: | ||
| result_by_line[line] = [] | ||
| result_by_line[line].append(msg) | ||
|
|
||
| # Compare the expected output with the actual mypy messages for each marked line | ||
| for marker, line in marked_lines.items(): | ||
| expected_msgs = expected_output[marker] | ||
| mypy_msgs: list[MypyMessage] | None = result_by_line.pop(line) | ||
| assert mypy_msgs is not None and len(mypy_msgs) > 0 | ||
|
|
||
| if isinstance(expected_msgs, str): | ||
| expected_msgs = [expected_msgs] | ||
|
|
||
| for expected_msg, msg in zip(expected_msgs, mypy_msgs, strict=True): | ||
| actual_msg = _format_mypy_msg(msg) | ||
| assert expected_msg.strip() == actual_msg.strip() | ||
|
timrid marked this conversation as resolved.
|
||
|
|
||
| if len(result_by_line) > 0: | ||
| unexpected_msgs = [] | ||
| for line, msgs in result_by_line.items(): | ||
| for msg in msgs: | ||
| unexpected_msgs.append(f"line {line}: {_format_mypy_msg(msg)}") | ||
| assert False, "Unexpected mypy messages:\n" + "\n".join(unexpected_msgs) | ||
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.