├── .gitignore ├── setup.cfg ├── .travis.yml ├── LICENCE ├── pyproject.toml ├── README.md ├── flake8_debugger.py ├── test_linter.py └── poetry.lock /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | *.pyc 3 | dist 4 | .mypy_cache -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest 3 | 4 | [flake8] 5 | max-line-length = 120 -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | - "3.7" 5 | - "3.8" 6 | - "3.9" 7 | 8 | env: 9 | matrix: 10 | - FLAKE8_VERSION="" 11 | - FLAKE8_VERSION="3.0" 12 | 13 | install: 14 | - pip install poetry 15 | - poetry install 16 | - if [[ -n "$FLAKE8_VERSION" ]]; then poetry run pip install flake8=="$FLAKE8_VERSION"; fi 17 | script: 18 | - poetry run pytest 19 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Joseph Kahn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["poetry-core>=1.0"] 3 | build-backend = "poetry.core.masonry.api" 4 | 5 | [tool.poetry] 6 | name = "flake8-debugger" 7 | version = "4.1.2" 8 | description = "ipdb/pdb statement checker plugin for flake8" 9 | readme = "README.md" 10 | 11 | license = "MIT" 12 | 13 | authors = [ 14 | "Joseph Kahn " 15 | ] 16 | 17 | repository = "https://github.com/jbkahn/flake8-debugger" 18 | homepage = "https://github.com/jbkahn/flake8-debugger" 19 | keywords = [ 20 | "flake8", 21 | "plugin", 22 | "linting", 23 | "debugger", 24 | "ipdb", 25 | "code quality" 26 | ] 27 | 28 | classifiers = [ 29 | 'Development Status :: 3 - Alpha', 30 | 'Environment :: Console', 31 | 'Framework :: Flake8', 32 | 'Intended Audience :: Developers', 33 | 'License :: OSI Approved :: MIT License', 34 | 'Operating System :: OS Independent', 35 | 'Programming Language :: Python', 36 | 'Programming Language :: Python :: 2', 37 | 'Programming Language :: Python :: 3', 38 | 'Topic :: Software Development :: Libraries :: Python Modules', 39 | 'Topic :: Software Development :: Quality Assurance', 40 | ] 41 | 42 | 43 | include = ["pyproject.toml", "flake8_debugger.py", "LICENCE", "test_linter.py"] 44 | 45 | [tool.poetry.plugins."flake8.extension"] 46 | T100 = "flake8_debugger:DebuggerChecker" 47 | 48 | [tool.poetry.dependencies] 49 | python = ">=3.7" 50 | "flake8" = ">=3.0" 51 | pycodestyle = "*" 52 | 53 | [tool.poetry.dev-dependencies] 54 | black = { version = "^22.3.0" } 55 | pytest = "*" 56 | 57 | [tool.black] 58 | line-length = 120 59 | target-version = ['py37', 'py38', 'py39', 'py310'] 60 | include = '\.pyi?$' 61 | exclude = ''' 62 | /( 63 | \.git 64 | | \.hg 65 | | \.mypy_cache 66 | | \.tox 67 | | \.venv 68 | | venv 69 | | _build 70 | | buck-out 71 | | build 72 | | dist 73 | )/ 74 | ''' 75 | skip-numeric-underscore-normalization = true 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Flake8 debugger plugin 2 | ====================== 3 | 4 | Check for pdb;idbp imports and set traces, as well as `from IPython.terminal.embed import InteractiveShellEmbed` and `InteractiveShellEmbed()()`. 5 | 6 | This module provides a plugin for ``flake8``, the Python code checker. 7 | 8 | 9 | Installation 10 | ------------ 11 | 12 | You can install or upgrade ``flake8-debugger`` with these commands:: 13 | 14 | $ pip install flake8-debugger 15 | $ pip install --upgrade flake8-debugger 16 | 17 | 18 | Plugin for Flake8 19 | ----------------- 20 | 21 | When both ``flake8 2.2`` and ``flake8-debugger`` are installed, the plugin is 22 | available in ``flake8``:: 23 | 24 | $ flake8 --version 25 | 2.0 (pep8: 1.4.5, flake8-debugger: 1.0, pyflakes: 0.6.1) 26 | 27 | 28 | Changes 29 | ------- 30 | 31 | ##### 4.1.2 - 2022-04-30 32 | 33 | * Add tests to bundle. 34 | 35 | ##### 4.1.1 - 2022-04-30 36 | 37 | * Add tests to bundle. 38 | 39 | ##### 4.1.0 - 2022-04-30 40 | 41 | * Drop support for python 3.6 and remove special handling code. 42 | * bundle licence file. 43 | 44 | ##### 4.0.0 - 2020-11-29 45 | 46 | * Opted back into using Poetry now that the existing issues have been fixed. 47 | * Python 2.7 support was no officially dropped. 48 | 49 | ##### 3.2.1 - 2019-10-31 50 | 51 | * Swapped back from poetry to setup.py :(....python ecosystem issues.... 52 | 53 | ##### 3.2.0 - 2019-10-15 54 | 55 | * Forgot to add `breakpoint` support to the last changelog entry as well as fixing a bug introduced into that version that flagged `import builtins` as noteworthy. 56 | 57 | 58 | ##### 3.1.1 - 2019-10-12 59 | 60 | * Fix reading from stdin when it is closed (requires flake8 > 2.1). 61 | * Swapped to poetry from setup.py 62 | * Ran black on the repository 63 | 64 | ##### 3.1.0 - 2018-02-11 65 | * Add a framework classifier for use in pypi.org 66 | * Fix entry_point in setup.py leaving it off by default again 67 | * Detect __import__ debugger statements 68 | * Add support for `pudb` detection 69 | 70 | ##### 3.0.0 - 2017-05-11 71 | * fix the refactor of the detector in 2.0.0 that was removed from pypi. 72 | * fix a flake8 issue that had it turned off by default. 73 | 74 | 75 | ##### 2.0.0 - 2016-09-19 76 | * refactor detector 77 | * drop official support for python 2.6 and 3.3 78 | 79 | 80 | ##### 1.4.0 - 2015-05-18 81 | * refactor detector, run tests in python 2.6, 2.7 and 3.4 as well as adding a check for InteractiveShellEmbed. 82 | 83 | ##### 1.3.2 - 2014-11-04 84 | * more tests, fix edge case and debugger identification. 85 | 86 | ##### 1.3.1 - 2014-11-04 87 | * more tests, a little refactoring and improvements in catching. 88 | 89 | ##### 1.3 - 2014-11-04 90 | * using ast instead of regular expressions 91 | 92 | ##### 1.2 - 2014-06-30 93 | * Added a few simple tests 94 | 95 | ##### 1.1 - 2014-06-30 96 | * First release 97 | 98 | ##### 1.0 - 2014-06-30 99 | * Whoops 100 | -------------------------------------------------------------------------------- /flake8_debugger.py: -------------------------------------------------------------------------------- 1 | """Extension for flake8 that finds usage of the debugger.""" 2 | import ast 3 | from itertools import chain 4 | 5 | import pycodestyle 6 | 7 | try: 8 | from flake8.engine import pep8 as stdin_utils 9 | except ImportError: 10 | from flake8 import utils as stdin_utils 11 | 12 | __version__ = "4.1.2" 13 | 14 | DEBUGGER_ERROR_CODE = "T100" 15 | 16 | debuggers = { 17 | "pdb": ["set_trace"], 18 | "pudb": ["set_trace"], 19 | "ipdb": ["set_trace", "sset_trace"], 20 | "IPython.terminal.embed": ["InteractiveShellEmbed"], 21 | "IPython.frontend.terminal.embed": ["InteractiveShellEmbed"], 22 | "celery.contrib.rdb": ["set_trace"], 23 | "builtins": ["breakpoint"] 24 | } 25 | 26 | 27 | class DebuggerFinder(ast.NodeVisitor): 28 | def __init__(self, *args, **kwargs): 29 | super(DebuggerFinder, self).__init__(*args, **kwargs) 30 | self.debuggers_used = {} 31 | self.debuggers_traces_redefined = {} 32 | self.debuggers_traces_names = {} 33 | self.debugger_traces_imported = {} 34 | self.debuggers_names = {} 35 | self.debuggers_redefined = {} 36 | self.debuggers_imported = {} 37 | 38 | def visit_Call(self, node): 39 | if getattr(node.func, "id", None) == "breakpoint": 40 | entry = self.debuggers_used.setdefault((node.lineno, node.col_offset), []) 41 | entry.append("{0} trace found: breakpoint used".format(DEBUGGER_ERROR_CODE)) 42 | 43 | debugger_method_names = list(chain(self.debuggers_traces_names.values(), *debuggers.values())) 44 | is_debugger_function = getattr(node.func, "id", None) in debugger_method_names 45 | if is_debugger_function: 46 | if node.func.id in self.debuggers_traces_names.values(): 47 | debugger_method = next( 48 | item[0] for item in self.debuggers_traces_names.items() if item[1] == node.func.id 49 | ) 50 | entry = self.debuggers_used.setdefault((node.lineno, node.col_offset), []) 51 | if debugger_method == node.func.id: 52 | entry.append("{0} trace found: {1} used".format(DEBUGGER_ERROR_CODE, node.func.id)) 53 | else: 54 | entry.append( 55 | "{0} trace found: {1} used as {2}".format(DEBUGGER_ERROR_CODE, debugger_method, node.func.id) 56 | ) 57 | 58 | is_debugger_attribute = getattr(node.func, "attr", None) in debugger_method_names 59 | if is_debugger_attribute: 60 | caller = getattr(node.func.value, "id", None) 61 | entry = self.debuggers_used.setdefault((node.lineno, node.col_offset), []) 62 | if caller in self.debuggers_names.values(): 63 | entry.append("{0} trace found: {1}.{2} used".format(DEBUGGER_ERROR_CODE, caller, node.func.attr)) 64 | else: 65 | entry.append("{0} trace found: {1} used".format(DEBUGGER_ERROR_CODE, node.func.attr)) 66 | self.generic_visit(node) 67 | 68 | def visit_Import(self, node): 69 | for name_node in node.names: 70 | if name_node.name in list(debuggers.keys()): 71 | if name_node.asname is not None: 72 | self.debuggers_names[name_node.name] = name_node.asname 73 | entry = self.debuggers_redefined.setdefault((node.lineno, node.col_offset), []) 74 | entry.append( 75 | "{0} import for {1} found as {2}".format(DEBUGGER_ERROR_CODE, name_node.name, name_node.asname) 76 | ) 77 | # Unlike the other imports, we don't want to consider all builtin imports as worthy of flagging. 78 | elif name_node.name != "builtins": 79 | self.debuggers_names[name_node.name] = name_node.name 80 | entry = self.debuggers_imported.setdefault((node.lineno, node.col_offset), []) 81 | entry.append("{0} import for {1} found".format(DEBUGGER_ERROR_CODE, name_node.name)) 82 | 83 | def visit_ImportFrom(self, node): 84 | if node.module in list(debuggers.keys()): 85 | for name_node in node.names: 86 | if name_node.name in debuggers[node.module]: 87 | if name_node.asname is not None: 88 | self.debuggers_traces_names[name_node.name] = name_node.asname 89 | entry = self.debuggers_traces_redefined.setdefault((node.lineno, node.col_offset), []) 90 | entry.append( 91 | "{0} import for {1} found as {2}".format( 92 | DEBUGGER_ERROR_CODE, name_node.name, name_node.asname 93 | ) 94 | ) 95 | else: 96 | self.debuggers_traces_names[name_node.name] = name_node.name 97 | entry = self.debugger_traces_imported.setdefault((node.lineno, node.col_offset), []) 98 | entry.append("{0} import for {1} found".format(DEBUGGER_ERROR_CODE, name_node.name)) 99 | 100 | 101 | class DebuggerChecker(object): 102 | options = None 103 | name = "flake8-debugger" 104 | version = __version__ 105 | 106 | def __init__(self, tree, filename): 107 | self.tree = tree 108 | self.filename = filename 109 | self.lines = None 110 | 111 | def load_file(self): 112 | if self.filename in ("stdin", "-", None): 113 | self.filename = "stdin" 114 | self.lines = stdin_utils.stdin_get_value().splitlines(True) 115 | else: 116 | self.lines = pycodestyle.readlines(self.filename) 117 | 118 | if not self.tree: 119 | self.tree = ast.parse("".join(self.lines)) 120 | 121 | def run(self): 122 | if not self.tree or not self.lines: 123 | self.load_file() 124 | 125 | parser = DebuggerFinder() 126 | parser.visit(self.tree) 127 | 128 | for error, messages in parser.debuggers_used.items(): 129 | if not pycodestyle.noqa(self.lines[error[0] - 1]): 130 | for message in messages: 131 | yield (error[0], error[1], message, DebuggerChecker) 132 | 133 | for error, messages in chain( 134 | parser.debuggers_traces_redefined.items(), 135 | parser.debugger_traces_imported.items(), 136 | parser.debuggers_redefined.items(), 137 | parser.debuggers_imported.items(), 138 | ): 139 | if error not in parser.debuggers_used: 140 | if not pycodestyle.noqa(self.lines[error[0] - 1]): 141 | for message in messages: 142 | yield (error[0], error[1], message, DebuggerChecker) 143 | -------------------------------------------------------------------------------- /test_linter.py: -------------------------------------------------------------------------------- 1 | import pycodestyle 2 | 3 | from flake8_debugger import DebuggerChecker 4 | 5 | import pytest 6 | 7 | 8 | class CaptureReport(pycodestyle.BaseReport): 9 | """Collect the results of the checks.""" 10 | 11 | def __init__(self, options): 12 | self._results = [] 13 | super(CaptureReport, self).__init__(options) 14 | 15 | def error(self, line_number, offset, text, check): 16 | """Store each error.""" 17 | code = super(CaptureReport, self).error(line_number, offset, text, check) 18 | if code: 19 | record = {"line": line_number, "col": offset, "message": "{0} {1}".format(code, text[5:])} 20 | self._results.append(record) 21 | return code 22 | 23 | 24 | class DebuggerTestStyleGuide(pycodestyle.StyleGuide): 25 | 26 | logical_checks = [] 27 | physical_checks = [] 28 | ast_checks = [("debugger_usage", DebuggerChecker, ["tree", "filename", "lines"])] 29 | max_line_length = None 30 | max_doc_length = None 31 | hang_closing = False 32 | verbose = False 33 | benchmark_keys = {"files": 0, "physical lines": 0, "logical lines": 0} 34 | indent_size = 4 35 | 36 | 37 | _debugger_test_style = DebuggerTestStyleGuide() 38 | 39 | 40 | def check_code_for_debugger_statements(code): 41 | """Process code using pycodestyle Checker and return all errors.""" 42 | from tempfile import NamedTemporaryFile 43 | 44 | test_file = NamedTemporaryFile(delete=False) 45 | test_file.write(code.encode()) 46 | test_file.flush() 47 | report = CaptureReport(options=_debugger_test_style) 48 | lines = [line + "\n" for line in code.split("\n")] 49 | checker = pycodestyle.Checker(filename=test_file.name, lines=lines, options=_debugger_test_style, report=report) 50 | 51 | checker.check_all() 52 | return report._results 53 | 54 | 55 | class TestQA(object): 56 | def test_catches_simple_debugger(self): 57 | result = check_code_for_debugger_statements("from ipdb import set_trace as r\nr()") 58 | 59 | expected_result = [ 60 | {"line": 2, "message": "T100 trace found: set_trace used as r", "col": 0}, 61 | {"line": 1, "message": "T100 import for set_trace found as r", "col": 0}, 62 | ] 63 | 64 | assert result == expected_result 65 | 66 | def test_catches_simple_debugger_when_called_off_lib(self): 67 | result = check_code_for_debugger_statements("import ipdb\nipdb.set_trace()") 68 | 69 | expected_result = [ 70 | {"line": 2, "message": "T100 trace found: ipdb.set_trace used", "col": 0}, 71 | {"line": 1, "message": "T100 import for ipdb found", "col": 0}, 72 | ] 73 | 74 | assert result == expected_result 75 | 76 | def test_catches_simple_debugger_when_called_off_global(self): 77 | result = check_code_for_debugger_statements("__import__('ipdb').set_trace()") 78 | 79 | expected_result = [{"line": 1, "message": "T100 trace found: set_trace used", "col": 0}] 80 | 81 | assert result == expected_result 82 | 83 | @pytest.mark.skipif(True, reason="Not supported just yet") 84 | def test_catches_simple_debugger_when_called_off_var(self): 85 | result = check_code_for_debugger_statements("import ipdb\ntest = ipdb.set_trace\ntest()") 86 | 87 | expected_result = [ 88 | {"line": 1, "message": "T100 import for ipdb found", "col": 0}, 89 | {"line": 3, "message": "T100 trace found: ipdb.set_trace used", "col": 0}, 90 | ] 91 | assert result == expected_result 92 | 93 | 94 | class TestBreakpoint(object): 95 | def test_catches_breakpoint_call_for_python_3_7_and_above(self): 96 | result = check_code_for_debugger_statements("breakpoint()") 97 | 98 | expected_result = [{"line": 1, "message": "T100 trace found: breakpoint used", "col": 0}] 99 | 100 | assert result == expected_result 101 | 102 | def test_catches_breakpoint_import(self): 103 | result = check_code_for_debugger_statements("from builtins import breakpoint") 104 | 105 | expected_result = [{"line": 1, "message": "T100 import for breakpoint found", "col": 0}] 106 | 107 | assert result == expected_result 108 | 109 | def test_allows_builtins_import(self): 110 | result = check_code_for_debugger_statements("import builtins") 111 | 112 | expected_result = [] 113 | 114 | assert result == expected_result 115 | 116 | def test_catches_breakpoint_usage_from_builtins(self): 117 | result = check_code_for_debugger_statements("import builtins\nbuiltins.breakpoint()") 118 | 119 | expected_result = [{"col": 0, "line": 2, "message": "T100 trace found: breakpoint used"}] 120 | 121 | assert result == expected_result 122 | 123 | def test_catches_breakpoint_imported_as_other_name(self): 124 | result = check_code_for_debugger_statements("from builtins import breakpoint as b\nb()") 125 | 126 | expected_result = [ 127 | {"line": 2, "message": "T100 trace found: breakpoint used as b", "col": 0}, 128 | {"line": 1, "message": "T100 import for breakpoint found as b", "col": 0}, 129 | ] 130 | 131 | assert result == expected_result 132 | 133 | 134 | class TestNoQA(object): 135 | def test_skip_import(self): 136 | result = check_code_for_debugger_statements("from ipdb import set_trace as r # noqa\nr()") 137 | 138 | expected_result = [{"line": 2, "message": "T100 trace found: set_trace used as r", "col": 0}] 139 | 140 | assert result == expected_result 141 | 142 | def test_skip_usage(self): 143 | result = check_code_for_debugger_statements("from ipdb import set_trace as r\nr() # noqa") 144 | 145 | expected_result = [{"line": 1, "message": "T100 import for set_trace found as r", "col": 0}] 146 | 147 | assert result == expected_result 148 | 149 | def test_skip_import_and_usage(self): 150 | result = check_code_for_debugger_statements("from ipdb import set_trace as r # noqa\nr() # noqa") 151 | 152 | expected_result = [] 153 | 154 | assert result == expected_result 155 | 156 | 157 | class TestImportCases(object): 158 | def test_import_multiple(self): 159 | result = check_code_for_debugger_statements("import math, ipdb, collections") 160 | assert result == [{"col": 0, "line": 1, "message": "T100 import for ipdb found"}] 161 | 162 | def test_import(self): 163 | result = check_code_for_debugger_statements("import pdb") 164 | assert result == [{"col": 0, "line": 1, "message": "T100 import for pdb found"}] 165 | 166 | def test_import_interactive_shell_embed(self): 167 | result = check_code_for_debugger_statements("from IPython.terminal.embed import InteractiveShellEmbed") 168 | assert result == [{"col": 0, "line": 1, "message": "T100 import for InteractiveShellEmbed found"}] 169 | 170 | def test_import_both_same_line(self): 171 | result = check_code_for_debugger_statements("import pdb, ipdb") 172 | result = sorted(result, key=lambda debugger: debugger["message"]) 173 | expected_result = [ 174 | {"col": 0, "line": 1, "message": "T100 import for ipdb found"}, 175 | {"col": 0, "line": 1, "message": "T100 import for pdb found"}, 176 | ] 177 | assert result == expected_result 178 | 179 | def test_import_math(self): 180 | result = check_code_for_debugger_statements("import math") 181 | assert result == [] 182 | 183 | def test_import_noqa(self): 184 | result = check_code_for_debugger_statements("import ipdb # noqa") 185 | assert result == [] 186 | 187 | 188 | class TestModuleSetTraceCases(object): 189 | def test_import_ipython_terminal_embed_use_InteractiveShellEmbed(self): 190 | result = check_code_for_debugger_statements( 191 | "from IPython.terminal.embed import InteractiveShellEmbed; InteractiveShellEmbed()()" 192 | ) 193 | 194 | expected_result = [ 195 | {"col": 58, "line": 1, "message": "T100 trace found: InteractiveShellEmbed used"}, 196 | {"col": 0, "line": 1, "message": "T100 import for InteractiveShellEmbed found"}, 197 | ] 198 | 199 | try: 200 | assert result == expected_result 201 | except AssertionError: 202 | for item in expected_result: 203 | item["col"] = 0 204 | 205 | assert result == expected_result 206 | 207 | def test_import_ipdb_use_set_trace(self): 208 | result = check_code_for_debugger_statements("import ipdb;ipdb.set_trace();") 209 | 210 | expected_result = [ 211 | {"col": 12, "line": 1, "message": "T100 trace found: ipdb.set_trace used"}, 212 | {"col": 0, "line": 1, "message": "T100 import for ipdb found"}, 213 | ] 214 | 215 | try: 216 | assert result == expected_result 217 | except AssertionError: 218 | for item in expected_result: 219 | item["col"] = 0 220 | 221 | assert result == expected_result 222 | 223 | def test_import_pdb_use_set_trace(self): 224 | result = check_code_for_debugger_statements("import pdb;pdb.set_trace();") 225 | 226 | expected_result = [ 227 | {"col": 11, "line": 1, "message": "T100 trace found: pdb.set_trace used"}, 228 | {"col": 0, "line": 1, "message": "T100 import for pdb found"}, 229 | ] 230 | 231 | try: 232 | assert result == expected_result 233 | except AssertionError: 234 | for item in expected_result: 235 | item["col"] = 0 236 | 237 | assert result == expected_result 238 | 239 | def test_import_pdb_use_set_trace_twice(self): 240 | result = check_code_for_debugger_statements("import pdb;pdb.set_trace() and pdb.set_trace();") 241 | 242 | expected_result = [ 243 | {"col": 11, "line": 1, "message": "T100 trace found: pdb.set_trace used"}, 244 | {"col": 31, "line": 1, "message": "T100 trace found: pdb.set_trace used"}, 245 | {"col": 0, "line": 1, "message": "T100 import for pdb found"}, 246 | ] 247 | 248 | try: 249 | assert result == expected_result 250 | except AssertionError: 251 | for item in expected_result: 252 | item["col"] = 0 253 | 254 | assert result == expected_result 255 | 256 | def test_import_other_module_as_set_trace_and_use_it(self): 257 | result = check_code_for_debugger_statements("from math import Max as set_trace\nset_trace()") 258 | assert result == [] 259 | 260 | def test_import_rdb_use_set_trace(self): 261 | result = check_code_for_debugger_statements("from celery.contrib import rdb;rdb.set_trace();") 262 | 263 | expected_result = [ 264 | {"col": 31, "line": 1, "message": "T100 trace found: set_trace used"}, 265 | ] 266 | 267 | try: 268 | assert result == expected_result 269 | except AssertionError: 270 | for item in expected_result: 271 | item["col"] = 0 272 | 273 | assert result == expected_result 274 | 275 | def test_from_celery_import_rdb_use_set_trace(self): 276 | result = check_code_for_debugger_statements("import celery.contrib.rdb;celery.contrib.rdb.set_trace();") 277 | 278 | expected_result = [ 279 | {"col": 26, "line": 1, "message": "T100 trace found: set_trace used"}, 280 | {"col": 0, "line": 1, "message": "T100 import for celery.contrib.rdb found"}, 281 | ] 282 | 283 | try: 284 | assert result == expected_result 285 | except AssertionError: 286 | for item in expected_result: 287 | item["col"] = 0 288 | 289 | assert result == expected_result 290 | 291 | 292 | class TestImportAsCases(object): 293 | def test_import_ipdb_as(self): 294 | result = check_code_for_debugger_statements("import math, ipdb as sif, collections") 295 | assert result == [{"col": 0, "line": 1, "message": "T100 import for ipdb found as sif"}] 296 | 297 | 298 | class TestModuleASSetTraceCases(object): 299 | def test_import_ipdb_as_use_set_trace(self): 300 | result = check_code_for_debugger_statements("import ipdb as sif;sif.set_trace();") 301 | 302 | expected_result = [ 303 | {"col": 19, "line": 1, "message": "T100 trace found: sif.set_trace used"}, 304 | {"col": 0, "line": 1, "message": "T100 import for ipdb found as sif"}, 305 | ] 306 | 307 | try: 308 | assert result == expected_result 309 | except AssertionError: 310 | for item in expected_result: 311 | item["col"] = 0 312 | 313 | assert result == expected_result 314 | 315 | 316 | class TestImportSetTraceCases(object): 317 | def test_import_set_trace_ipdb(self): 318 | result = check_code_for_debugger_statements("from ipdb import run, set_trace;set_trace();") 319 | 320 | expected_result = [ 321 | {"col": 32, "line": 1, "message": "T100 trace found: set_trace used"}, 322 | {"col": 0, "line": 1, "message": "T100 import for set_trace found"}, 323 | ] 324 | 325 | try: 326 | assert result == expected_result 327 | except AssertionError: 328 | for item in expected_result: 329 | item["col"] = 0 330 | 331 | assert result == expected_result 332 | 333 | def test_import_set_trace_pdb(self): 334 | result = check_code_for_debugger_statements("from pdb import set_trace; set_trace();") 335 | 336 | expected_result = [ 337 | {"col": 27, "line": 1, "message": "T100 trace found: set_trace used"}, 338 | {"col": 0, "line": 1, "message": "T100 import for set_trace found"}, 339 | ] 340 | 341 | try: 342 | assert result == expected_result 343 | except AssertionError: 344 | for item in expected_result: 345 | item["col"] = 0 346 | 347 | assert result == expected_result 348 | 349 | def test_import_set_trace_ipdb_as_and_use(self): 350 | result = check_code_for_debugger_statements("from ipdb import run, set_trace as sif; sif();") 351 | 352 | expected_result = [ 353 | {"col": 40, "line": 1, "message": "T100 trace found: set_trace used as sif"}, 354 | {"col": 0, "line": 1, "message": "T100 import for set_trace found as sif"}, 355 | ] 356 | 357 | try: 358 | assert result == expected_result 359 | except AssertionError: 360 | for item in expected_result: 361 | item["col"] = 0 362 | 363 | assert result == expected_result 364 | 365 | def test_import_set_trace_ipdb_as_and_use_with_conjunction_and(self): 366 | result = check_code_for_debugger_statements("from ipdb import run, set_trace as sif; True and sif();") 367 | 368 | expected_result = [ 369 | {"col": 49, "line": 1, "message": "T100 trace found: set_trace used as sif"}, 370 | {"col": 0, "line": 1, "message": "T100 import for set_trace found as sif"}, 371 | ] 372 | 373 | try: 374 | assert result == expected_result 375 | except AssertionError: 376 | for item in expected_result: 377 | item["col"] = 0 378 | 379 | assert result == expected_result 380 | 381 | def test_import_set_trace_ipdb_as_and_use_with_conjunction_or(self): 382 | result = check_code_for_debugger_statements("from ipdb import run, set_trace as sif; True or sif();") 383 | 384 | expected_result = [ 385 | {"col": 48, "line": 1, "message": "T100 trace found: set_trace used as sif"}, 386 | {"col": 0, "line": 1, "message": "T100 import for set_trace found as sif"}, 387 | ] 388 | 389 | try: 390 | assert result == expected_result 391 | except AssertionError: 392 | for item in expected_result: 393 | item["col"] = 0 394 | 395 | assert result == expected_result 396 | 397 | def test_import_set_trace_ipdb_as_and_use_with_conjunction_or_noqa(self): 398 | result = check_code_for_debugger_statements("from ipdb import run, set_trace as sif; True or sif(); # noqa") 399 | try: 400 | assert result == [] 401 | except AssertionError: 402 | pass 403 | 404 | def test_import_set_trace_ipdb_as_and_use_with_conjunction_or_noqa_import_only(self): 405 | result = check_code_for_debugger_statements("from ipdb import run, set_trace as sif # noqa\nTrue or sif()") 406 | 407 | expected_result = [{"col": 8, "line": 2, "message": "T100 trace found: set_trace used as sif"}] 408 | 409 | try: 410 | assert result == expected_result 411 | except AssertionError: 412 | for item in expected_result: 413 | item["col"] = 0 414 | 415 | assert result == expected_result 416 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "atomicwrites" 3 | version = "1.4.0" 4 | description = "Atomic file writes." 5 | category = "dev" 6 | optional = false 7 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 8 | 9 | [[package]] 10 | name = "attrs" 11 | version = "21.4.0" 12 | description = "Classes Without Boilerplate" 13 | category = "dev" 14 | optional = false 15 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 16 | 17 | [package.extras] 18 | dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] 19 | docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] 20 | tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] 21 | tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] 22 | 23 | [[package]] 24 | name = "black" 25 | version = "22.3.0" 26 | description = "The uncompromising code formatter." 27 | category = "dev" 28 | optional = false 29 | python-versions = ">=3.6.2" 30 | 31 | [package.dependencies] 32 | click = ">=8.0.0" 33 | mypy-extensions = ">=0.4.3" 34 | pathspec = ">=0.9.0" 35 | platformdirs = ">=2" 36 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 37 | typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""} 38 | typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} 39 | 40 | [package.extras] 41 | colorama = ["colorama (>=0.4.3)"] 42 | d = ["aiohttp (>=3.7.4)"] 43 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 44 | uvloop = ["uvloop (>=0.15.2)"] 45 | 46 | [[package]] 47 | name = "click" 48 | version = "8.1.3" 49 | description = "Composable command line interface toolkit" 50 | category = "dev" 51 | optional = false 52 | python-versions = ">=3.7" 53 | 54 | [package.dependencies] 55 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 56 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 57 | 58 | [[package]] 59 | name = "colorama" 60 | version = "0.4.4" 61 | description = "Cross-platform colored terminal text." 62 | category = "dev" 63 | optional = false 64 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 65 | 66 | [[package]] 67 | name = "flake8" 68 | version = "4.0.1" 69 | description = "the modular source code checker: pep8 pyflakes and co" 70 | category = "main" 71 | optional = false 72 | python-versions = ">=3.6" 73 | 74 | [package.dependencies] 75 | importlib-metadata = {version = "<4.3", markers = "python_version < \"3.8\""} 76 | mccabe = ">=0.6.0,<0.7.0" 77 | pycodestyle = ">=2.8.0,<2.9.0" 78 | pyflakes = ">=2.4.0,<2.5.0" 79 | 80 | [[package]] 81 | name = "importlib-metadata" 82 | version = "4.2.0" 83 | description = "Read metadata from Python packages" 84 | category = "main" 85 | optional = false 86 | python-versions = ">=3.6" 87 | 88 | [package.dependencies] 89 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 90 | zipp = ">=0.5" 91 | 92 | [package.extras] 93 | docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"] 94 | testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"] 95 | 96 | [[package]] 97 | name = "iniconfig" 98 | version = "1.1.1" 99 | description = "iniconfig: brain-dead simple config-ini parsing" 100 | category = "dev" 101 | optional = false 102 | python-versions = "*" 103 | 104 | [[package]] 105 | name = "mccabe" 106 | version = "0.6.1" 107 | description = "McCabe checker, plugin for flake8" 108 | category = "main" 109 | optional = false 110 | python-versions = "*" 111 | 112 | [[package]] 113 | name = "mypy-extensions" 114 | version = "0.4.3" 115 | description = "Experimental type system extensions for programs checked with the mypy typechecker." 116 | category = "dev" 117 | optional = false 118 | python-versions = "*" 119 | 120 | [[package]] 121 | name = "packaging" 122 | version = "21.3" 123 | description = "Core utilities for Python packages" 124 | category = "dev" 125 | optional = false 126 | python-versions = ">=3.6" 127 | 128 | [package.dependencies] 129 | pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" 130 | 131 | [[package]] 132 | name = "pathspec" 133 | version = "0.9.0" 134 | description = "Utility library for gitignore style pattern matching of file paths." 135 | category = "dev" 136 | optional = false 137 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 138 | 139 | [[package]] 140 | name = "platformdirs" 141 | version = "2.5.2" 142 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 143 | category = "dev" 144 | optional = false 145 | python-versions = ">=3.7" 146 | 147 | [package.extras] 148 | docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] 149 | test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] 150 | 151 | [[package]] 152 | name = "pluggy" 153 | version = "1.0.0" 154 | description = "plugin and hook calling mechanisms for python" 155 | category = "dev" 156 | optional = false 157 | python-versions = ">=3.6" 158 | 159 | [package.dependencies] 160 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 161 | 162 | [package.extras] 163 | dev = ["pre-commit", "tox"] 164 | testing = ["pytest", "pytest-benchmark"] 165 | 166 | [[package]] 167 | name = "py" 168 | version = "1.11.0" 169 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 170 | category = "dev" 171 | optional = false 172 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 173 | 174 | [[package]] 175 | name = "pycodestyle" 176 | version = "2.8.0" 177 | description = "Python style guide checker" 178 | category = "main" 179 | optional = false 180 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 181 | 182 | [[package]] 183 | name = "pyflakes" 184 | version = "2.4.0" 185 | description = "passive checker of Python programs" 186 | category = "main" 187 | optional = false 188 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 189 | 190 | [[package]] 191 | name = "pyparsing" 192 | version = "3.0.8" 193 | description = "pyparsing module - Classes and methods to define and execute parsing grammars" 194 | category = "dev" 195 | optional = false 196 | python-versions = ">=3.6.8" 197 | 198 | [package.extras] 199 | diagrams = ["railroad-diagrams", "jinja2"] 200 | 201 | [[package]] 202 | name = "pytest" 203 | version = "7.1.2" 204 | description = "pytest: simple powerful testing with Python" 205 | category = "dev" 206 | optional = false 207 | python-versions = ">=3.7" 208 | 209 | [package.dependencies] 210 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 211 | attrs = ">=19.2.0" 212 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 213 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 214 | iniconfig = "*" 215 | packaging = "*" 216 | pluggy = ">=0.12,<2.0" 217 | py = ">=1.8.2" 218 | tomli = ">=1.0.0" 219 | 220 | [package.extras] 221 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] 222 | 223 | [[package]] 224 | name = "tomli" 225 | version = "2.0.1" 226 | description = "A lil' TOML parser" 227 | category = "dev" 228 | optional = false 229 | python-versions = ">=3.7" 230 | 231 | [[package]] 232 | name = "typed-ast" 233 | version = "1.5.3" 234 | description = "a fork of Python 2 and 3 ast modules with type comment support" 235 | category = "dev" 236 | optional = false 237 | python-versions = ">=3.6" 238 | 239 | [[package]] 240 | name = "typing-extensions" 241 | version = "4.2.0" 242 | description = "Backported and Experimental Type Hints for Python 3.7+" 243 | category = "main" 244 | optional = false 245 | python-versions = ">=3.7" 246 | 247 | [[package]] 248 | name = "zipp" 249 | version = "3.8.0" 250 | description = "Backport of pathlib-compatible object wrapper for zip files" 251 | category = "main" 252 | optional = false 253 | python-versions = ">=3.7" 254 | 255 | [package.extras] 256 | docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] 257 | testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] 258 | 259 | [metadata] 260 | lock-version = "1.1" 261 | python-versions = ">=3.7" 262 | content-hash = "ad4dc8113cc28c9e84d1e5cc868cf0ed3bd91ba8dde7748de7155ea4b651e1ba" 263 | 264 | [metadata.files] 265 | atomicwrites = [ 266 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 267 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 268 | ] 269 | attrs = [ 270 | {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, 271 | {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, 272 | ] 273 | black = [ 274 | {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"}, 275 | {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"}, 276 | {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"}, 277 | {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"}, 278 | {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"}, 279 | {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"}, 280 | {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"}, 281 | {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"}, 282 | {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"}, 283 | {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"}, 284 | {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"}, 285 | {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"}, 286 | {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"}, 287 | {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"}, 288 | {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"}, 289 | {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"}, 290 | {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"}, 291 | {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"}, 292 | {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"}, 293 | {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"}, 294 | {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"}, 295 | {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"}, 296 | {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"}, 297 | ] 298 | click = [ 299 | {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, 300 | {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, 301 | ] 302 | colorama = [ 303 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 304 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 305 | ] 306 | flake8 = [ 307 | {file = "flake8-4.0.1-py2.py3-none-any.whl", hash = "sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d"}, 308 | {file = "flake8-4.0.1.tar.gz", hash = "sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"}, 309 | ] 310 | importlib-metadata = [ 311 | {file = "importlib_metadata-4.2.0-py3-none-any.whl", hash = "sha256:057e92c15bc8d9e8109738a48db0ccb31b4d9d5cfbee5a8670879a30be66304b"}, 312 | {file = "importlib_metadata-4.2.0.tar.gz", hash = "sha256:b7e52a1f8dec14a75ea73e0891f3060099ca1d8e6a462a4dff11c3e119ea1b31"}, 313 | ] 314 | iniconfig = [ 315 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 316 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 317 | ] 318 | mccabe = [ 319 | {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, 320 | {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, 321 | ] 322 | mypy-extensions = [ 323 | {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, 324 | {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, 325 | ] 326 | packaging = [ 327 | {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, 328 | {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, 329 | ] 330 | pathspec = [ 331 | {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, 332 | {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, 333 | ] 334 | platformdirs = [ 335 | {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, 336 | {file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"}, 337 | ] 338 | pluggy = [ 339 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 340 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 341 | ] 342 | py = [ 343 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 344 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 345 | ] 346 | pycodestyle = [ 347 | {file = "pycodestyle-2.8.0-py2.py3-none-any.whl", hash = "sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20"}, 348 | {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, 349 | ] 350 | pyflakes = [ 351 | {file = "pyflakes-2.4.0-py2.py3-none-any.whl", hash = "sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"}, 352 | {file = "pyflakes-2.4.0.tar.gz", hash = "sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c"}, 353 | ] 354 | pyparsing = [ 355 | {file = "pyparsing-3.0.8-py3-none-any.whl", hash = "sha256:ef7b523f6356f763771559412c0d7134753f037822dad1b16945b7b846f7ad06"}, 356 | {file = "pyparsing-3.0.8.tar.gz", hash = "sha256:7bf433498c016c4314268d95df76c81b842a4cb2b276fa3312cfb1e1d85f6954"}, 357 | ] 358 | pytest = [ 359 | {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, 360 | {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, 361 | ] 362 | tomli = [ 363 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 364 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 365 | ] 366 | typed-ast = [ 367 | {file = "typed_ast-1.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ad3b48cf2b487be140072fb86feff36801487d4abb7382bb1929aaac80638ea"}, 368 | {file = "typed_ast-1.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:542cd732351ba8235f20faa0fc7398946fe1a57f2cdb289e5497e1e7f48cfedb"}, 369 | {file = "typed_ast-1.5.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc2c11ae59003d4a26dda637222d9ae924387f96acae9492df663843aefad55"}, 370 | {file = "typed_ast-1.5.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd5df1313915dbd70eaaa88c19030b441742e8b05e6103c631c83b75e0435ccc"}, 371 | {file = "typed_ast-1.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:e34f9b9e61333ecb0f7d79c21c28aa5cd63bec15cb7e1310d7d3da6ce886bc9b"}, 372 | {file = "typed_ast-1.5.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f818c5b81966d4728fec14caa338e30a70dfc3da577984d38f97816c4b3071ec"}, 373 | {file = "typed_ast-1.5.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3042bfc9ca118712c9809201f55355479cfcdc17449f9f8db5e744e9625c6805"}, 374 | {file = "typed_ast-1.5.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:4fff9fdcce59dc61ec1b317bdb319f8f4e6b69ebbe61193ae0a60c5f9333dc49"}, 375 | {file = "typed_ast-1.5.3-cp36-cp36m-win_amd64.whl", hash = "sha256:8e0b8528838ffd426fea8d18bde4c73bcb4167218998cc8b9ee0a0f2bfe678a6"}, 376 | {file = "typed_ast-1.5.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8ef1d96ad05a291f5c36895d86d1375c0ee70595b90f6bb5f5fdbee749b146db"}, 377 | {file = "typed_ast-1.5.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed44e81517364cb5ba367e4f68fca01fba42a7a4690d40c07886586ac267d9b9"}, 378 | {file = "typed_ast-1.5.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f60d9de0d087454c91b3999a296d0c4558c1666771e3460621875021bf899af9"}, 379 | {file = "typed_ast-1.5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:9e237e74fd321a55c90eee9bc5d44be976979ad38a29bbd734148295c1ce7617"}, 380 | {file = "typed_ast-1.5.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ee852185964744987609b40aee1d2eb81502ae63ee8eef614558f96a56c1902d"}, 381 | {file = "typed_ast-1.5.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:27e46cdd01d6c3a0dd8f728b6a938a6751f7bd324817501c15fb056307f918c6"}, 382 | {file = "typed_ast-1.5.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d64dabc6336ddc10373922a146fa2256043b3b43e61f28961caec2a5207c56d5"}, 383 | {file = "typed_ast-1.5.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8cdf91b0c466a6c43f36c1964772918a2c04cfa83df8001ff32a89e357f8eb06"}, 384 | {file = "typed_ast-1.5.3-cp38-cp38-win_amd64.whl", hash = "sha256:9cc9e1457e1feb06b075c8ef8aeb046a28ec351b1958b42c7c31c989c841403a"}, 385 | {file = "typed_ast-1.5.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e20d196815eeffb3d76b75223e8ffed124e65ee62097e4e73afb5fec6b993e7a"}, 386 | {file = "typed_ast-1.5.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:37e5349d1d5de2f4763d534ccb26809d1c24b180a477659a12c4bde9dd677d74"}, 387 | {file = "typed_ast-1.5.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9f1a27592fac87daa4e3f16538713d705599b0a27dfe25518b80b6b017f0a6d"}, 388 | {file = "typed_ast-1.5.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8831479695eadc8b5ffed06fdfb3e424adc37962a75925668deeb503f446c0a3"}, 389 | {file = "typed_ast-1.5.3-cp39-cp39-win_amd64.whl", hash = "sha256:20d5118e494478ef2d3a2702d964dae830aedd7b4d3b626d003eea526be18718"}, 390 | {file = "typed_ast-1.5.3.tar.gz", hash = "sha256:27f25232e2dd0edfe1f019d6bfaaf11e86e657d9bdb7b0956db95f560cceb2b3"}, 391 | ] 392 | typing-extensions = [ 393 | {file = "typing_extensions-4.2.0-py3-none-any.whl", hash = "sha256:6657594ee297170d19f67d55c05852a874e7eb634f4f753dbd667855e07c1708"}, 394 | {file = "typing_extensions-4.2.0.tar.gz", hash = "sha256:f1c24655a0da0d1b67f07e17a5e6b2a105894e6824b92096378bb3668ef02376"}, 395 | ] 396 | zipp = [ 397 | {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, 398 | {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, 399 | ] 400 | --------------------------------------------------------------------------------