python-ta 2.11.0__tar.gz → 2.12.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_ta-2.11.0/python_ta.egg-info → python_ta-2.12.0}/PKG-INFO +11 -9
- {python_ta-2.11.0 → python_ta-2.12.0}/README.md +2 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/pyproject.toml +9 -9
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/__init__.py +1 -1
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/cfg/cfg_generator.py +2 -2
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/cfg/graph.py +1 -1
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/check/helpers.py +1 -3
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/forbidden_io_function_checker.py +2 -2
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/global_variables_checker.py +6 -1
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/infinite_loop_checker.py +115 -28
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/pycodestyle_checker.py +4 -4
- python_ta-2.12.0/python_ta/checkers/simplifiable_if_checker.py +42 -0
- python_ta-2.12.0/python_ta/checkers/unnecessary_f_string_checker.py +43 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/config/.pylintrc +4 -3
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/config/messages_config.toml +1 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/contracts/__init__.py +125 -39
- python_ta-2.12.0/python_ta/debug/accumulation_table.py +296 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/debug/snapshot_tracer.py +44 -44
- python_ta-2.12.0/python_ta/debug/webstepper/webstepper_template.html.jinja +35 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/reporters/core.py +10 -1
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/reporters/html_reporter.py +6 -1
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/reporters/json_reporter.py +1 -1
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/reporters/node_printers.py +271 -285
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/reporters/templates/stylesheet.css +19 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/reporters/templates/template.html.jinja +3 -3
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/transforms/setendings.py +1 -125
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/util/servers/persistent_server.py +2 -1
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/z3/z3_parser.py +11 -11
- {python_ta-2.11.0 → python_ta-2.12.0/python_ta.egg-info}/PKG-INFO +11 -9
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta.egg-info/SOURCES.txt +3 -1
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta.egg-info/requires.txt +7 -9
- {python_ta-2.11.0 → python_ta-2.12.0}/tests/test_black.py +0 -1
- {python_ta-2.11.0 → python_ta-2.12.0}/tests/test_check.py +3 -4
- {python_ta-2.11.0 → python_ta-2.12.0}/tests/test_examples.py +80 -24
- python_ta-2.11.0/python_ta/debug/accumulation_table.py +0 -225
- python_ta-2.11.0/python_ta/debug/webstepper/index.html +0 -12
- {python_ta-2.11.0 → python_ta-2.12.0}/LICENSE +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/MANIFEST.in +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/__main__.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/cfg/__init__.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/cfg/__main__.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/cfg/visitor.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/check/__init__.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/check/watch.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/__init__.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/condition_logic_checker.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/forbidden_import_checker.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/forbidden_python_syntax_checker.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/inconsistent_or_missing_returns_checker.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/invalid_for_target_checker.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/invalid_name_checker.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/invalid_range_index_checker.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/missing_space_in_doctest_checker.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/one_iteration_checker.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/possibly_undefined_checker.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/redundant_assignment_checker.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/shadowing_in_comprehension_checker.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/static_type_checker.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/top_level_code_checker.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/type_annotation_checker.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/unmentioned_parameter_checker.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/unnecessary_indexing_checker.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/config/__init__.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/contracts/__main__.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/debug/__init__.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/debug/id_tracker.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/debug/recursion_table.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/debug/snapshot.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/debug/webstepper/99ee5c67fd0c522b4b6a.png +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/debug/webstepper/fd6133fe40f4f90440d6.png +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/debug/webstepper/index.bundle.js +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/debug/webstepper/index.bundle.js.LICENSE.txt +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/patches/__init__.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/patches/checkers.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/patches/messages.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/patches/transforms.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/reporters/__init__.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/reporters/color_reporter.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/reporters/plain_reporter.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/reporters/templates/pyta_logo_markdown.png +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/reporters/templates/script.js +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/transforms/__init__.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/transforms/z3_visitor.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/upload.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/util/__init__.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/util/autoformat.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/util/servers/one_shot_server.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/util/tree.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/utils.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/z3/__init__.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta.egg-info/dependency_links.txt +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta.egg-info/entry_points.txt +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta.egg-info/not-zip-safe +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/python_ta.egg-info/top_level.txt +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/setup.cfg +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/setup.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/tests/test_init_logging.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/tests/test_main.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/tests/test_setendings.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/tests/test_subclass_contracts.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/tests/test_validate_invariants.py +0 -0
- {python_ta-2.11.0 → python_ta-2.12.0}/tests/test_z3_visitor.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-ta
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.12.0
|
|
4
4
|
Summary: Code checking tool for teaching Python
|
|
5
5
|
Author-email: David Liu <david@cs.toronto.edu>
|
|
6
6
|
License: MIT
|
|
@@ -8,34 +8,35 @@ Project-URL: Homepage, https://github.com/pyta-uoft/pyta
|
|
|
8
8
|
Project-URL: Documentation, https://www.cs.toronto.edu/~david/pyta/
|
|
9
9
|
Project-URL: Repository, https://github.com/pyta-uoft/pyta.git
|
|
10
10
|
Project-URL: Changelog, https://github.com/pyta-uoft/pyta/blob/master/CHANGELOG.md
|
|
11
|
-
Requires-Python: >=3.
|
|
11
|
+
Requires-Python: >=3.10
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
|
-
Requires-Dist: aiohttp<3.
|
|
15
|
-
Requires-Dist: astroid~=
|
|
16
|
-
Requires-Dist: beautifulsoup4
|
|
14
|
+
Requires-Dist: aiohttp<3.14.0,>=3.13.0
|
|
15
|
+
Requires-Dist: astroid~=4.0.2
|
|
17
16
|
Requires-Dist: black
|
|
18
17
|
Requires-Dist: click<9,>=8.0.1
|
|
19
18
|
Requires-Dist: colorama~=0.4.6
|
|
20
19
|
Requires-Dist: jinja2~=3.1.2
|
|
20
|
+
Requires-Dist: markdown-it-py<5
|
|
21
21
|
Requires-Dist: mypy~=1.13
|
|
22
22
|
Requires-Dist: pycodestyle~=2.11
|
|
23
23
|
Requires-Dist: pygments<2.20,>=2.14
|
|
24
|
-
Requires-Dist: pylint~=
|
|
24
|
+
Requires-Dist: pylint~=4.0.3
|
|
25
25
|
Requires-Dist: requests<2.33,>=2.28
|
|
26
26
|
Requires-Dist: six
|
|
27
27
|
Requires-Dist: tabulate~=0.9.0
|
|
28
28
|
Requires-Dist: toml~=0.10.2
|
|
29
29
|
Requires-Dist: typeguard<5,>=4.1.0
|
|
30
30
|
Requires-Dist: watchdog~=6.0.0
|
|
31
|
-
Requires-Dist: wrapt<
|
|
31
|
+
Requires-Dist: wrapt<3,>=1.15.0
|
|
32
32
|
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: coverage>=7.10.6; extra == "dev"
|
|
33
34
|
Requires-Dist: hypothesis; extra == "dev"
|
|
34
35
|
Requires-Dist: inflection; extra == "dev"
|
|
35
36
|
Requires-Dist: myst-parser; extra == "dev"
|
|
36
37
|
Requires-Dist: pre-commit; extra == "dev"
|
|
37
38
|
Requires-Dist: pytest; extra == "dev"
|
|
38
|
-
Requires-Dist: pytest-cov<
|
|
39
|
+
Requires-Dist: pytest-cov<7.1,>=4.0; extra == "dev"
|
|
39
40
|
Requires-Dist: pytest-mock; extra == "dev"
|
|
40
41
|
Requires-Dist: pytest-snapshot; extra == "dev"
|
|
41
42
|
Requires-Dist: websocket-client; extra == "dev"
|
|
@@ -45,7 +46,6 @@ Provides-Extra: cfg
|
|
|
45
46
|
Requires-Dist: graphviz; extra == "cfg"
|
|
46
47
|
Provides-Extra: z3
|
|
47
48
|
Requires-Dist: z3-solver; extra == "z3"
|
|
48
|
-
Requires-Dist: importlib_resources; python_version < "3.9" and extra == "z3"
|
|
49
49
|
Dynamic: license-file
|
|
50
50
|
|
|
51
51
|
# PyTA
|
|
@@ -149,6 +149,7 @@ Merrick Liu,
|
|
|
149
149
|
Wendy Liu,
|
|
150
150
|
Yibing (Amy) Lu,
|
|
151
151
|
Maria Shurui Ma,
|
|
152
|
+
Zain Mahmoud,
|
|
152
153
|
Aina Fatema Asim Merchant,
|
|
153
154
|
Karl-Alexandre Michaud,
|
|
154
155
|
Shweta Mogalapalli,
|
|
@@ -170,4 +171,5 @@ Lana Wehbeh,
|
|
|
170
171
|
Jasmine Wu,
|
|
171
172
|
Raine Yang,
|
|
172
173
|
Philippe Yu,
|
|
174
|
+
Shirley Zhang,
|
|
173
175
|
Yi Cheng (Michael) Zhao
|
|
@@ -99,6 +99,7 @@ Merrick Liu,
|
|
|
99
99
|
Wendy Liu,
|
|
100
100
|
Yibing (Amy) Lu,
|
|
101
101
|
Maria Shurui Ma,
|
|
102
|
+
Zain Mahmoud,
|
|
102
103
|
Aina Fatema Asim Merchant,
|
|
103
104
|
Karl-Alexandre Michaud,
|
|
104
105
|
Shweta Mogalapalli,
|
|
@@ -120,4 +121,5 @@ Lana Wehbeh,
|
|
|
120
121
|
Jasmine Wu,
|
|
121
122
|
Raine Yang,
|
|
122
123
|
Philippe Yu,
|
|
124
|
+
Shirley Zhang,
|
|
123
125
|
Yi Cheng (Michael) Zhao
|
|
@@ -7,36 +7,37 @@ authors = [
|
|
|
7
7
|
license = {text = "MIT"}
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
dependencies = [
|
|
10
|
-
"aiohttp >= 3.
|
|
11
|
-
"astroid ~=
|
|
12
|
-
"beautifulsoup4",
|
|
10
|
+
"aiohttp >= 3.13.0,< 3.14.0",
|
|
11
|
+
"astroid ~= 4.0.2",
|
|
13
12
|
"black",
|
|
14
13
|
"click >= 8.0.1, < 9",
|
|
15
14
|
"colorama ~= 0.4.6",
|
|
16
15
|
"jinja2 ~= 3.1.2",
|
|
16
|
+
"markdown-it-py < 5",
|
|
17
17
|
"mypy ~= 1.13",
|
|
18
18
|
"pycodestyle ~= 2.11",
|
|
19
19
|
"pygments >= 2.14,< 2.20",
|
|
20
|
-
"pylint ~=
|
|
20
|
+
"pylint ~= 4.0.3",
|
|
21
21
|
"requests >= 2.28,< 2.33",
|
|
22
22
|
"six",
|
|
23
23
|
"tabulate ~= 0.9.0",
|
|
24
24
|
"toml ~= 0.10.2",
|
|
25
25
|
"typeguard >= 4.1.0, < 5",
|
|
26
26
|
"watchdog ~= 6.0.0",
|
|
27
|
-
"wrapt >= 1.15.0, <
|
|
27
|
+
"wrapt >= 1.15.0, < 3"
|
|
28
28
|
]
|
|
29
29
|
dynamic = ["version"]
|
|
30
|
-
requires-python = ">=3.
|
|
30
|
+
requires-python = ">=3.10"
|
|
31
31
|
|
|
32
32
|
[project.optional-dependencies]
|
|
33
33
|
dev = [
|
|
34
|
+
"coverage >= 7.10.6",
|
|
34
35
|
"hypothesis",
|
|
35
36
|
"inflection",
|
|
36
37
|
"myst-parser",
|
|
37
38
|
"pre-commit",
|
|
38
39
|
"pytest",
|
|
39
|
-
"pytest-cov >= 4.0,<
|
|
40
|
+
"pytest-cov >= 4.0,< 7.1",
|
|
40
41
|
"pytest-mock",
|
|
41
42
|
"pytest-snapshot",
|
|
42
43
|
"websocket-client",
|
|
@@ -48,7 +49,6 @@ cfg = [
|
|
|
48
49
|
]
|
|
49
50
|
z3 = [
|
|
50
51
|
"z3-solver",
|
|
51
|
-
"importlib_resources ; python_version<'3.9'"
|
|
52
52
|
]
|
|
53
53
|
|
|
54
54
|
[project.scripts]
|
|
@@ -82,7 +82,7 @@ extend-exclude = '''
|
|
|
82
82
|
^/tests/fixtures/
|
|
83
83
|
'''
|
|
84
84
|
line-length = 100
|
|
85
|
-
target-version = ['
|
|
85
|
+
target-version = ['py310', 'py311', 'py312', 'py313', 'py314']
|
|
86
86
|
|
|
87
87
|
|
|
88
88
|
[tool.isort]
|
|
@@ -17,7 +17,7 @@ if __name__ == '__main__':
|
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
-
__version__ = "2.
|
|
20
|
+
__version__ = "2.12.0" # Version number
|
|
21
21
|
# First, remove underscore from builtins if it has been bound in the REPL.
|
|
22
22
|
# Must appear before other imports from pylint/python_ta.
|
|
23
23
|
import builtins
|
|
@@ -13,7 +13,7 @@ from typing import TYPE_CHECKING, Any, Optional
|
|
|
13
13
|
|
|
14
14
|
import graphviz
|
|
15
15
|
from astroid import nodes
|
|
16
|
-
from astroid.
|
|
16
|
+
from astroid.manager import AstroidManager
|
|
17
17
|
|
|
18
18
|
from .visitor import CFGVisitor
|
|
19
19
|
|
|
@@ -72,7 +72,7 @@ def _generate(
|
|
|
72
72
|
return
|
|
73
73
|
|
|
74
74
|
file_name = os.path.splitext(os.path.basename(abs_path))[0]
|
|
75
|
-
module =
|
|
75
|
+
module = AstroidManager().ast_from_file(abs_path)
|
|
76
76
|
|
|
77
77
|
# invoke Z3Visitor if z3 dependency is enabled
|
|
78
78
|
if z3_enabled:
|
|
@@ -93,7 +93,7 @@ def check_file(
|
|
|
93
93
|
|
|
94
94
|
# At this point, the only possible errors are those from parsing the config file
|
|
95
95
|
# so print them, if there are any.
|
|
96
|
-
if current_reporter.
|
|
96
|
+
if current_reporter.has_messages():
|
|
97
97
|
current_reporter.print_messages()
|
|
98
98
|
else:
|
|
99
99
|
linter.set_reporter(current_reporter)
|
|
@@ -291,8 +291,6 @@ def reset_linter(
|
|
|
291
291
|
load_config(linter, default_config_path)
|
|
292
292
|
# If we do specify to load the default config, we just need to override the options later.
|
|
293
293
|
set_config = override_config
|
|
294
|
-
if default_config_path in linter.reporter.messages:
|
|
295
|
-
del linter.reporter.messages[default_config_path]
|
|
296
294
|
|
|
297
295
|
if isinstance(config, str) and config != "":
|
|
298
296
|
set_config(linter, config)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
from re import sub
|
|
4
4
|
from typing import Union
|
|
5
5
|
|
|
6
|
-
from astroid import BoundMethod,
|
|
6
|
+
from astroid import BoundMethod, nodes
|
|
7
7
|
from pylint.checkers import BaseChecker
|
|
8
8
|
from pylint.checkers.utils import only_required_for_messages, safe_infer
|
|
9
9
|
from pylint.lint import PyLinter
|
|
@@ -73,7 +73,7 @@ class IOFunctionChecker(BaseChecker):
|
|
|
73
73
|
def _resolve_qualname(node: nodes.Call) -> Union[str, None]:
|
|
74
74
|
"""Resolves the qualified name for function and method calls"""
|
|
75
75
|
if (inferred_definition := safe_infer(node.func)) is not None:
|
|
76
|
-
if isinstance(inferred_definition, (BoundMethod, FunctionDef)):
|
|
76
|
+
if isinstance(inferred_definition, (BoundMethod, nodes.FunctionDef)):
|
|
77
77
|
return sub(r"^[^.]*\.", "", inferred_definition.qname())
|
|
78
78
|
if isinstance(node.func, nodes.Name):
|
|
79
79
|
return node.func.name
|
|
@@ -9,7 +9,7 @@ from astroid import nodes
|
|
|
9
9
|
from pylint.checkers import BaseChecker
|
|
10
10
|
from pylint.checkers.base import UpperCaseStyle
|
|
11
11
|
from pylint.checkers.base.name_checker.checker import DEFAULT_PATTERNS
|
|
12
|
-
from pylint.checkers.utils import is_builtin
|
|
12
|
+
from pylint.checkers.utils import is_builtin, only_required_for_messages
|
|
13
13
|
|
|
14
14
|
from python_ta.utils import _is_in_main
|
|
15
15
|
|
|
@@ -33,26 +33,31 @@ class GlobalVariablesChecker(BaseChecker):
|
|
|
33
33
|
super().__init__(linter)
|
|
34
34
|
self.import_names = []
|
|
35
35
|
|
|
36
|
+
@only_required_for_messages("forbidden-global-variables")
|
|
36
37
|
def visit_global(self, node: nodes.Global) -> None:
|
|
37
38
|
args = "the keyword 'global' is used on line {}".format(node.lineno)
|
|
38
39
|
self.add_message("forbidden-global-variables", node=node, args=args)
|
|
39
40
|
|
|
41
|
+
@only_required_for_messages("forbidden-global-variables")
|
|
40
42
|
def visit_assignname(self, node: nodes.AssignName) -> None:
|
|
41
43
|
"""Allow global constant variables (uppercase) and type aliases (type alias pattern), but issue messages for
|
|
42
44
|
all other globals.
|
|
43
45
|
"""
|
|
44
46
|
self._inspect_vars(node)
|
|
45
47
|
|
|
48
|
+
@only_required_for_messages("forbidden-global-variables")
|
|
46
49
|
def visit_name(self, node: nodes.Name) -> None:
|
|
47
50
|
"""Allow global constant variables (uppercase) and type aliases (type alias pattern), but issue messages for
|
|
48
51
|
all other globals.
|
|
49
52
|
"""
|
|
50
53
|
self._inspect_vars(node)
|
|
51
54
|
|
|
55
|
+
@only_required_for_messages("forbidden-global-variables")
|
|
52
56
|
def visit_import(self, node: nodes.Import) -> None:
|
|
53
57
|
"""Save the names of imports, to prevent mistaking for global vars."""
|
|
54
58
|
self._store_name_or_alias(node)
|
|
55
59
|
|
|
60
|
+
@only_required_for_messages("forbidden-global-variables")
|
|
56
61
|
def visit_importfrom(self, node: nodes.ImportFrom) -> None:
|
|
57
62
|
"""Save the names of imports, to prevent mistaking for global vars."""
|
|
58
63
|
self._store_name_or_alias(node)
|
|
@@ -5,9 +5,32 @@ from typing import Optional
|
|
|
5
5
|
|
|
6
6
|
from astroid import BoundMethod, InferenceError, UnboundMethod, bases, nodes, util
|
|
7
7
|
from pylint.checkers import BaseChecker, utils
|
|
8
|
+
from pylint.checkers.utils import only_required_for_messages
|
|
8
9
|
from pylint.interfaces import INFERENCE
|
|
9
10
|
from pylint.lint import PyLinter
|
|
10
11
|
|
|
12
|
+
IMMUTABLE_TYPES = (
|
|
13
|
+
int,
|
|
14
|
+
float,
|
|
15
|
+
bool,
|
|
16
|
+
complex,
|
|
17
|
+
str,
|
|
18
|
+
bytes,
|
|
19
|
+
tuple,
|
|
20
|
+
type(None),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
CONST_NODES = (
|
|
24
|
+
nodes.Module,
|
|
25
|
+
nodes.GeneratorExp,
|
|
26
|
+
nodes.Lambda,
|
|
27
|
+
nodes.FunctionDef,
|
|
28
|
+
nodes.ClassDef,
|
|
29
|
+
bases.Generator,
|
|
30
|
+
UnboundMethod,
|
|
31
|
+
BoundMethod,
|
|
32
|
+
)
|
|
33
|
+
|
|
11
34
|
|
|
12
35
|
class InfiniteLoopChecker(BaseChecker):
|
|
13
36
|
name = "infinite-loop"
|
|
@@ -20,8 +43,13 @@ class InfiniteLoopChecker(BaseChecker):
|
|
|
20
43
|
),
|
|
21
44
|
}
|
|
22
45
|
|
|
46
|
+
@only_required_for_messages("infinite-loop")
|
|
23
47
|
def visit_while(self, node: nodes.While) -> None:
|
|
24
|
-
checks = [
|
|
48
|
+
checks = [
|
|
49
|
+
self._check_condition_constant,
|
|
50
|
+
self._check_condition_all_var_used,
|
|
51
|
+
self._check_immutable_cond_var_reassigned,
|
|
52
|
+
]
|
|
25
53
|
any(check(node) for check in checks)
|
|
26
54
|
|
|
27
55
|
def _check_condition_all_var_used(self, node: nodes.While) -> bool:
|
|
@@ -38,6 +66,9 @@ class InfiniteLoopChecker(BaseChecker):
|
|
|
38
66
|
cond_vars.add(child.name)
|
|
39
67
|
if not cond_vars:
|
|
40
68
|
return False
|
|
69
|
+
inferred_test = infer_condition(node)
|
|
70
|
+
if not inferred_test:
|
|
71
|
+
return False
|
|
41
72
|
# Check to see if condition variable(s) used inside body
|
|
42
73
|
for child in node.body:
|
|
43
74
|
for name_node in child.nodes_of_class((nodes.Name, nodes.AssignName)):
|
|
@@ -45,10 +76,7 @@ class InfiniteLoopChecker(BaseChecker):
|
|
|
45
76
|
# At least one condition variable is used in the loop body
|
|
46
77
|
return False
|
|
47
78
|
else:
|
|
48
|
-
self.add_message(
|
|
49
|
-
"infinite-loop",
|
|
50
|
-
node=node.test,
|
|
51
|
-
)
|
|
79
|
+
self.add_message("infinite-loop", node=node.test, confidence=INFERENCE)
|
|
52
80
|
return True
|
|
53
81
|
|
|
54
82
|
def _check_condition_constant(self, node: nodes.While) -> bool:
|
|
@@ -62,14 +90,9 @@ class InfiniteLoopChecker(BaseChecker):
|
|
|
62
90
|
node.test
|
|
63
91
|
) and not self._check_constant_form_condition(node):
|
|
64
92
|
return False
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
(isinstance(inferred, nodes.Const) and bool(inferred.value) is False)
|
|
70
|
-
or (isinstance(inferred, (nodes.List, nodes.Tuple, nodes.Set)) and not inferred.elts)
|
|
71
|
-
or (isinstance(inferred, nodes.Dict) and not inferred.items)
|
|
72
|
-
):
|
|
93
|
+
|
|
94
|
+
inferred = infer_condition(node)
|
|
95
|
+
if not inferred:
|
|
73
96
|
return False
|
|
74
97
|
|
|
75
98
|
check_nodes = (nodes.Break, nodes.Return, nodes.Raise, nodes.Yield)
|
|
@@ -83,10 +106,9 @@ class InfiniteLoopChecker(BaseChecker):
|
|
|
83
106
|
and isinstance(exit_node.func, nodes.Attribute)
|
|
84
107
|
and exit_node.func.attrname == "exit"
|
|
85
108
|
):
|
|
86
|
-
inferred =
|
|
109
|
+
inferred = get_safely_inferred(exit_node.func.expr)
|
|
87
110
|
if (
|
|
88
|
-
not
|
|
89
|
-
and inferred is not None
|
|
111
|
+
inferred is not None
|
|
90
112
|
and isinstance(inferred, nodes.Module)
|
|
91
113
|
and inferred.name == "sys"
|
|
92
114
|
):
|
|
@@ -104,16 +126,6 @@ class InfiniteLoopChecker(BaseChecker):
|
|
|
104
126
|
|
|
105
127
|
See `https://github.com/pylint-dev/pylint/blob/main/pylint/checkers/base/basic_checker.py#L303` for further
|
|
106
128
|
detail."""
|
|
107
|
-
const_nodes = (
|
|
108
|
-
nodes.Module,
|
|
109
|
-
nodes.GeneratorExp,
|
|
110
|
-
nodes.Lambda,
|
|
111
|
-
nodes.FunctionDef,
|
|
112
|
-
nodes.ClassDef,
|
|
113
|
-
bases.Generator,
|
|
114
|
-
UnboundMethod,
|
|
115
|
-
BoundMethod,
|
|
116
|
-
)
|
|
117
129
|
structs = (nodes.Dict, nodes.Tuple, nodes.Set, nodes.List)
|
|
118
130
|
except_nodes = (
|
|
119
131
|
nodes.Call,
|
|
@@ -124,7 +136,7 @@ class InfiniteLoopChecker(BaseChecker):
|
|
|
124
136
|
)
|
|
125
137
|
inferred = None
|
|
126
138
|
maybe_generator_call = None
|
|
127
|
-
emit = isinstance(test_node, (nodes.Const, *structs, *
|
|
139
|
+
emit = isinstance(test_node, (nodes.Const, *structs, *CONST_NODES))
|
|
128
140
|
if not isinstance(test_node, except_nodes):
|
|
129
141
|
inferred = utils.safe_infer(test_node)
|
|
130
142
|
# If we can't infer what the value is but the test is just a variable name
|
|
@@ -150,7 +162,7 @@ class InfiniteLoopChecker(BaseChecker):
|
|
|
150
162
|
return True
|
|
151
163
|
if emit:
|
|
152
164
|
return True
|
|
153
|
-
elif isinstance(inferred,
|
|
165
|
+
elif isinstance(inferred, CONST_NODES):
|
|
154
166
|
return True
|
|
155
167
|
return False
|
|
156
168
|
|
|
@@ -186,6 +198,81 @@ class InfiniteLoopChecker(BaseChecker):
|
|
|
186
198
|
maybe_generator_call = lookup_result[1][0].parent.value
|
|
187
199
|
return emit, maybe_generator_call
|
|
188
200
|
|
|
201
|
+
def _check_immutable_cond_var_reassigned(self, node: nodes.While) -> bool:
|
|
202
|
+
"""Helper function that checks if a while-loop condition uses only immutable variables
|
|
203
|
+
and none of them are reassigned inside the loop body.
|
|
204
|
+
|
|
205
|
+
Flags loops that meet **both** of the following criteria:
|
|
206
|
+
- All variables in the `while` condition are immutable (int, float, complex, bool,
|
|
207
|
+
str, bytes, tuple, or NoneType)
|
|
208
|
+
- None of these variables are reassigned in the loop body"""
|
|
209
|
+
immutable_vars = set()
|
|
210
|
+
for child in node.test.nodes_of_class(nodes.Name):
|
|
211
|
+
if isinstance(child.parent, nodes.Call) and child.parent.func is child:
|
|
212
|
+
continue
|
|
213
|
+
try:
|
|
214
|
+
inferred_values = list(child.infer())
|
|
215
|
+
except InferenceError:
|
|
216
|
+
return False
|
|
217
|
+
for inferred in inferred_values:
|
|
218
|
+
if inferred is util.Uninferable or not _is_immutable_node(inferred):
|
|
219
|
+
# Return False when the node may evaluate to a mutable object
|
|
220
|
+
return False
|
|
221
|
+
immutable_vars.add(child.name)
|
|
222
|
+
if not immutable_vars:
|
|
223
|
+
return False
|
|
224
|
+
inferred_test = infer_condition(node)
|
|
225
|
+
if not inferred_test:
|
|
226
|
+
return False
|
|
227
|
+
|
|
228
|
+
for child in node.body:
|
|
229
|
+
for assign_node in child.nodes_of_class(nodes.AssignName):
|
|
230
|
+
if assign_node.name in immutable_vars:
|
|
231
|
+
return False
|
|
232
|
+
else:
|
|
233
|
+
self.add_message(
|
|
234
|
+
"infinite-loop",
|
|
235
|
+
node=node.test,
|
|
236
|
+
confidence=INFERENCE,
|
|
237
|
+
)
|
|
238
|
+
return True
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _is_immutable_node(node: nodes.NodeNG) -> bool:
|
|
242
|
+
"""Helper used to check whether node represents an immutable type."""
|
|
243
|
+
return (isinstance(node, nodes.Const) and type(node.value) in IMMUTABLE_TYPES) or isinstance(
|
|
244
|
+
node, nodes.Tuple
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def get_safely_inferred(node: nodes.NodeNG) -> Optional[nodes.NodeNG]:
|
|
249
|
+
"""Helper used to safely infer a node with `astroid.safe_infer`. Return None if inference failed."""
|
|
250
|
+
inferred = utils.safe_infer(node)
|
|
251
|
+
if isinstance(inferred, util.UninferableBase) or inferred is None:
|
|
252
|
+
return None
|
|
253
|
+
else:
|
|
254
|
+
return inferred
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def infer_condition(node: nodes.While) -> bool:
|
|
258
|
+
"""Helper used to safely infer the value of a loop condition. Return False if inference failed or condition
|
|
259
|
+
evaluated to be false."""
|
|
260
|
+
try:
|
|
261
|
+
inferred_values = list(node.test.infer())
|
|
262
|
+
except InferenceError:
|
|
263
|
+
return True
|
|
264
|
+
for inferred in inferred_values:
|
|
265
|
+
if inferred is util.Uninferable:
|
|
266
|
+
continue
|
|
267
|
+
if (
|
|
268
|
+
(isinstance(inferred, nodes.Const) and bool(inferred.value))
|
|
269
|
+
or (isinstance(inferred, (nodes.List, nodes.Tuple, nodes.Set)) and bool(inferred.elts))
|
|
270
|
+
or (isinstance(inferred, nodes.Dict) and bool(inferred.items))
|
|
271
|
+
or (isinstance(inferred, CONST_NODES))
|
|
272
|
+
):
|
|
273
|
+
return True
|
|
274
|
+
return False
|
|
275
|
+
|
|
189
276
|
|
|
190
277
|
def register(linter: PyLinter) -> None:
|
|
191
278
|
linter.register_checker(InfiniteLoopChecker(linter))
|
|
@@ -18,7 +18,7 @@ class PycodestyleChecker(BaseRawFileChecker):
|
|
|
18
18
|
Use options to specify the list of PEP8 errors to ignore"""
|
|
19
19
|
|
|
20
20
|
name = "pep8_errors"
|
|
21
|
-
msgs = {"E9989": ("
|
|
21
|
+
msgs = {"E9989": ("%s (pycodestyle code %s)", "pep8-errors", "")}
|
|
22
22
|
|
|
23
23
|
options = (
|
|
24
24
|
(
|
|
@@ -40,15 +40,15 @@ class PycodestyleChecker(BaseRawFileChecker):
|
|
|
40
40
|
)
|
|
41
41
|
report = style_guide.check_files()
|
|
42
42
|
|
|
43
|
-
for line_num, msg, code in report.get_file_results():
|
|
44
|
-
self.add_message("pep8-errors", line=line_num, args=(
|
|
43
|
+
for line_num, msg, code, offset in report.get_file_results():
|
|
44
|
+
self.add_message("pep8-errors", col_offset=offset, line=line_num, args=(msg, code))
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
class JSONReport(pycodestyle.StandardReport):
|
|
48
48
|
def get_file_results(self) -> list[tuple]:
|
|
49
49
|
self._deferred_print.sort()
|
|
50
50
|
return [
|
|
51
|
-
(line_number,
|
|
51
|
+
(line_number, text, code, offset)
|
|
52
52
|
for line_number, offset, code, text, _ in self._deferred_print
|
|
53
53
|
]
|
|
54
54
|
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Checker that checks for nested if statements that can be simplified"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from astroid import nodes
|
|
6
|
+
from pylint.checkers import BaseChecker
|
|
7
|
+
from pylint.checkers.utils import only_required_for_messages
|
|
8
|
+
from pylint.lint import PyLinter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SimplifiableIfChecker(BaseChecker):
|
|
12
|
+
"""A checker class that reports nested if statements that can be simplified"""
|
|
13
|
+
|
|
14
|
+
name = "simplifiable-if"
|
|
15
|
+
msgs = {
|
|
16
|
+
"E9930": (
|
|
17
|
+
"This nested `if` statement can be simplified. Combine the inner condition with the outer condition using the `and` operator and remove the nested `if` statement.",
|
|
18
|
+
"simplifiable-if",
|
|
19
|
+
"Used when an `if` or `elif` branch only contains a single nested `if` statement with a single branch.",
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@only_required_for_messages("simplifiable-if")
|
|
24
|
+
def visit_if(self, node: nodes.If) -> None:
|
|
25
|
+
if (
|
|
26
|
+
len(node.body) == 1
|
|
27
|
+
and not node.has_elif_block()
|
|
28
|
+
and not node.orelse
|
|
29
|
+
and isinstance(node.body[0], nodes.If)
|
|
30
|
+
and not node.body[0].orelse
|
|
31
|
+
):
|
|
32
|
+
inner_node = node.body[0]
|
|
33
|
+
self.add_message(
|
|
34
|
+
"simplifiable-if",
|
|
35
|
+
node=inner_node.test,
|
|
36
|
+
line=inner_node.lineno,
|
|
37
|
+
col_offset=inner_node.col_offset,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def register(linter: PyLinter) -> None:
|
|
42
|
+
linter.register_checker(SimplifiableIfChecker(linter))
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Checker to check for unncessary f-strings that only consist of single expressions"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from astroid import nodes
|
|
6
|
+
from pylint.checkers import BaseChecker
|
|
7
|
+
from pylint.checkers.utils import only_required_for_messages
|
|
8
|
+
from pylint.lint import PyLinter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FormattedStringChecker(BaseChecker):
|
|
12
|
+
"""A checker class that reports unnecessary uses of f-strings when they can be replaced
|
|
13
|
+
with the expression directly"""
|
|
14
|
+
|
|
15
|
+
name = "unnecessary-f-string"
|
|
16
|
+
msgs = {
|
|
17
|
+
"E9920": (
|
|
18
|
+
'Unnecessary use of an f-string in the expression `f"{%s}"`. Use `str(%s)` instead.',
|
|
19
|
+
"unnecessary-f-string",
|
|
20
|
+
"Used when the use of an f-string is unnecessary and can be replaced with the variable directly",
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@only_required_for_messages("unnecessary-f-string")
|
|
25
|
+
def visit_joinedstr(self, node: nodes.JoinedStr) -> None:
|
|
26
|
+
if (
|
|
27
|
+
len(node.values) == 1
|
|
28
|
+
and isinstance(node.values[0], nodes.FormattedValue)
|
|
29
|
+
and node.values[0].conversion == -1
|
|
30
|
+
and node.values[0].format_spec is None
|
|
31
|
+
):
|
|
32
|
+
expression = node.values[0].value.as_string()
|
|
33
|
+
self.add_message(
|
|
34
|
+
"unnecessary-f-string",
|
|
35
|
+
node=node,
|
|
36
|
+
args=(expression, expression),
|
|
37
|
+
line=node.lineno,
|
|
38
|
+
col_offset=node.col_offset,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def register(linter: PyLinter) -> None:
|
|
43
|
+
linter.register_checker(FormattedStringChecker(linter))
|
|
@@ -74,12 +74,12 @@ ignore-module-names =
|
|
|
74
74
|
|
|
75
75
|
# Disable the message, report, category or checker with the given id(s).
|
|
76
76
|
disable=
|
|
77
|
-
E0100, E0105, E0106, E0110, E0112, E0113, E0114, E0115,
|
|
77
|
+
E0100, E0105, E0106, E0110, E0112, E0113, E0114, E0115, E0117, E0118,
|
|
78
78
|
E0236, E0237, E0238, E0240, E0242, E0243, E0244, E0305, E0308, E0309, E0310, E0311, E0312, E0313,
|
|
79
79
|
E0402,
|
|
80
80
|
E0603, E0604, E0605, E0606,
|
|
81
81
|
E0703, W0707,
|
|
82
|
-
E1124, E1125, E1132, E1139, E1142,
|
|
82
|
+
E1124, E1125, E1132, E1139, E1142, E1145,
|
|
83
83
|
E1200, E1201, E1205, E1206,
|
|
84
84
|
E1300, E1301, E1302, E1303, E1304,
|
|
85
85
|
W1406,
|
|
@@ -113,7 +113,8 @@ disable=
|
|
|
113
113
|
unnecessary-dunder-call,
|
|
114
114
|
unsupported_version,
|
|
115
115
|
E2502, E2510, E2511, E2512, E2513, E2514, E2515,
|
|
116
|
-
missing-timeout, positional-only-arguments-expected
|
|
116
|
+
missing-timeout, positional-only-arguments-expected,
|
|
117
|
+
match_statements,
|
|
117
118
|
|
|
118
119
|
|
|
119
120
|
# Enable single-letter identifiers
|
|
@@ -72,6 +72,7 @@ W0301 = "You should remove this semicolon. In Python, statements are separated b
|
|
|
72
72
|
C0301 = "This line is %s characters long, exceeding the limit of %s characters."
|
|
73
73
|
C0303 = "Trailing whitespace is poor formatting, and should be removed."
|
|
74
74
|
C0304 = "By convention, each Python file should end with a blank line. To fix this, go down to the bottom of your file and press Enter/Return to add an empty line."
|
|
75
|
+
C0305 = "By convention, each Python file should end with exactly one blank line. To fix this, go down to the bottom of your file and delete the extra blank lines so that there is only one left."
|
|
75
76
|
C0321 = "Avoid writing multiple statements on a single line. This makes your code harder to understand."
|
|
76
77
|
E0401 = "Unable to import %s. Check the spelling of the module name, or whether the module is installed (if it is a module you did not write), or whether the module is in the same directory as this file (if it is a module you did write)."
|
|
77
78
|
W0401 = "Wildcard (*) imports add all objects from the imported module as global variables, which may result in name collisions. You should only import from %s what you need, or import the whole module (\"import module\") and use dot notation to access the module's functions/classes."
|