Skip to content

Commit

Permalink
Add mypy-options config option to StaticTypeChecker (#1136)
Browse files Browse the repository at this point in the history
  • Loading branch information
herenali authored Feb 6, 2025
1 parent 8f4e0b3 commit f0c4c6f
Show file tree
Hide file tree
Showing 7 changed files with 263 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
### ✨ Enhancements

- Added custom error message for `comparison-with-callable`
- Added new checker option `mypy-options` in `static-type-checker` to let users override default mypy command-line arguments

### 💫 New checkers

Expand Down
2 changes: 2 additions & 0 deletions docs/checkers/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3194,6 +3194,8 @@ and `pdb.set_trace()`) are found. These breakpoints should be removed in product

```

(mypy-based-checks)=

## Mypy-based checks

The following errors are identified by the StaticTypeChecker, which uses Mypy to detect issues related to type annotations in Python code.
Expand Down
11 changes: 11 additions & 0 deletions docs/usage/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,14 @@ Modules added to this list are permitted in addition to the ones listed in `allo

When `true`, allow all local modules to be imported, without being reported by the **forbidden-import** check.
By default this option is `false`.

### `mypy-options`

A list of [command-line arguments](https://mypy.readthedocs.io/en/stable/command_line.html) to be passed into mypy when performing the [**static type** checks](#mypy-based-checks).

By default, this list includes the following flags:

- `ignore-missing-imports`, `follow-imports=skip`

Modifying this option will override all default flags.
Note that the `show-error-end` flag is always passed into mypy, so it does not need to be specified within this option.
17 changes: 16 additions & 1 deletion python_ta/checkers/static_type_checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,17 @@ class StaticTypeChecker(BaseRawFileChecker):
"Used when a dictionary entry has an incompatible key or value type",
),
}
options = (
(
"mypy-options",
{
"default": ["ignore-missing-imports", "follow-imports=skip"],
"type": "csv",
"metavar": "<mypy options>",
"help": "List of configuration flags for mypy",
},
),
)

COMMON_PATTERN = (
r"^(?P<file>[^:]+):(?P<start_line>\d+):(?P<start_col>\d+):"
Expand Down Expand Up @@ -73,7 +84,11 @@ class StaticTypeChecker(BaseRawFileChecker):
def process_module(self, node: nodes.NodeNG) -> None:
"""Run Mypy on the current file and handle type errors."""
filename = node.stream().name
mypy_options = ["--ignore-missing-imports", "--show-error-end", "--follow-imports=skip"]

mypy_options = ["--show-error-end"]
for arg in self.linter.config.mypy_options:
mypy_options.append("--" + arg)

result, _, _ = api.run([filename] + mypy_options)

for line in result.splitlines():
Expand Down
4 changes: 4 additions & 0 deletions python_ta/config/.pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,7 @@ pycodestyle-ignore =
W391, # pylint C0305
W503, # this one just conflicts with pycodestyle W504
W605, # pylint W1401

[MYPY]
# List of configuration flags for mypy
mypy-options = ignore-missing-imports, follow-imports=skip
4 changes: 4 additions & 0 deletions tests/test.pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -135,3 +135,7 @@ pycodestyle-ignore =
W293, # pylint C0303
W391, # pylint C0305
W503 # this one just conflicts with pycodestyle W504

[MYPY]
# List of configuration flags for mypy
mypy-options = ignore-missing-imports, follow-imports=skip
226 changes: 225 additions & 1 deletion tests/test_custom_checkers/test_static_type_checker.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import os

import pylint.testutils
import pytest
from astroid import MANAGER

from python_ta.checkers.static_type_checker import StaticTypeChecker
Expand Down Expand Up @@ -229,3 +228,228 @@ def test_imports_no_error(self) -> None:
mod = MANAGER.ast_from_file(file_path)
with self.assertNoMessages():
self.checker.process_module(mod)


class TestStaticTypeCheckerCustomConfig(pylint.testutils.CheckerTestCase):
CHECKER_CLASS = StaticTypeChecker
CONFIG = {"mypy_options": ["warn-redundant-casts"]}

def test_e9951_incompatible_argument_type(self) -> None:
file_path = os.path.normpath(
os.path.join(
__file__,
"../../../examples/custom_checkers/static_type_checker_examples/e9951_incompatible_argument_type.py",
)
)
mod = MANAGER.ast_from_file(file_path)
with self.assertAddsMessages(
pylint.testutils.MessageTest(
msg_id="incompatible-argument-type",
line=5,
col_offset=23,
end_line=5,
end_col_offset=28,
args=("1", "calculate_area", "str", "float"),
),
pylint.testutils.MessageTest(
msg_id="incompatible-argument-type",
line=11,
col_offset=27,
end_line=11,
end_col_offset=27,
args=("1", "convert_to_upper", "int", "str"),
),
ignore_position=True,
):
self.checker.process_module(mod)

def test_e9952_incompatible_assignment(self) -> None:
file_path = os.path.normpath(
os.path.join(
__file__,
"../../../examples/custom_checkers/static_type_checker_examples/e9952_incompatible_assignment.py",
)
)
mod = MANAGER.ast_from_file(file_path)
with self.assertAddsMessages(
pylint.testutils.MessageTest(
msg_id="incompatible-assignment",
line=2,
col_offset=7,
end_line=2,
end_col_offset=19,
args=("str", "int"),
),
pylint.testutils.MessageTest(
msg_id="incompatible-assignment",
line=4,
col_offset=14,
end_line=4,
end_col_offset=18,
args=("str", "int"),
),
):
self.checker.process_module(mod)

def test_e9953_list_item_type_mismatch(self) -> None:
file_path = os.path.normpath(
os.path.join(
__file__,
"../../../examples/custom_checkers/static_type_checker_examples/e9953_list_item_type_mismatch.py",
)
)
mod = MANAGER.ast_from_file(file_path)
with self.assertAddsMessages(
pylint.testutils.MessageTest(
msg_id="list-item-type-mismatch",
line=1,
col_offset=37,
end_line=1,
end_col_offset=37,
args=("2", "int", "str"),
),
pylint.testutils.MessageTest(
msg_id="list-item-type-mismatch",
line=3,
col_offset=29,
end_line=3,
end_col_offset=35,
args=("2", "str", "int"),
),
pylint.testutils.MessageTest(
msg_id="list-item-type-mismatch",
line=5,
col_offset=33,
end_line=5,
end_col_offset=37,
args=("2", "str", "float"),
),
):
self.checker.process_module(mod)

def test_e9954_unsupported_operand_types(self) -> None:
file_path = os.path.normpath(
os.path.join(
__file__,
"../../../examples/custom_checkers/static_type_checker_examples/e9954_unsupported_operand_types.py",
)
)
mod = MANAGER.ast_from_file(file_path)
with self.assertAddsMessages(
pylint.testutils.MessageTest(
msg_id="unsupported-operand-types",
line=1,
col_offset=10,
end_line=1,
end_col_offset=10,
args=("-", "str", "int"),
),
pylint.testutils.MessageTest(
msg_id="unsupported-operand-types",
line=3,
col_offset=14,
end_line=3,
end_col_offset=17,
args=("+", "int", "str"),
),
pylint.testutils.MessageTest(
msg_id="unsupported-operand-types",
line=5,
col_offset=15,
end_line=5,
end_col_offset=17,
args=("*", "float", "str"),
),
):
self.checker.process_module(mod)

def test_e9955_union_attr_error(self) -> None:
"""Test for union attribute errors (E9955)."""
file_path = os.path.normpath(
os.path.join(
__file__,
"../../../examples/custom_checkers/static_type_checker_examples/e9955_union_attr_error.py",
)
)
mod = MANAGER.ast_from_file(file_path)
with self.assertAddsMessages(
pylint.testutils.MessageTest(
msg_id="union-attr-error",
line=4,
col_offset=13,
end_line=5,
end_col_offset=18,
args=("int", "upper"),
),
pylint.testutils.MessageTest(
msg_id="union-attr-error",
line=4,
col_offset=13,
end_line=5,
end_col_offset=18,
args=("float", "upper"),
),
pylint.testutils.MessageTest(
msg_id="union-attr-error",
line=9,
col_offset=12,
end_line=9,
end_col_offset=20,
args=("list[Any]", "keys"),
),
):
self.checker.process_module(mod)

def test_e9956_dict_item_type_mismatch(self) -> None:
file_path = os.path.normpath(
os.path.join(
__file__,
"../../../examples/custom_checkers/static_type_checker_examples/e9956_dict_item_type_mismatch.py",
)
)
if not os.path.exists(file_path):
raise FileNotFoundError(f"Test file not found: {file_path}")

mod = MANAGER.ast_from_file(file_path)
with self.assertAddsMessages(
pylint.testutils.MessageTest(
msg_id="dict-item-type-mismatch",
line=1,
col_offset=45,
end_line=1,
end_col_offset=54,
args=("2", "str", "int", "int", "str"),
),
pylint.testutils.MessageTest(
msg_id="dict-item-type-mismatch",
line=3,
col_offset=50,
end_line=3,
end_col_offset=60,
args=("2", "int", "str", "str", "float"),
),
):
self.checker.process_module(mod)

def test_no_errors_in_static_type_checker_no_error(self) -> None:
file_path = os.path.normpath(
os.path.join(
__file__,
"../../../examples/custom_checkers/static_type_checker_examples/static_type_checker_no_error.py",
)
)
mod = MANAGER.ast_from_file(file_path)
with self.assertNoMessages():
self.checker.process_module(mod)

def test_imports_no_error(self) -> None:
"""Imports a module with mypy errors, no errors raised."""
file_path = os.path.normpath(
os.path.join(
__file__,
"../../../examples/custom_checkers/static_type_checker_examples/imports_no_error.py",
)
)
mod = MANAGER.ast_from_file(file_path)
with self.assertNoMessages():
self.checker.process_module(mod)

0 comments on commit f0c4c6f

Please sign in to comment.