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.
Files changed (102) hide show
  1. {python_ta-2.11.0/python_ta.egg-info → python_ta-2.12.0}/PKG-INFO +11 -9
  2. {python_ta-2.11.0 → python_ta-2.12.0}/README.md +2 -0
  3. {python_ta-2.11.0 → python_ta-2.12.0}/pyproject.toml +9 -9
  4. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/__init__.py +1 -1
  5. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/cfg/cfg_generator.py +2 -2
  6. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/cfg/graph.py +1 -1
  7. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/check/helpers.py +1 -3
  8. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/forbidden_io_function_checker.py +2 -2
  9. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/global_variables_checker.py +6 -1
  10. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/infinite_loop_checker.py +115 -28
  11. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/pycodestyle_checker.py +4 -4
  12. python_ta-2.12.0/python_ta/checkers/simplifiable_if_checker.py +42 -0
  13. python_ta-2.12.0/python_ta/checkers/unnecessary_f_string_checker.py +43 -0
  14. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/config/.pylintrc +4 -3
  15. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/config/messages_config.toml +1 -0
  16. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/contracts/__init__.py +125 -39
  17. python_ta-2.12.0/python_ta/debug/accumulation_table.py +296 -0
  18. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/debug/snapshot_tracer.py +44 -44
  19. python_ta-2.12.0/python_ta/debug/webstepper/webstepper_template.html.jinja +35 -0
  20. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/reporters/core.py +10 -1
  21. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/reporters/html_reporter.py +6 -1
  22. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/reporters/json_reporter.py +1 -1
  23. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/reporters/node_printers.py +271 -285
  24. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/reporters/templates/stylesheet.css +19 -0
  25. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/reporters/templates/template.html.jinja +3 -3
  26. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/transforms/setendings.py +1 -125
  27. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/util/servers/persistent_server.py +2 -1
  28. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/z3/z3_parser.py +11 -11
  29. {python_ta-2.11.0 → python_ta-2.12.0/python_ta.egg-info}/PKG-INFO +11 -9
  30. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta.egg-info/SOURCES.txt +3 -1
  31. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta.egg-info/requires.txt +7 -9
  32. {python_ta-2.11.0 → python_ta-2.12.0}/tests/test_black.py +0 -1
  33. {python_ta-2.11.0 → python_ta-2.12.0}/tests/test_check.py +3 -4
  34. {python_ta-2.11.0 → python_ta-2.12.0}/tests/test_examples.py +80 -24
  35. python_ta-2.11.0/python_ta/debug/accumulation_table.py +0 -225
  36. python_ta-2.11.0/python_ta/debug/webstepper/index.html +0 -12
  37. {python_ta-2.11.0 → python_ta-2.12.0}/LICENSE +0 -0
  38. {python_ta-2.11.0 → python_ta-2.12.0}/MANIFEST.in +0 -0
  39. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/__main__.py +0 -0
  40. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/cfg/__init__.py +0 -0
  41. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/cfg/__main__.py +0 -0
  42. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/cfg/visitor.py +0 -0
  43. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/check/__init__.py +0 -0
  44. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/check/watch.py +0 -0
  45. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/__init__.py +0 -0
  46. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/condition_logic_checker.py +0 -0
  47. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/forbidden_import_checker.py +0 -0
  48. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/forbidden_python_syntax_checker.py +0 -0
  49. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/inconsistent_or_missing_returns_checker.py +0 -0
  50. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/invalid_for_target_checker.py +0 -0
  51. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/invalid_name_checker.py +0 -0
  52. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/invalid_range_index_checker.py +0 -0
  53. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/missing_space_in_doctest_checker.py +0 -0
  54. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/one_iteration_checker.py +0 -0
  55. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/possibly_undefined_checker.py +0 -0
  56. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/redundant_assignment_checker.py +0 -0
  57. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/shadowing_in_comprehension_checker.py +0 -0
  58. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/static_type_checker.py +0 -0
  59. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/top_level_code_checker.py +0 -0
  60. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/type_annotation_checker.py +0 -0
  61. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/unmentioned_parameter_checker.py +0 -0
  62. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/checkers/unnecessary_indexing_checker.py +0 -0
  63. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/config/__init__.py +0 -0
  64. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/contracts/__main__.py +0 -0
  65. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/debug/__init__.py +0 -0
  66. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/debug/id_tracker.py +0 -0
  67. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/debug/recursion_table.py +0 -0
  68. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/debug/snapshot.py +0 -0
  69. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/debug/webstepper/99ee5c67fd0c522b4b6a.png +0 -0
  70. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/debug/webstepper/fd6133fe40f4f90440d6.png +0 -0
  71. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/debug/webstepper/index.bundle.js +0 -0
  72. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/debug/webstepper/index.bundle.js.LICENSE.txt +0 -0
  73. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/patches/__init__.py +0 -0
  74. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/patches/checkers.py +0 -0
  75. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/patches/messages.py +0 -0
  76. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/patches/transforms.py +0 -0
  77. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/reporters/__init__.py +0 -0
  78. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/reporters/color_reporter.py +0 -0
  79. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/reporters/plain_reporter.py +0 -0
  80. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/reporters/templates/pyta_logo_markdown.png +0 -0
  81. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/reporters/templates/script.js +0 -0
  82. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/transforms/__init__.py +0 -0
  83. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/transforms/z3_visitor.py +0 -0
  84. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/upload.py +0 -0
  85. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/util/__init__.py +0 -0
  86. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/util/autoformat.py +0 -0
  87. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/util/servers/one_shot_server.py +0 -0
  88. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/util/tree.py +0 -0
  89. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/utils.py +0 -0
  90. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta/z3/__init__.py +0 -0
  91. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta.egg-info/dependency_links.txt +0 -0
  92. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta.egg-info/entry_points.txt +0 -0
  93. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta.egg-info/not-zip-safe +0 -0
  94. {python_ta-2.11.0 → python_ta-2.12.0}/python_ta.egg-info/top_level.txt +0 -0
  95. {python_ta-2.11.0 → python_ta-2.12.0}/setup.cfg +0 -0
  96. {python_ta-2.11.0 → python_ta-2.12.0}/setup.py +0 -0
  97. {python_ta-2.11.0 → python_ta-2.12.0}/tests/test_init_logging.py +0 -0
  98. {python_ta-2.11.0 → python_ta-2.12.0}/tests/test_main.py +0 -0
  99. {python_ta-2.11.0 → python_ta-2.12.0}/tests/test_setendings.py +0 -0
  100. {python_ta-2.11.0 → python_ta-2.12.0}/tests/test_subclass_contracts.py +0 -0
  101. {python_ta-2.11.0 → python_ta-2.12.0}/tests/test_validate_invariants.py +0 -0
  102. {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.11.0
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.9
11
+ Requires-Python: >=3.10
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
- Requires-Dist: aiohttp<3.13.0,>=3.11.18
15
- Requires-Dist: astroid~=3.3.5
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~=3.3.1
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<2,>=1.15.0
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<6.3,>=4.0; extra == "dev"
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.18,< 3.13.0",
11
- "astroid ~= 3.3.5",
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 ~= 3.3.1",
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, < 2"
27
+ "wrapt >= 1.15.0, < 3"
28
28
  ]
29
29
  dynamic = ["version"]
30
- requires-python = ">=3.9"
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,< 6.3",
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 = ['py38']
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.11.0" # Version number
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.builder import AstroidBuilder
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 = AstroidBuilder().file_build(abs_path)
75
+ module = AstroidManager().ast_from_file(abs_path)
76
76
 
77
77
  # invoke Z3Visitor if z3 dependency is enabled
78
78
  if z3_enabled:
@@ -9,7 +9,7 @@ if TYPE_CHECKING:
9
9
  except ImportError:
10
10
  ExprRef = Any
11
11
 
12
- from astroid import (
12
+ from astroid.nodes import (
13
13
  AnnAssign,
14
14
  Arguments,
15
15
  Assign,
@@ -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.messages:
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, FunctionDef, nodes
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 = [self._check_condition_constant, self._check_condition_all_var_used]
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
- inferred = utils.safe_infer(node.test)
66
- if isinstance(inferred, util.UninferableBase) or inferred is None:
67
- return False
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 = utils.safe_infer(exit_node.func.expr)
109
+ inferred = get_safely_inferred(exit_node.func.expr)
87
110
  if (
88
- not isinstance(inferred, util.UninferableBase)
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, *const_nodes))
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, const_nodes):
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": ("Found pycodestyle (PEP8) style error %s at %s", "pep8-errors", "")}
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=(code, msg))
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, f"line {line_number}, column {offset}: {text}", code)
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, E0116, E0117, E0118,
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."