-
Notifications
You must be signed in to change notification settings - Fork 34
Support test_code to add pyright-config comment #98
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| """A simple question, only for running tests. | ||
| """ | ||
|
|
||
|
|
||
| def foo(): | ||
| pass | ||
|
|
||
|
|
||
| ## End of your code ## | ||
| foo(1) | ||
| foo(1, 2) # expect-type-error | ||
|
|
||
| ## End of test code ## | ||
| # pyright: reportGeneralTypeIssues=error |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,17 +8,19 @@ | |
|
|
||
|
|
||
| def test_identical(solution_file: Path): | ||
| level, challenge_name = solution_file.parent.name.split("-", maxsplit=1) | ||
| with solution_file.open() as f: | ||
| solution_code = f.read() | ||
| solution_test = Challenge( | ||
| name=challenge_name, level=Level(level), code=solution_code | ||
| ).test_code | ||
|
|
||
| question_file = solution_file.parent / "question.py" | ||
| with question_file.open() as f: | ||
| question_code = f.read() | ||
| question_test = Challenge( | ||
| name=challenge_name, level=Level(level), code=question_code | ||
| ).test_code | ||
| def get_test_code(path: Path): | ||
| TEST_SPLITTER = "\n## End of test code ##\n" | ||
| level, challenge_name = path.parent.name.split("-", maxsplit=1) | ||
|
|
||
| with solution_file.open() as f: | ||
| challenge_code = f.read() | ||
| challenge = Challenge( | ||
| name=challenge_name, level=Level(level), code=challenge_code | ||
| ) | ||
|
|
||
| return challenge.test_code.partition(TEST_SPLITTER)[0] | ||
|
|
||
| solution_test = get_test_code(solution_file) | ||
| question_test = get_test_code(solution_file.parent / "question.py") | ||
|
Comment on lines
+11
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Critical bug: Line 15 opens 🐛 Proposed fix def get_test_code(path: Path):
TEST_SPLITTER = "\n## End of test code ##\n"
level, challenge_name = path.parent.name.split("-", maxsplit=1)
- with solution_file.open() as f:
+ with path.open() as f:
challenge_code = f.read()
challenge = Challenge(
name=challenge_name, level=Level(level), code=challenge_code
)
return challenge.test_code.partition(TEST_SPLITTER)[0]🤖 Prompt for AI Agents |
||
|
|
||
| assert solution_test.strip() == question_test.strip() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,8 +4,6 @@ | |
|
|
||
| from pathlib import Path | ||
|
|
||
| import pytest | ||
|
|
||
| from views.challenge import ChallengeManager | ||
|
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,29 @@ | |
| from typing import ClassVar, Optional, TypeAlias | ||
|
|
||
| ROOT_DIR = Path(__file__).parent.parent | ||
| PYRIGHT_BASIC_CONFIG = """ | ||
| # pyright: analyzeUnannotatedFunctions=true | ||
| # pyright: strictParameterNoneValue=true | ||
| # pyright: disableBytesTypePromotions=false | ||
| # pyright: strictListInference=false | ||
| # pyright: strictDictionaryInference=false | ||
| # pyright: strictSetInference=false | ||
| # pyright: deprecateTypingAliases=false | ||
| # pyright: enableExperimentalFeatures=false | ||
| # pyright: reportMissingImports=error | ||
| # pyright: reportUndefinedVariable=error | ||
| # pyright: reportGeneralTypeIssues=error | ||
| # pyright: reportOptionalSubscript=error | ||
| # pyright: reportOptionalMemberAccess=error | ||
| # pyright: reportOptionalCall=error | ||
| # pyright: reportOptionalIterable=error | ||
| # pyright: reportOptionalContextManager=error | ||
| # pyright: reportOptionalOperand=error | ||
| # pyright: reportTypedDictNotRequiredAccess=error | ||
| # pyright: reportPrivateImportUsage=error | ||
| # pyright: reportUnboundVariable=error | ||
| # pyright: reportUnusedCoroutine=error | ||
| """ | ||
|
|
||
|
|
||
| class Level(StrEnum): | ||
|
|
@@ -145,13 +168,30 @@ def _get_challenges_groupby_level(self) -> dict[Level, list[ChallengeName]]: | |
| # Pyright error messages look like: | ||
| # `<filename>:<line_no>:<col_no> - <error|warning|information>: <message>` | ||
| # Here we only capture the error messages and line numbers | ||
| PYRIGHT_MESSAGE_REGEX = r"^(?:.+?):(\d+):[\s\-\d]+(error:.+)$" | ||
| PYRIGHT_MESSAGE_REGEX = ( | ||
| r"^(?:.+?):(?P<line_number>\d+):[\s\-\d]+(?P<message>error:.+)$" | ||
| ) | ||
|
|
||
| @staticmethod | ||
| def _partition_test_code(test_code: str): | ||
| TEST_SPLITTER = "\n## End of test code ##\n" | ||
|
|
||
| # PYRIGHT_BASIC_CONFIG aim to limit user to modify the config | ||
| test_code, end_test_comment, pyright_config = test_code.partition(TEST_SPLITTER) | ||
| pyright_basic_config = PYRIGHT_BASIC_CONFIG | ||
|
|
||
| # Replace `## End of test code ##` with PYRIGHT_BASIC_CONFIG | ||
| if end_test_comment: | ||
| pyright_basic_config += pyright_config | ||
| return test_code, pyright_basic_config | ||
|
|
||
| @classmethod | ||
| def _type_check_with_pyright( | ||
| cls, user_code: str, test_code: str | ||
| ) -> TypeCheckResult: | ||
| code = f"{user_code}{test_code}" | ||
| test_code, pyright_basic_config = cls._partition_test_code(test_code) | ||
|
|
||
| code = f"{user_code}{test_code}{pyright_basic_config}" | ||
| buffer = io.StringIO(code) | ||
|
|
||
| # This produces a stream of TokenInfos, example: | ||
|
|
@@ -187,37 +227,45 @@ def _type_check_with_pyright( | |
| return TypeCheckResult(message=stderr, passed=False) | ||
| error_lines: list[str] = [] | ||
|
|
||
| # Substract lineno in merged code by lineno_delta, so that the lineno in | ||
| # Substract lineno in merged code by user_code_line_len, so that the lineno in | ||
| # error message matches those in the test code editor. Fixed #20. | ||
| lineno_delta = len(user_code.splitlines()) | ||
| user_code_lines_len = len(user_code.splitlines()) | ||
| for line in stdout.splitlines(): | ||
| m = re.match(cls.PYRIGHT_MESSAGE_REGEX, line) | ||
| if m is None: | ||
| continue | ||
| line_number, message = int(m.group(1)), m.group(2) | ||
| line_number, message = int(m["line_number"]), m["message"] | ||
| if line_number in error_line_seen_in_err_msg: | ||
| # Each reported error should be attached to a specific line, | ||
| # If it is commented with # expect-type-error, let it pass. | ||
| error_line_seen_in_err_msg[line_number] = True | ||
| continue | ||
| # Error could be thrown from user code too, in which case delta shouldn't be applied. | ||
| error_lines.append( | ||
| f"{line_number if line_number <= lineno_delta else line_number - lineno_delta}:{message}" | ||
| ) | ||
| error_line = f"%s:{message}" | ||
|
|
||
| if line_number <= user_code_lines_len: | ||
| error_lines.append(error_line % line_number) | ||
| elif line_number <= user_code_lines_len + len(test_code.splitlines()): | ||
| error_lines.append(error_line % (line_number - user_code_lines_len)) | ||
| else: | ||
| error_lines.append(error_line % "[pyright-config]") | ||
|
|
||
| # If there are any lines that are expected to fail but not reported by pyright, | ||
| # they should be considered as errors. | ||
| for line_number, seen in error_line_seen_in_err_msg.items(): | ||
| if not seen: | ||
| error_lines.append( | ||
| f"{line_number - lineno_delta}: error: Expected type error but instead passed" | ||
| f"{line_number - user_code_lines_len}: error: Expected type error but instead passed" | ||
| ) | ||
|
|
||
| passed = len(error_lines) == 0 | ||
| if passed: | ||
| error_lines.append("\nAll tests passed") | ||
| else: | ||
| error_lines.append(f"\nFound {len(error_lines)} errors") | ||
| # Error for pyright-config will not fail the challenge | ||
| passed = True | ||
| for error_line in error_lines: | ||
| if error_line.startswith("[pyright-config]"): | ||
| continue | ||
| passed = False | ||
|
|
||
| error_lines.append(f"\nFound {len(error_lines)} errors") | ||
|
Comment on lines
+261
to
+268
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: Error count includes non-failing pyright-config errors, which may confuse users. A user might see "Found 1 errors" but still pass the challenge if the only error is from pyright-config. Consider either:
Also, minor grammar nit: "Found 1 errors" should be "Found 1 error" for singular. 💡 Suggested improvement- error_lines.append(f"\nFound {len(error_lines)} errors")
+ blocking_count = sum(1 for e in error_lines if not e.startswith("[pyright-config]"))
+ error_lines.append(f"\nFound {blocking_count} error{'s' if blocking_count != 1 else ''}")🤖 Prompt for AI Agents
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well I don't agree There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @laike9m Understood! I'll defer to your judgment on this. Thanks for the feedback.
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The suggested solution seems complicated. Can you work on a better solution? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @laike9m Sure! Here's a simpler approach that adds a counter to the existing loop where you're already checking for pyright-config errors: # Error for pyright-config will not fail the challenge
passed = True
blocking_errors = 0
for error_line in error_lines:
if error_line.startswith("[pyright-config]"):
continue
blocking_errors += 1
passed = False
error_word = "error" if blocking_errors == 1 else "errors"
error_lines.append(f"\nFound {blocking_errors} {error_word}")This reuses the loop you already have, so there's no extra iteration. It counts only the blocking errors and fixes the singular/plural grammar issue. Does this work better for you? |
||
|
|
||
| return TypeCheckResult(message="\n".join(error_lines), passed=passed) | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor grammatical issue in the documentation.
The sentence structure is incomplete. After describing the comment marker, it reads "Several [pyright-config]..." which doesn't form a complete sentence.
📝 Suggested fix
📝 Committable suggestion
🤖 Prompt for AI Agents