python-ta 2.10.0__tar.gz → 2.11.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.10.0/python_ta.egg-info → python_ta-2.11.0}/PKG-INFO +13 -5
- {python_ta-2.10.0 → python_ta-2.11.0}/README.md +4 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/pyproject.toml +8 -4
- python_ta-2.11.0/python_ta/__init__.py +207 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/__main__.py +1 -1
- python_ta-2.11.0/python_ta/cfg/__init__.py +13 -0
- python_ta-2.11.0/python_ta/cfg/__main__.py +92 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/cfg/cfg_generator.py +29 -16
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/cfg/graph.py +46 -34
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/cfg/visitor.py +61 -6
- python_ta-2.11.0/python_ta/check/helpers.py +481 -0
- python_ta-2.11.0/python_ta/check/watch.py +96 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/condition_logic_checker.py +23 -19
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/forbidden_import_checker.py +5 -3
- python_ta-2.11.0/python_ta/checkers/forbidden_io_function_checker.py +85 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/forbidden_python_syntax_checker.py +1 -2
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/global_variables_checker.py +5 -3
- python_ta-2.11.0/python_ta/checkers/infinite_loop_checker.py +191 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/invalid_for_target_checker.py +1 -2
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/invalid_name_checker.py +4 -2
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/one_iteration_checker.py +1 -2
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/possibly_undefined_checker.py +5 -4
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/pycodestyle_checker.py +6 -2
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/redundant_assignment_checker.py +4 -2
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/shadowing_in_comprehension_checker.py +1 -2
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/static_type_checker.py +0 -1
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/type_annotation_checker.py +1 -2
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/unmentioned_parameter_checker.py +6 -4
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/unnecessary_indexing_checker.py +5 -4
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/config/.pylintrc +7 -3
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/config/__init__.py +17 -11
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/config/messages_config.toml +0 -43
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/contracts/__init__.py +39 -2
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/debug/accumulation_table.py +48 -20
- python_ta-2.11.0/python_ta/debug/id_tracker.py +47 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/debug/recursion_table.py +4 -2
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/debug/snapshot.py +40 -20
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/debug/snapshot_tracer.py +7 -3
- python_ta-2.11.0/python_ta/debug/webstepper/index.bundle.js +103 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/patches/__init__.py +3 -6
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/patches/checkers.py +1 -2
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/patches/transforms.py +7 -7
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/reporters/color_reporter.py +6 -1
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/reporters/core.py +29 -20
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/reporters/html_reporter.py +41 -5
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/reporters/json_reporter.py +11 -3
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/reporters/node_printers.py +52 -58
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/reporters/plain_reporter.py +11 -1
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/reporters/templates/script.js +45 -0
- python_ta-2.11.0/python_ta/reporters/templates/stylesheet.css +1013 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/reporters/templates/template.html.jinja +38 -44
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/transforms/z3_visitor.py +5 -2
- python_ta-2.11.0/python_ta/util/autoformat.py +23 -0
- python_ta-2.11.0/python_ta/util/servers/one_shot_server.py +32 -0
- python_ta-2.11.0/python_ta/util/servers/persistent_server.py +72 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/utils.py +0 -1
- python_ta-2.11.0/python_ta/z3/__init__.py +0 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/z3/z3_parser.py +23 -0
- {python_ta-2.10.0 → python_ta-2.11.0/python_ta.egg-info}/PKG-INFO +13 -5
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta.egg-info/SOURCES.txt +9 -3
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta.egg-info/requires.txt +6 -3
- {python_ta-2.10.0 → python_ta-2.11.0}/tests/test_black.py +23 -4
- python_ta-2.11.0/tests/test_check.py +513 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/tests/test_examples.py +18 -4
- {python_ta-2.10.0 → python_ta-2.11.0}/tests/test_init_logging.py +41 -14
- {python_ta-2.10.0 → python_ta-2.11.0}/tests/test_main.py +1 -1
- {python_ta-2.10.0 → python_ta-2.11.0}/tests/test_z3_visitor.py +57 -1
- python_ta-2.10.0/python_ta/__init__.py +0 -534
- python_ta-2.10.0/python_ta/cfg/__init__.py +0 -3
- python_ta-2.10.0/python_ta/checkers/forbidden_io_function_checker.py +0 -80
- python_ta-2.10.0/python_ta/debug/webstepper/index.bundle.js +0 -76
- python_ta-2.10.0/python_ta/patches/error_messages.py +0 -20
- python_ta-2.10.0/python_ta/reporters/html_server.py +0 -58
- python_ta-2.10.0/python_ta/reporters/stat_reporter.py +0 -30
- python_ta-2.10.0/python_ta/reporters/templates/stylesheet.css +0 -649
- python_ta-2.10.0/tests/test_check.py +0 -214
- {python_ta-2.10.0 → python_ta-2.11.0}/LICENSE +0 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/MANIFEST.in +0 -0
- {python_ta-2.10.0/python_ta/checkers → python_ta-2.11.0/python_ta/check}/__init__.py +0 -0
- {python_ta-2.10.0/python_ta/transforms → python_ta-2.11.0/python_ta/checkers}/__init__.py +0 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/inconsistent_or_missing_returns_checker.py +0 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/invalid_range_index_checker.py +0 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/missing_space_in_doctest_checker.py +0 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/top_level_code_checker.py +0 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/contracts/__main__.py +0 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/debug/__init__.py +0 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/debug/webstepper/99ee5c67fd0c522b4b6a.png +0 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/debug/webstepper/fd6133fe40f4f90440d6.png +0 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/debug/webstepper/index.bundle.js.LICENSE.txt +0 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/debug/webstepper/index.html +0 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/patches/messages.py +0 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/reporters/__init__.py +0 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/reporters/templates/pyta_logo_markdown.png +0 -0
- {python_ta-2.10.0/python_ta/util → python_ta-2.11.0/python_ta/transforms}/__init__.py +0 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/transforms/setendings.py +0 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/upload.py +0 -0
- {python_ta-2.10.0/python_ta/z3 → python_ta-2.11.0/python_ta/util}/__init__.py +0 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/util/tree.py +0 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta.egg-info/dependency_links.txt +0 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta.egg-info/entry_points.txt +0 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta.egg-info/not-zip-safe +0 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/python_ta.egg-info/top_level.txt +0 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/setup.cfg +0 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/setup.py +0 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/tests/test_setendings.py +0 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/tests/test_subclass_contracts.py +0 -0
- {python_ta-2.10.0 → python_ta-2.11.0}/tests/test_validate_invariants.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: python-ta
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.11.0
|
|
4
4
|
Summary: Code checking tool for teaching Python
|
|
5
5
|
Author-email: David Liu <david@cs.toronto.edu>
|
|
6
6
|
License: MIT
|
|
@@ -11,7 +11,10 @@ Project-URL: Changelog, https://github.com/pyta-uoft/pyta/blob/master/CHANGELOG.
|
|
|
11
11
|
Requires-Python: >=3.9
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
|
+
Requires-Dist: aiohttp<3.13.0,>=3.11.18
|
|
14
15
|
Requires-Dist: astroid~=3.3.5
|
|
16
|
+
Requires-Dist: beautifulsoup4
|
|
17
|
+
Requires-Dist: black
|
|
15
18
|
Requires-Dist: click<9,>=8.0.1
|
|
16
19
|
Requires-Dist: colorama~=0.4.6
|
|
17
20
|
Requires-Dist: jinja2~=3.1.2
|
|
@@ -24,18 +27,18 @@ Requires-Dist: six
|
|
|
24
27
|
Requires-Dist: tabulate~=0.9.0
|
|
25
28
|
Requires-Dist: toml~=0.10.2
|
|
26
29
|
Requires-Dist: typeguard<5,>=4.1.0
|
|
30
|
+
Requires-Dist: watchdog~=6.0.0
|
|
27
31
|
Requires-Dist: wrapt<2,>=1.15.0
|
|
28
|
-
Requires-Dist: black
|
|
29
|
-
Requires-Dist: beautifulsoup4
|
|
30
32
|
Provides-Extra: dev
|
|
31
33
|
Requires-Dist: hypothesis; extra == "dev"
|
|
32
34
|
Requires-Dist: inflection; extra == "dev"
|
|
33
35
|
Requires-Dist: myst-parser; extra == "dev"
|
|
34
36
|
Requires-Dist: pre-commit; extra == "dev"
|
|
35
37
|
Requires-Dist: pytest; extra == "dev"
|
|
36
|
-
Requires-Dist: pytest-cov<6.
|
|
38
|
+
Requires-Dist: pytest-cov<6.3,>=4.0; extra == "dev"
|
|
37
39
|
Requires-Dist: pytest-mock; extra == "dev"
|
|
38
40
|
Requires-Dist: pytest-snapshot; extra == "dev"
|
|
41
|
+
Requires-Dist: websocket-client; extra == "dev"
|
|
39
42
|
Requires-Dist: sphinx; extra == "dev"
|
|
40
43
|
Requires-Dist: sphinx-rtd-theme; extra == "dev"
|
|
41
44
|
Provides-Extra: cfg
|
|
@@ -43,6 +46,7 @@ Requires-Dist: graphviz; extra == "cfg"
|
|
|
43
46
|
Provides-Extra: z3
|
|
44
47
|
Requires-Dist: z3-solver; extra == "z3"
|
|
45
48
|
Requires-Dist: importlib_resources; python_version < "3.9" and extra == "z3"
|
|
49
|
+
Dynamic: license-file
|
|
46
50
|
|
|
47
51
|
# PyTA
|
|
48
52
|
|
|
@@ -119,6 +123,7 @@ included as a library). In the Python interpreter, try running:
|
|
|
119
123
|
|
|
120
124
|
## Contributors
|
|
121
125
|
|
|
126
|
+
Ibrahim Bilal,
|
|
122
127
|
Lorena Buciu,
|
|
123
128
|
Simon Chen,
|
|
124
129
|
Freeman Cheng,
|
|
@@ -127,6 +132,7 @@ Yianni Culmone,
|
|
|
127
132
|
Daniel Dervishi,
|
|
128
133
|
Nigel Fong,
|
|
129
134
|
Adam Gleizer,
|
|
135
|
+
James Han,
|
|
130
136
|
Ibrahim Hasan,
|
|
131
137
|
Niayesh Ilkhani,
|
|
132
138
|
Craig Katsube,
|
|
@@ -144,6 +150,7 @@ Wendy Liu,
|
|
|
144
150
|
Yibing (Amy) Lu,
|
|
145
151
|
Maria Shurui Ma,
|
|
146
152
|
Aina Fatema Asim Merchant,
|
|
153
|
+
Karl-Alexandre Michaud,
|
|
147
154
|
Shweta Mogalapalli,
|
|
148
155
|
Ignas Panero Armoska,
|
|
149
156
|
Justin Park,
|
|
@@ -156,6 +163,7 @@ Richard Shi,
|
|
|
156
163
|
Kavin Singh,
|
|
157
164
|
Alexey Strokach,
|
|
158
165
|
Sophy Sun,
|
|
166
|
+
Ali Towaiji,
|
|
159
167
|
Utku Egemen Umut,
|
|
160
168
|
Sarah Wang,
|
|
161
169
|
Lana Wehbeh,
|
|
@@ -73,6 +73,7 @@ included as a library). In the Python interpreter, try running:
|
|
|
73
73
|
|
|
74
74
|
## Contributors
|
|
75
75
|
|
|
76
|
+
Ibrahim Bilal,
|
|
76
77
|
Lorena Buciu,
|
|
77
78
|
Simon Chen,
|
|
78
79
|
Freeman Cheng,
|
|
@@ -81,6 +82,7 @@ Yianni Culmone,
|
|
|
81
82
|
Daniel Dervishi,
|
|
82
83
|
Nigel Fong,
|
|
83
84
|
Adam Gleizer,
|
|
85
|
+
James Han,
|
|
84
86
|
Ibrahim Hasan,
|
|
85
87
|
Niayesh Ilkhani,
|
|
86
88
|
Craig Katsube,
|
|
@@ -98,6 +100,7 @@ Wendy Liu,
|
|
|
98
100
|
Yibing (Amy) Lu,
|
|
99
101
|
Maria Shurui Ma,
|
|
100
102
|
Aina Fatema Asim Merchant,
|
|
103
|
+
Karl-Alexandre Michaud,
|
|
101
104
|
Shweta Mogalapalli,
|
|
102
105
|
Ignas Panero Armoska,
|
|
103
106
|
Justin Park,
|
|
@@ -110,6 +113,7 @@ Richard Shi,
|
|
|
110
113
|
Kavin Singh,
|
|
111
114
|
Alexey Strokach,
|
|
112
115
|
Sophy Sun,
|
|
116
|
+
Ali Towaiji,
|
|
113
117
|
Utku Egemen Umut,
|
|
114
118
|
Sarah Wang,
|
|
115
119
|
Lana Wehbeh,
|
|
@@ -7,7 +7,10 @@ authors = [
|
|
|
7
7
|
license = {text = "MIT"}
|
|
8
8
|
readme = "README.md"
|
|
9
9
|
dependencies = [
|
|
10
|
+
"aiohttp >= 3.11.18,< 3.13.0",
|
|
10
11
|
"astroid ~= 3.3.5",
|
|
12
|
+
"beautifulsoup4",
|
|
13
|
+
"black",
|
|
11
14
|
"click >= 8.0.1, < 9",
|
|
12
15
|
"colorama ~= 0.4.6",
|
|
13
16
|
"jinja2 ~= 3.1.2",
|
|
@@ -20,9 +23,8 @@ dependencies = [
|
|
|
20
23
|
"tabulate ~= 0.9.0",
|
|
21
24
|
"toml ~= 0.10.2",
|
|
22
25
|
"typeguard >= 4.1.0, < 5",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
"beautifulsoup4"
|
|
26
|
+
"watchdog ~= 6.0.0",
|
|
27
|
+
"wrapt >= 1.15.0, < 2"
|
|
26
28
|
]
|
|
27
29
|
dynamic = ["version"]
|
|
28
30
|
requires-python = ">=3.9"
|
|
@@ -34,9 +36,10 @@ dev = [
|
|
|
34
36
|
"myst-parser",
|
|
35
37
|
"pre-commit",
|
|
36
38
|
"pytest",
|
|
37
|
-
"pytest-cov >= 4.0,< 6.
|
|
39
|
+
"pytest-cov >= 4.0,< 6.3",
|
|
38
40
|
"pytest-mock",
|
|
39
41
|
"pytest-snapshot",
|
|
42
|
+
"websocket-client",
|
|
40
43
|
"sphinx",
|
|
41
44
|
"sphinx-rtd-theme",
|
|
42
45
|
]
|
|
@@ -76,6 +79,7 @@ include = ["python_ta*"]
|
|
|
76
79
|
[tool.black]
|
|
77
80
|
extend-exclude = '''
|
|
78
81
|
^/examples/
|
|
82
|
+
^/tests/fixtures/
|
|
79
83
|
'''
|
|
80
84
|
line-length = 100
|
|
81
85
|
target-version = ['py38']
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Python Teaching Assistant
|
|
2
|
+
|
|
3
|
+
The goal of this module is to provide automated feedback to students in our
|
|
4
|
+
introductory Python courses, using static analysis of their code.
|
|
5
|
+
|
|
6
|
+
To run the checker, call the check function on the name of the module to check.
|
|
7
|
+
|
|
8
|
+
> import python_ta
|
|
9
|
+
> python_ta.check_all('mymodule.py')
|
|
10
|
+
|
|
11
|
+
Or, put the following code in your Python module:
|
|
12
|
+
|
|
13
|
+
if __name__ == '__main__':
|
|
14
|
+
import python_ta
|
|
15
|
+
python_ta.check_all()
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
__version__ = "2.11.0" # Version number
|
|
21
|
+
# First, remove underscore from builtins if it has been bound in the REPL.
|
|
22
|
+
# Must appear before other imports from pylint/python_ta.
|
|
23
|
+
import builtins
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
del builtins._
|
|
27
|
+
except AttributeError:
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
import logging
|
|
32
|
+
import webbrowser
|
|
33
|
+
from typing import IO, TYPE_CHECKING, Any, Literal, Optional, Union
|
|
34
|
+
|
|
35
|
+
from .check.helpers import (
|
|
36
|
+
check_file,
|
|
37
|
+
get_file_paths,
|
|
38
|
+
get_valid_files_to_check,
|
|
39
|
+
setup_linter,
|
|
40
|
+
upload_linter_results,
|
|
41
|
+
verify_pre_check,
|
|
42
|
+
)
|
|
43
|
+
from .check.watch import watch_files
|
|
44
|
+
|
|
45
|
+
if TYPE_CHECKING:
|
|
46
|
+
from .reporters.core import PythonTaReporter
|
|
47
|
+
|
|
48
|
+
HELP_URL = "http://www.cs.toronto.edu/~david/pyta/checkers/index.html"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def check_errors(
|
|
52
|
+
module_name: Union[list[str], str] = "",
|
|
53
|
+
config: Union[dict[str, Any], str] = "",
|
|
54
|
+
output: Optional[Union[str, IO]] = None,
|
|
55
|
+
load_default_config: bool = True,
|
|
56
|
+
autoformat: Optional[bool] = False,
|
|
57
|
+
on_verify_fail: Literal["log", "raise"] = "log",
|
|
58
|
+
) -> PythonTaReporter:
|
|
59
|
+
"""Check a module for errors, printing a report."""
|
|
60
|
+
return _check(
|
|
61
|
+
module_name=module_name,
|
|
62
|
+
level="error",
|
|
63
|
+
local_config=config,
|
|
64
|
+
output=output,
|
|
65
|
+
load_default_config=load_default_config,
|
|
66
|
+
autoformat=autoformat,
|
|
67
|
+
on_verify_fail=on_verify_fail,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def check_all(
|
|
72
|
+
module_name: Union[list[str], str] = "",
|
|
73
|
+
config: Union[dict[str, Any], str] = "",
|
|
74
|
+
output: Optional[Union[str, IO]] = None,
|
|
75
|
+
load_default_config: bool = True,
|
|
76
|
+
autoformat: Optional[bool] = False,
|
|
77
|
+
on_verify_fail: Literal["log", "raise"] = "log",
|
|
78
|
+
) -> PythonTaReporter:
|
|
79
|
+
"""Analyse one or more Python modules for code issues and display the results.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
module_name:
|
|
83
|
+
If an empty string (default), the module where this function is called is checked.
|
|
84
|
+
If a non-empty string, it is interpreted as a path to a single Python module or a
|
|
85
|
+
directory containing Python modules. If the latter, all Python modules in the directory
|
|
86
|
+
are checked.
|
|
87
|
+
If a list of strings, each string is interpreted as a path to a module or directory,
|
|
88
|
+
and all modules across all paths are checked.
|
|
89
|
+
config:
|
|
90
|
+
If a string, a path to a configuration file to use.
|
|
91
|
+
If a dictionary, a map of configuration options (each key is the name of an option).
|
|
92
|
+
output:
|
|
93
|
+
If a string, a path to a file to which the PythonTA report is written.
|
|
94
|
+
If a typing.IO object, the report is written to this stream.
|
|
95
|
+
If None, the report is written to standard out or automatically displayed in a
|
|
96
|
+
web browser, depending on which reporter is used.
|
|
97
|
+
load_default_config:
|
|
98
|
+
If True (default), additional configuration passed with the ``config`` option is
|
|
99
|
+
merged with the default PythonTA configuration file.
|
|
100
|
+
If False, the default PythonTA configuration is not used.
|
|
101
|
+
autoformat:
|
|
102
|
+
If True, autoformat all modules using the black formatting tool before analyzing code.
|
|
103
|
+
on_verify_fail:
|
|
104
|
+
Determines how to handle files that cannot be checked. If set to "log" (default), an error
|
|
105
|
+
message is logged and execution continues. If set to "raise", an error is raised immediately to stop
|
|
106
|
+
execution.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
The ``PythonTaReporter`` object that generated the report.
|
|
110
|
+
"""
|
|
111
|
+
return _check(
|
|
112
|
+
module_name=module_name,
|
|
113
|
+
level="all",
|
|
114
|
+
local_config=config,
|
|
115
|
+
output=output,
|
|
116
|
+
load_default_config=load_default_config,
|
|
117
|
+
autoformat=autoformat,
|
|
118
|
+
on_verify_fail=on_verify_fail,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _check(
|
|
123
|
+
module_name: Union[list[str], str] = "",
|
|
124
|
+
level: str = "all",
|
|
125
|
+
local_config: Union[dict[str, Any], str] = "",
|
|
126
|
+
output: Optional[Union[str, IO]] = None,
|
|
127
|
+
load_default_config: bool = True,
|
|
128
|
+
autoformat: Optional[bool] = False,
|
|
129
|
+
on_verify_fail: Literal["log", "raise"] = "log",
|
|
130
|
+
) -> PythonTaReporter:
|
|
131
|
+
"""Check a module for problems, printing a report.
|
|
132
|
+
|
|
133
|
+
The `module_name` can take several inputs:
|
|
134
|
+
- string of a directory, or file to check (`.py` extension optional).
|
|
135
|
+
- list of strings of directories or files -- can have multiple.
|
|
136
|
+
- no argument -- checks the python file containing the function call.
|
|
137
|
+
`level` is used to specify which checks should be made.
|
|
138
|
+
`local_config` is a dict of config options or string (config file name).
|
|
139
|
+
`output` is an absolute or relative path to a file, or a typing.IO object to capture pyta data
|
|
140
|
+
output. If None, stdout is used.
|
|
141
|
+
`load_default_config` is used to specify whether to load the default .pylintrc file that comes
|
|
142
|
+
with PythonTA. It will load it by default.
|
|
143
|
+
`autoformat` is used to specify whether the black formatting tool is run. It is not run by default.
|
|
144
|
+
`on_verify_fail` determines how to handle files that cannot be checked. If set to "log" (default), an error
|
|
145
|
+
message is logged and execution continues. If set to "raise", an error is raised immediately to stop execution.
|
|
146
|
+
"""
|
|
147
|
+
# Configuring logger
|
|
148
|
+
logging.basicConfig(format="[%(levelname)s] %(message)s", level=logging.INFO)
|
|
149
|
+
linter, current_reporter = setup_linter(local_config, load_default_config, output)
|
|
150
|
+
try:
|
|
151
|
+
# Flag indicating whether at least one file has been checked
|
|
152
|
+
is_any_file_checked = False
|
|
153
|
+
linted_files = set()
|
|
154
|
+
f_paths = [] # Paths to files for data submission
|
|
155
|
+
for locations in get_valid_files_to_check(module_name):
|
|
156
|
+
f_paths = []
|
|
157
|
+
for file_py in get_file_paths(locations):
|
|
158
|
+
linted_files.add(file_py)
|
|
159
|
+
if not verify_pre_check(
|
|
160
|
+
file_py, linter.config.allow_pylint_comments, on_verify_fail=on_verify_fail
|
|
161
|
+
):
|
|
162
|
+
# The only way to reach this is if verify_pre_check returns False, and `on_verify_fail="log"`.
|
|
163
|
+
continue
|
|
164
|
+
is_any_file_checked, linter = check_file(
|
|
165
|
+
file_py=file_py,
|
|
166
|
+
local_config=local_config,
|
|
167
|
+
load_default_config=load_default_config,
|
|
168
|
+
autoformat=autoformat,
|
|
169
|
+
is_any_file_checked=is_any_file_checked,
|
|
170
|
+
current_reporter=current_reporter,
|
|
171
|
+
f_paths=f_paths,
|
|
172
|
+
)
|
|
173
|
+
current_reporter = linter.reporter
|
|
174
|
+
current_reporter.print_messages(level)
|
|
175
|
+
upload_linter_results(linter, current_reporter, f_paths, local_config)
|
|
176
|
+
# Only generate reports (display the webpage) if there were valid files to check
|
|
177
|
+
if is_any_file_checked:
|
|
178
|
+
linter.generate_reports()
|
|
179
|
+
if linter.config.watch:
|
|
180
|
+
watch_files(
|
|
181
|
+
file_paths=linted_files,
|
|
182
|
+
level=level,
|
|
183
|
+
local_config=local_config,
|
|
184
|
+
load_default_config=load_default_config,
|
|
185
|
+
autoformat=autoformat,
|
|
186
|
+
linter=linter,
|
|
187
|
+
f_paths=f_paths,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
return current_reporter
|
|
191
|
+
except Exception as e:
|
|
192
|
+
logging.error(
|
|
193
|
+
"Unexpected error encountered! Please report this to your instructor (and attach the code that caused the error)."
|
|
194
|
+
)
|
|
195
|
+
logging.error('Error message: "{}"'.format(e))
|
|
196
|
+
raise e
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def doc(msg_id: str) -> None:
|
|
200
|
+
"""Open the PythonTA documentation page for the given error message id.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
msg_id: The five-character error code, e.g. ``"E0401"``.
|
|
204
|
+
"""
|
|
205
|
+
msg_url = HELP_URL + "#" + msg_id.lower()
|
|
206
|
+
print("Opening {} in a browser.".format(msg_url))
|
|
207
|
+
webbrowser.open(msg_url)
|
|
@@ -37,7 +37,7 @@ CONTEXT_SETTINGS = dict(help_option_names=["-h", "--help"])
|
|
|
37
37
|
@click.option(
|
|
38
38
|
"--output-format",
|
|
39
39
|
help="Specify the format of output report. This option is ignored if a --config argument is specified.",
|
|
40
|
-
default="
|
|
40
|
+
default="pyta-html",
|
|
41
41
|
)
|
|
42
42
|
def main(
|
|
43
43
|
version: bool,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
try:
|
|
2
|
+
from .cfg_generator import generate_cfg
|
|
3
|
+
except ModuleNotFoundError:
|
|
4
|
+
|
|
5
|
+
def generate_cfg(*_args, **_kwargs) -> None:
|
|
6
|
+
"""Dummy version of generate_cfg"""
|
|
7
|
+
raise Exception(
|
|
8
|
+
"This function requires additional dependencies to be installed: " "python_ta[cfg]"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
from .graph import *
|
|
13
|
+
from .visitor import *
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
from typing import Any, Dict, List, Optional, Union
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
|
|
5
|
+
from .cfg_generator import generate_cfg
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def split_by_comma_if_outside_quotes(options_str: str) -> List[str]:
|
|
9
|
+
"""Split string by commas, but not commas inside quotes."""
|
|
10
|
+
parts: List[str] = []
|
|
11
|
+
current_part: List[str] = []
|
|
12
|
+
in_quotes: bool = False
|
|
13
|
+
quote_char: Optional[str] = None
|
|
14
|
+
|
|
15
|
+
for char in options_str:
|
|
16
|
+
if char in ('"', "'") and not in_quotes:
|
|
17
|
+
in_quotes = True
|
|
18
|
+
quote_char = char
|
|
19
|
+
current_part.append(char)
|
|
20
|
+
elif char == quote_char and in_quotes:
|
|
21
|
+
in_quotes = False
|
|
22
|
+
quote_char = None
|
|
23
|
+
current_part.append(char)
|
|
24
|
+
elif char == "," and not in_quotes:
|
|
25
|
+
parts.append("".join(current_part).strip())
|
|
26
|
+
current_part = []
|
|
27
|
+
else:
|
|
28
|
+
current_part.append(char)
|
|
29
|
+
|
|
30
|
+
if current_part:
|
|
31
|
+
parts.append("".join(current_part).strip())
|
|
32
|
+
|
|
33
|
+
return parts
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def parse_visitor_options(options_str: str) -> Dict[str, Union[bool, List[str], str]]:
|
|
37
|
+
"""Parse comma-separated key=value pairs."""
|
|
38
|
+
if not options_str:
|
|
39
|
+
return {}
|
|
40
|
+
|
|
41
|
+
parts = split_by_comma_if_outside_quotes(options_str)
|
|
42
|
+
|
|
43
|
+
options: Dict[str, Union[bool, List[str], str]] = {}
|
|
44
|
+
for part in parts:
|
|
45
|
+
if "=" not in part:
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
key, value = part.split("=", 1)
|
|
49
|
+
value = value.strip()
|
|
50
|
+
|
|
51
|
+
# Strip any quotes if present
|
|
52
|
+
if value.startswith(("'", '"')) and value.endswith(("'", '"')):
|
|
53
|
+
value = value[1:-1]
|
|
54
|
+
|
|
55
|
+
# Type conversions
|
|
56
|
+
if value.lower() in ("true", "false"):
|
|
57
|
+
options[key] = value.lower() == "true"
|
|
58
|
+
elif "," in value:
|
|
59
|
+
options[key] = [v.strip() for v in value.split(",")]
|
|
60
|
+
else:
|
|
61
|
+
options[key] = value
|
|
62
|
+
|
|
63
|
+
return options
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@click.command()
|
|
67
|
+
@click.argument("mod")
|
|
68
|
+
@click.option(
|
|
69
|
+
"--auto-open", is_flag=True, default=False, help="automatically opens the cfg in the browser"
|
|
70
|
+
)
|
|
71
|
+
@click.option(
|
|
72
|
+
"--visitor-options",
|
|
73
|
+
type=str,
|
|
74
|
+
help="Comma-separated key=value pairs for visitor options, e.g., "
|
|
75
|
+
"\"separate-condition-blocks=true,functions='foo,bar,baz'\"",
|
|
76
|
+
)
|
|
77
|
+
def main(mod: str, auto_open: bool, visitor_options: Optional[str]) -> None:
|
|
78
|
+
"""Generate a Control Flow Graph for a Python file."""
|
|
79
|
+
parsed_options: Optional[Dict[str, Any]] = None
|
|
80
|
+
|
|
81
|
+
if visitor_options:
|
|
82
|
+
try:
|
|
83
|
+
parsed_options = parse_visitor_options(visitor_options)
|
|
84
|
+
except ValueError as error:
|
|
85
|
+
click.echo(f"Error: {error}", err=True)
|
|
86
|
+
raise click.Abort()
|
|
87
|
+
|
|
88
|
+
generate_cfg(mod=mod, auto_open=auto_open, visitor_options=parsed_options)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
if __name__ == "__main__":
|
|
92
|
+
main()
|
|
@@ -6,31 +6,30 @@ from __future__ import annotations
|
|
|
6
6
|
|
|
7
7
|
import html
|
|
8
8
|
import importlib.util
|
|
9
|
+
import logging
|
|
9
10
|
import os.path
|
|
10
11
|
import sys
|
|
11
|
-
from typing import Any, Optional
|
|
12
|
+
from typing import TYPE_CHECKING, Any, Optional
|
|
12
13
|
|
|
13
14
|
import graphviz
|
|
14
15
|
from astroid import nodes
|
|
15
16
|
from astroid.builder import AstroidBuilder
|
|
16
17
|
|
|
17
|
-
try:
|
|
18
|
-
from ..transforms.z3_visitor import Z3Visitor
|
|
19
|
-
|
|
20
|
-
z3_dependency_available = True
|
|
21
|
-
except ImportError:
|
|
22
|
-
Z3Visitor = Any
|
|
23
|
-
z3_dependency_available = False
|
|
24
|
-
|
|
25
|
-
from .graph import CFGBlock, ControlFlowGraph
|
|
26
18
|
from .visitor import CFGVisitor
|
|
27
19
|
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from ..transforms.z3_visitor import Z3Visitor
|
|
22
|
+
from .graph import CFGBlock, ControlFlowGraph
|
|
23
|
+
|
|
28
24
|
GRAPH_OPTIONS = {"format": "svg", "node_attr": {"shape": "box", "fontname": "Courier New"}}
|
|
29
25
|
SUBGRAPH_OPTIONS = {"fontname": "Courier New"}
|
|
30
26
|
|
|
31
27
|
|
|
32
28
|
def generate_cfg(
|
|
33
|
-
mod: str = "",
|
|
29
|
+
mod: str = "",
|
|
30
|
+
auto_open: bool = False,
|
|
31
|
+
visitor_options: Optional[dict[str, Any]] = None,
|
|
32
|
+
z3_enabled: bool = False,
|
|
34
33
|
) -> None:
|
|
35
34
|
"""Generate a control flow graph for the given module.
|
|
36
35
|
|
|
@@ -49,12 +48,16 @@ def generate_cfg(
|
|
|
49
48
|
function is called).
|
|
50
49
|
auto_open (bool): Automatically open the graph in your browser.
|
|
51
50
|
visitor_options (dict): An options dict to configure how the cfgs are generated.
|
|
51
|
+
z3_enabled (bool): An option that enables z3 when True (by default False).
|
|
52
52
|
"""
|
|
53
|
-
_generate(mod=mod, auto_open=auto_open, visitor_options=visitor_options)
|
|
53
|
+
_generate(mod=mod, auto_open=auto_open, visitor_options=visitor_options, z3_enabled=z3_enabled)
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
def _generate(
|
|
57
|
-
mod: str = "",
|
|
57
|
+
mod: str = "",
|
|
58
|
+
auto_open: bool = False,
|
|
59
|
+
visitor_options: Optional[dict[str, Any]] = None,
|
|
60
|
+
z3_enabled: bool = False,
|
|
58
61
|
) -> None:
|
|
59
62
|
"""Generate a control flow graph for the given module.
|
|
60
63
|
|
|
@@ -71,12 +74,18 @@ def _generate(
|
|
|
71
74
|
file_name = os.path.splitext(os.path.basename(abs_path))[0]
|
|
72
75
|
module = AstroidBuilder().file_build(abs_path)
|
|
73
76
|
|
|
74
|
-
# invoke Z3Visitor if z3 dependency is
|
|
75
|
-
if
|
|
77
|
+
# invoke Z3Visitor if z3 dependency is enabled
|
|
78
|
+
if z3_enabled:
|
|
79
|
+
try:
|
|
80
|
+
from ..transforms.z3_visitor import Z3Visitor
|
|
81
|
+
|
|
82
|
+
except ImportError:
|
|
83
|
+
logging.error("Failed to import Z3Visitor. Aborting.")
|
|
84
|
+
raise
|
|
76
85
|
z3v = Z3Visitor()
|
|
77
86
|
module = z3v.visitor.visit(module)
|
|
78
87
|
|
|
79
|
-
visitor = CFGVisitor(options=visitor_options)
|
|
88
|
+
visitor = CFGVisitor(options=visitor_options, z3_enabled=z3_enabled)
|
|
80
89
|
module.accept(visitor)
|
|
81
90
|
|
|
82
91
|
_display(visitor.cfgs, file_name, auto_open=auto_open)
|
|
@@ -159,6 +168,10 @@ def _visit(block: CFGBlock, graph: graphviz.Digraph, visited: set[int], end: CFG
|
|
|
159
168
|
elif isinstance(stmt.parent, nodes.For) and stmt is stmt.parent.target:
|
|
160
169
|
label = f"< for<U><B>{html.escape(stmt.as_string())} </B></U> in {html.escape(stmt.parent.iter.as_string())}<BR/> >"
|
|
161
170
|
|
|
171
|
+
if block.statements and isinstance(block.statements[0], nodes.Pattern):
|
|
172
|
+
label = f"case {html.escape(block.statements[0].as_string())}"
|
|
173
|
+
label += f" if {block.statements[1].as_string()}" if len(block.statements) == 2 else ""
|
|
174
|
+
|
|
162
175
|
if not label: # Default
|
|
163
176
|
label = "\n".join([s.as_string() for s in block.statements]) + "\n"
|
|
164
177
|
|