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.
Files changed (107) hide show
  1. {python_ta-2.10.0/python_ta.egg-info → python_ta-2.11.0}/PKG-INFO +13 -5
  2. {python_ta-2.10.0 → python_ta-2.11.0}/README.md +4 -0
  3. {python_ta-2.10.0 → python_ta-2.11.0}/pyproject.toml +8 -4
  4. python_ta-2.11.0/python_ta/__init__.py +207 -0
  5. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/__main__.py +1 -1
  6. python_ta-2.11.0/python_ta/cfg/__init__.py +13 -0
  7. python_ta-2.11.0/python_ta/cfg/__main__.py +92 -0
  8. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/cfg/cfg_generator.py +29 -16
  9. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/cfg/graph.py +46 -34
  10. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/cfg/visitor.py +61 -6
  11. python_ta-2.11.0/python_ta/check/helpers.py +481 -0
  12. python_ta-2.11.0/python_ta/check/watch.py +96 -0
  13. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/condition_logic_checker.py +23 -19
  14. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/forbidden_import_checker.py +5 -3
  15. python_ta-2.11.0/python_ta/checkers/forbidden_io_function_checker.py +85 -0
  16. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/forbidden_python_syntax_checker.py +1 -2
  17. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/global_variables_checker.py +5 -3
  18. python_ta-2.11.0/python_ta/checkers/infinite_loop_checker.py +191 -0
  19. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/invalid_for_target_checker.py +1 -2
  20. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/invalid_name_checker.py +4 -2
  21. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/one_iteration_checker.py +1 -2
  22. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/possibly_undefined_checker.py +5 -4
  23. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/pycodestyle_checker.py +6 -2
  24. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/redundant_assignment_checker.py +4 -2
  25. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/shadowing_in_comprehension_checker.py +1 -2
  26. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/static_type_checker.py +0 -1
  27. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/type_annotation_checker.py +1 -2
  28. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/unmentioned_parameter_checker.py +6 -4
  29. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/unnecessary_indexing_checker.py +5 -4
  30. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/config/.pylintrc +7 -3
  31. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/config/__init__.py +17 -11
  32. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/config/messages_config.toml +0 -43
  33. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/contracts/__init__.py +39 -2
  34. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/debug/accumulation_table.py +48 -20
  35. python_ta-2.11.0/python_ta/debug/id_tracker.py +47 -0
  36. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/debug/recursion_table.py +4 -2
  37. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/debug/snapshot.py +40 -20
  38. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/debug/snapshot_tracer.py +7 -3
  39. python_ta-2.11.0/python_ta/debug/webstepper/index.bundle.js +103 -0
  40. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/patches/__init__.py +3 -6
  41. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/patches/checkers.py +1 -2
  42. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/patches/transforms.py +7 -7
  43. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/reporters/color_reporter.py +6 -1
  44. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/reporters/core.py +29 -20
  45. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/reporters/html_reporter.py +41 -5
  46. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/reporters/json_reporter.py +11 -3
  47. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/reporters/node_printers.py +52 -58
  48. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/reporters/plain_reporter.py +11 -1
  49. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/reporters/templates/script.js +45 -0
  50. python_ta-2.11.0/python_ta/reporters/templates/stylesheet.css +1013 -0
  51. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/reporters/templates/template.html.jinja +38 -44
  52. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/transforms/z3_visitor.py +5 -2
  53. python_ta-2.11.0/python_ta/util/autoformat.py +23 -0
  54. python_ta-2.11.0/python_ta/util/servers/one_shot_server.py +32 -0
  55. python_ta-2.11.0/python_ta/util/servers/persistent_server.py +72 -0
  56. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/utils.py +0 -1
  57. python_ta-2.11.0/python_ta/z3/__init__.py +0 -0
  58. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/z3/z3_parser.py +23 -0
  59. {python_ta-2.10.0 → python_ta-2.11.0/python_ta.egg-info}/PKG-INFO +13 -5
  60. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta.egg-info/SOURCES.txt +9 -3
  61. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta.egg-info/requires.txt +6 -3
  62. {python_ta-2.10.0 → python_ta-2.11.0}/tests/test_black.py +23 -4
  63. python_ta-2.11.0/tests/test_check.py +513 -0
  64. {python_ta-2.10.0 → python_ta-2.11.0}/tests/test_examples.py +18 -4
  65. {python_ta-2.10.0 → python_ta-2.11.0}/tests/test_init_logging.py +41 -14
  66. {python_ta-2.10.0 → python_ta-2.11.0}/tests/test_main.py +1 -1
  67. {python_ta-2.10.0 → python_ta-2.11.0}/tests/test_z3_visitor.py +57 -1
  68. python_ta-2.10.0/python_ta/__init__.py +0 -534
  69. python_ta-2.10.0/python_ta/cfg/__init__.py +0 -3
  70. python_ta-2.10.0/python_ta/checkers/forbidden_io_function_checker.py +0 -80
  71. python_ta-2.10.0/python_ta/debug/webstepper/index.bundle.js +0 -76
  72. python_ta-2.10.0/python_ta/patches/error_messages.py +0 -20
  73. python_ta-2.10.0/python_ta/reporters/html_server.py +0 -58
  74. python_ta-2.10.0/python_ta/reporters/stat_reporter.py +0 -30
  75. python_ta-2.10.0/python_ta/reporters/templates/stylesheet.css +0 -649
  76. python_ta-2.10.0/tests/test_check.py +0 -214
  77. {python_ta-2.10.0 → python_ta-2.11.0}/LICENSE +0 -0
  78. {python_ta-2.10.0 → python_ta-2.11.0}/MANIFEST.in +0 -0
  79. {python_ta-2.10.0/python_ta/checkers → python_ta-2.11.0/python_ta/check}/__init__.py +0 -0
  80. {python_ta-2.10.0/python_ta/transforms → python_ta-2.11.0/python_ta/checkers}/__init__.py +0 -0
  81. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/inconsistent_or_missing_returns_checker.py +0 -0
  82. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/invalid_range_index_checker.py +0 -0
  83. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/missing_space_in_doctest_checker.py +0 -0
  84. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/checkers/top_level_code_checker.py +0 -0
  85. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/contracts/__main__.py +0 -0
  86. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/debug/__init__.py +0 -0
  87. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/debug/webstepper/99ee5c67fd0c522b4b6a.png +0 -0
  88. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/debug/webstepper/fd6133fe40f4f90440d6.png +0 -0
  89. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/debug/webstepper/index.bundle.js.LICENSE.txt +0 -0
  90. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/debug/webstepper/index.html +0 -0
  91. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/patches/messages.py +0 -0
  92. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/reporters/__init__.py +0 -0
  93. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/reporters/templates/pyta_logo_markdown.png +0 -0
  94. {python_ta-2.10.0/python_ta/util → python_ta-2.11.0/python_ta/transforms}/__init__.py +0 -0
  95. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/transforms/setendings.py +0 -0
  96. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/upload.py +0 -0
  97. {python_ta-2.10.0/python_ta/z3 → python_ta-2.11.0/python_ta/util}/__init__.py +0 -0
  98. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta/util/tree.py +0 -0
  99. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta.egg-info/dependency_links.txt +0 -0
  100. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta.egg-info/entry_points.txt +0 -0
  101. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta.egg-info/not-zip-safe +0 -0
  102. {python_ta-2.10.0 → python_ta-2.11.0}/python_ta.egg-info/top_level.txt +0 -0
  103. {python_ta-2.10.0 → python_ta-2.11.0}/setup.cfg +0 -0
  104. {python_ta-2.10.0 → python_ta-2.11.0}/setup.py +0 -0
  105. {python_ta-2.10.0 → python_ta-2.11.0}/tests/test_setendings.py +0 -0
  106. {python_ta-2.10.0 → python_ta-2.11.0}/tests/test_subclass_contracts.py +0 -0
  107. {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.2
1
+ Metadata-Version: 2.4
2
2
  Name: python-ta
3
- Version: 2.10.0
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.1,>=4.0; extra == "dev"
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
- "wrapt >= 1.15.0, < 2",
24
- "black",
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.1",
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="python_ta.reporters.HTMLReporter",
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 = "", auto_open: bool = False, visitor_options: Optional[dict[str, Any]] = None
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 = "", auto_open: bool = False, visitor_options: Optional[dict[str, Any]] = None
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 available
75
- if z3_dependency_available:
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