├── .editorconfig ├── .env ├── .gitignore ├── .travis.yml ├── AUTHORS ├── LICENSE ├── README.md ├── frosted ├── __init__.py ├── api.py ├── checker.py ├── main.py ├── messages.py ├── reporter.py ├── settings.py └── test │ ├── __init__.py │ ├── test_api.py │ ├── test_doctests.py │ ├── test_function_calls.py │ ├── test_imports.py │ ├── test_noqa.py │ ├── test_other.py │ ├── test_plugins.py │ ├── test_return_with_arguments_inside_generator.py │ ├── test_script.py │ ├── test_undefined_names.py │ └── utils.py ├── logo.png ├── runtests.py ├── setup.cfg ├── setup.py └── tox.ini /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.py] 4 | max_line_length = 120 5 | indent_style = space 6 | indent_size = 4 7 | known_first_party = frosted 8 | balanced_wrapping = true 9 | ignore_frosted_errors = E103,W201 10 | ignore_frosted_errors_for_test_function_calls.py = E101 11 | ignore_frosted_errors_for_test_return_with_arguments_inside_generator.py = E101 12 | skip = runtests.py,build 13 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | OPEN_PROJECT_NAME="frosted" 3 | 4 | if [ "$PROJECT_NAME" = "$OPEN_PROJECT_NAME" ]; then 5 | return 6 | fi 7 | 8 | export PROJECT_NAME=$OPEN_PROJECT_NAME 9 | export PROJECT_DIR="$PWD" 10 | 11 | # Let's make sure this is a hubflow enabled repo 12 | yes | git hf init >/dev/null 2>/dev/null 13 | 14 | # Quick directory switching 15 | alias root="cd $PROJECT_DIR" 16 | alias project="root; cd $PROJECT_NAME" 17 | alias tests="project; cd test" 18 | alias scripts="project; cd scripts" 19 | 20 | # Commands 21 | alias test="_test_project" 22 | alias install="_install_project" 23 | alias distribute="python setup.py sdist upload; python setup.py bdist_wheel upload" 24 | alias leave="_leave_project" 25 | 26 | function _install_project() 27 | { 28 | CURRENT_DIRECTORY="$PWD" 29 | root 30 | sudo python setup.py install 31 | cd $CURRENT_DIRECTORY 32 | } 33 | 34 | function _test_project() 35 | { 36 | CURRENT_DIRECTORY="$PWD" 37 | tests 38 | py.test -s 39 | cd $CURRENT_DIRECTORY 40 | } 41 | 42 | function _leave_project() 43 | { 44 | export PROJECT_NAME="" 45 | export PROJECT_DIR="" 46 | 47 | unalias root 48 | unalias project 49 | unalias tests 50 | unalias test 51 | unalias install 52 | unalias distribute 53 | unalias leave 54 | } 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | .DS_Store 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | build 10 | eggs 11 | parts 12 | var 13 | sdist 14 | develop-eggs 15 | .installed.cfg 16 | lib 17 | lib64 18 | MANIFEST 19 | MANIFEST.in 20 | 21 | # Installer logs 22 | pip-log.txt 23 | npm-debug.log 24 | 25 | # Unit test / coverage reports 26 | .coverage 27 | .tox 28 | nosetests.xml 29 | htmlcov 30 | .cache 31 | 32 | # Translations 33 | *.mo 34 | 35 | # Mr Developer 36 | .mr.developer.cfg 37 | .project 38 | .pydevproject 39 | 40 | # SQLite 41 | test_exp_framework 42 | 43 | # npm 44 | node_modules/ 45 | 46 | # dolphin 47 | .directory 48 | libpeerconnection.log 49 | 50 | # setuptools 51 | dist 52 | 53 | # IDE Files 54 | atlassian-ide-plugin.xml 55 | .idea/ 56 | *.swp 57 | *.kate-swp 58 | .ropeproject/ 59 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - 2.6 4 | - 2.7 5 | - 3.2 6 | - 3.3 7 | - 3.4 8 | - pypy 9 | install: 10 | - python setup.py install 11 | script: 12 | - python setup.py test 13 | matrix: 14 | allow_failures: 15 | - python: pypy 16 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Contributors 2 | ------------ 3 | 4 | * Phil Frost - Former Divmod Team 5 | * Moe Aboulkheir - Former Divmod Team 6 | * Jean-Paul Calderone - Former Divmod Team 7 | * Glyph Lefkowitz - Former Divmod Team 8 | * Tristan Seligmann 9 | * Jonathan Lange 10 | * Georg Brandl 11 | * Ronny Pfannschmidt 12 | * Virgil Dupras 13 | * Kevin Watters 14 | * Ian Cordasco 15 | * Florent Xicluna 16 | * Domen Kožar 17 | * Marcin Cieślak 18 | * Steven Myint 19 | * Ignas Mikalajūnas 20 | * Timothy Crosley 21 | * Alexander Gordeyev 22 | * Randy Syring 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2005-2011 Divmod, Inc. 2 | Copyright 2013 Florent Xicluna 3 | Copyright 2014 Timothy Edmund Crosley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![frosted](https://raw.github.com/timothycrosley/frosted/master/logo.png) 2 | ===== 3 | [![PyPI version](https://badge.fury.io/py/frosted.png)](http://badge.fury.io/py/frosted) 4 | [![PyPi downloads](https://pypip.in/d/frosted/badge.png)](https://crate.io/packages/frosted/) 5 | [![Build Status](https://travis-ci.org/timothycrosley/frosted.png?branch=master)](https://travis-ci.org/timothycrosley/frosted) 6 | [![License](https://pypip.in/license/frosted/badge.png)](https://pypi.python.org/pypi/frosted/) 7 | [![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/timothycrosley/frosted/trend.png)](https://bitdeli.com/free "Bitdeli Badge") 8 | 9 | Frosted is a fork of pyflakes (originally created by Phil Frost) that aims at more open contribution from the outside public, a smaller more maintainable code base, and a better Python checker for all. 10 | It currently cleanly supports Python 2.6 - 3.4 using pies (https://github.com/timothycrosley/pies) to achieve this without ugly hacks and/or py2to3. 11 | 12 | IMPORTANT NOTE: FROSTED IS DEPRECATED! LONG LIVE FLAKE8 13 | =================== 14 | Frosted was born because pyflakes went for a period of around a year without a maintainer. At some point a maintainer reappeared and the original project continued the mantel of quickly checking code. At this point the best bet, if you are looking for a project to check your code quality is to use the newest version of PyFlakes or Flake8 (https://github.com/PyCQA/flake8) - which itself uses pyflakes in addition to some other great public tools. 15 | 16 | Installing Frosted 17 | =================== 18 | 19 | Installing Frosted is as simple as: 20 | 21 | pip install frosted --upgrade 22 | 23 | or if you prefer 24 | 25 | easy_install frosted 26 | 27 | Using Frosted 28 | =================== 29 | 30 | **from the command line:** 31 | 32 | frosted mypythonfile.py mypythonfile2.py 33 | 34 | or recursively: 35 | 36 | frosted -r . 37 | 38 | *which is equivalent to* 39 | 40 | frosted **/*.py 41 | 42 | or to read from stdin: 43 | 44 | frosted - 45 | 46 | **from within Python:** 47 | 48 | import frosted 49 | 50 | frosted.api.check_path("pythonfile.py") 51 | 52 | Discussing improvements and getting help 53 | =================== 54 | 55 | Using any of the following methods will result in a quick resolution to any issue you may have with Frosted 56 | or a quick response to any implementation detail you wish to discuss. 57 | - [Mailing List](https://mail.python.org/mailman/listinfo/code-quality) - best place to discuss large architectural changes or changes that effect that may effect Python code-quality projects beyond Frosted. 58 | - [Github issues](https://github.com/timothycrosley/frosted/issues) - best place to report bugs, ask for concretely defined features, and even ask for general help. 59 | - - feel free to email me any questions or concerns you have that you don't think would benefit from community wide involvement. 60 | 61 | What makes Frosted better then pyflakes? 62 | =================== 63 | 64 | The following improvements have already been implemented into Frosted 65 | 66 | - Several improvements and fixes that have stayed open (and ignored) on mainline pyflakes have been integrated. 67 | - Lots of code has been re-factored and simplified, Frosted aims to be faster and leaner then pyflakes ever was. 68 | - Frosted adds the ability to configure which files you want to check, and which errors you don't care about. Which, in my opinion, is a must have feature. 69 | - Frosted implements the .editorconfig standard for configuration. This means you only need one configuration file for isort, frosted, and all the code editors anybody working with your project may be using. 70 | - Frosted uses a more logical, self-documenting, and standard terminal interface. With pyflakes the default action without any arguments is to do nothing (waiting for stdin) with Frosted you get an error and help. 71 | - Frosted switched from Java style unittests to the more Pythonic py.test (I admit this is highly subjective). 72 | - The number one reason frosted is better is because of you! Or rather, the Python community at large. I will quickly respond to any pull requests, recommendations, or bug reports that come my way. 73 | - Frosting. Duh. 74 | 75 | And it will only get better from here on out! 76 | 77 | Configuring Frosted 78 | ====================== 79 | 80 | If you find the default frosted settings do not work well for your project, frosted provides several ways to adjust 81 | the behavior. 82 | 83 | To configure frosted for a single user create a ~/.frosted.cfg file: 84 | 85 | [settings] 86 | skip=file3.py,file4.py 87 | ignore_frosted_errors=E101,E205,E300 88 | run_doctests=True 89 | 90 | - **skip** - A comma delimited list of file or directory names to skip. The name must exactly match the entire path, the name of the file, or one of it's parent directories for it to be skipped. 91 | - **ignore_frosted_errors** - A comma delimited list of Frosted error codes to ignore. You can see a definition of all error codes in the next section. 92 | 93 | Additionally, you can specify project level configuration simply by placing a .frosted.cfg file at the root of your 94 | project. frosted will look up to 25 directories up, from the one it is ran, to find a project specific configuration. 95 | 96 | You can then override any of these settings by using command line arguments, or by passing in kwargs into any of the 97 | exposed api checking methods. 98 | 99 | Beyond that, frosted supports setup.cfg based configuration. All you need to do is add a [frosted] section to your 100 | project's setup.cfg file with any desired settings. 101 | 102 | Finally, frosted supports editorconfig files using the standard syntax defined here: 103 | http://editorconfig.org/ 104 | 105 | Meaning You can place any standard frosted configuration parameters within a .editorconfig file under the *.py section 106 | and they will be honored. 107 | 108 | Frosted Error Codes 109 | ====================== 110 | 111 | Frosted recognizes the following errors when present within your code. You can use the 'ignore_frosted_errors' setting to 112 | specify any errors you want Frosted to ignore. If you specify the series error code (ex: E100) all errors in that series will be 113 | ignored. 114 | 115 | **I100 Series** - *General Information* 116 | - **I101**: Generic 117 | 118 | **E100 Series** - *Import Errors* 119 | - **E101**: UnusedImport 120 | - Note that it is common practice to import something and not use it for the purpose of exposing it as an API, or using it in an exec statment below. Frosted tries to circumvent most of this by ignoring this error by default in __init__.py 121 | - **E102**: ImportShadowedByLoopVar 122 | - **E103**: ImportStarUsed 123 | 124 | **E200 Series** - *Function / Method Definition and Calling Errors* 125 | - **E201**: MultipleValuesForArgument 126 | - **E202**: TooFewArguments 127 | - **E203**: TooManyArguments 128 | - **E204**: UnexpectedArgument 129 | - **E205**: NeedKwOnlyArgument 130 | - **E206**: DuplicateArgument 131 | - **E207**: LateFutureImport 132 | - **E208**: ReturnWithArgsInsideGenerator 133 | 134 | **E300 Series** - *Variable / Definition Usage Errors* 135 | - **E301**: RedefinedWhileUnused 136 | - **E302**: RedefinedInListComp 137 | - **E303**: UndefinedName 138 | - **E304**: UndefinedExport 139 | - **E305**: UndefinedLocal 140 | - **E306**: Redefined 141 | - **E307**: UnusedVariable 142 | 143 | **E400 Series** - *Syntax Errors* 144 | - **E401**: DoctestSyntaxError 145 | - **E402**: PythonSyntaxError 146 | 147 | **W100 Series** - *Exception Warning* 148 | - **W101**: BareExcept 149 | - Note that one common case where a bare except is okay, and should be ignored is when handling the rollback of database transactions. In this or simular cases the warning can safely be ignored. 150 | 151 | **W200 Series** - *Handling Warning* 152 | - **W201**: FileSkipped 153 | 154 | 155 | When deciding whether or not to include an error for reporting, Frosted uses the 99% approach as a yard stick. If it is agreed that 99% of the time (or more) that a pattern occurs it's an error, Frosted will report on it, if not it will not be added to the Frosted project. 156 | 157 | Frosted Code API 158 | =================== 159 | 160 | Frosted exposes a simple API for checking Python code from withing other Python applications or plugins. 161 | 162 | - frosted.api.check (codeString, filename, reporter=modReporter.Default, **setting_overrides) 163 | Check the Python source given by codeString for unfrosted flakes. 164 | - frosted.api.check_path (filename, reporter=modReporter.Default, **setting_overrides) 165 | Check the given path, printing out any warnings detected. 166 | - frosted.check_recursive (paths, reporter=modReporter.Default, **setting_overrides) 167 | Recursively check all source files defined in paths. 168 | 169 | Additionally, you can use the command line tool in an API fashion, by passing '-' in as the filename and then sending 170 | file content to stdin. 171 | 172 | Text Editor Integration 173 | =================== 174 | 175 | Integration with text editors and tools is a priority for the project. As such, any pull request that adds integration support 176 | or links to a third-party project that does will be enthusiastically accepted. 177 | 178 | Current list of known supported text-editors: 179 | 180 | - **vim** - Support has been added via syntastic: https://github.com/scrooloose/syntastic 181 | 182 | Contributing to Frosted 183 | =================== 184 | 185 | Our preferred contributions come in the form of pull requests and issue reports. That said, we will not deny monetary contributions. 186 | If you desire to do this using flattr etc, please make sure you flattr @bitglue as he is the original creator of pyflakes and without his contribution 187 | Frosted would not be possible. 188 | 189 | Why did you fork pyflakes? 190 | =================== 191 | 192 | Pyflakes was a great project, and introduced a great approach for quickly checking for Python coding errors. I am very grateful to the original creators. 193 | However, I feel over the last year it has become stagnant, without a clear vision and someone willing to take true ownership of the project. 194 | While I know it is in no way intentional, critical failures have stayed open, despite perfectly complete and valid pull-requests open, without so much as an acknowledgement from the maintainer. 195 | As I genuinely believe open source projects need constant improvement (releasing early and often), I decided to start this project and look for as much 196 | input as possible from the Python community. I'm hoping together we can build an even more awesome code checker! 197 | 198 | Note: the maintainer of pyflakes has been added as a contributer to frosted. 199 | 200 | Why Frosted? 201 | =================== 202 | 203 | Frosted is a homage to the original pyflakes creator Phil Frost. 204 | 205 | -------------------------------------------- 206 | 207 | Thanks and I hope you enjoy the new Frosted pyflakes! 208 | 209 | ~Timothy Crosley 210 | -------------------------------------------------------------------------------- /frosted/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = '1.4.1' 2 | -------------------------------------------------------------------------------- /frosted/api.py: -------------------------------------------------------------------------------- 1 | """frosted/api.py. 2 | 3 | Defines the api for the command-line frosted utility 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 7 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 8 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or 11 | substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 14 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 15 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 16 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 17 | 18 | """ 19 | import os 20 | import re 21 | import sys 22 | import tokenize 23 | from io import StringIO 24 | from token import N_TOKENS 25 | 26 | from pies.overrides import * 27 | 28 | import _ast 29 | from frosted import reporter as modReporter 30 | from frosted import checker, settings 31 | from frosted.messages import FileSkipped, PythonSyntaxError 32 | 33 | __all__ = ['check', 'check_path', 'check_recursive', 'iter_source_code'] 34 | 35 | _re_noqa = re.compile(r'((frosted)[:=]\s*noqa)|(#\s*noqa)', re.I) 36 | 37 | 38 | def _noqa_lines(codeString): 39 | line_nums = [] 40 | g = tokenize.generate_tokens(StringIO(str(codeString)).readline) # tokenize the string 41 | for toknum, tokval, begins, _, _ in g: 42 | lineno = begins[0] 43 | # not sure what N_TOKENS really means, but in testing, that was what comments were 44 | # tokenized as 45 | if toknum == N_TOKENS: 46 | if _re_noqa.search(tokval): 47 | line_nums.append(lineno) 48 | return line_nums 49 | 50 | 51 | def _should_skip(filename, skip): 52 | if filename in skip: 53 | return True 54 | 55 | position = os.path.split(filename) 56 | while position[1]: 57 | if position[1] in skip: 58 | return True 59 | position = os.path.split(position[0]) 60 | 61 | 62 | def check(codeString, filename, reporter=modReporter.Default, settings_path=None, **setting_overrides): 63 | """Check the Python source given by codeString for unfrosted flakes.""" 64 | 65 | if not settings_path and filename: 66 | settings_path = os.path.dirname(os.path.abspath(filename)) 67 | settings_path = settings_path or os.getcwd() 68 | 69 | active_settings = settings.from_path(settings_path).copy() 70 | for key, value in itemsview(setting_overrides): 71 | access_key = key.replace('not_', '').lower() 72 | if type(active_settings.get(access_key)) in (list, tuple): 73 | if key.startswith('not_'): 74 | active_settings[access_key] = list(set(active_settings[access_key]).difference(value)) 75 | else: 76 | active_settings[access_key] = list(set(active_settings[access_key]).union(value)) 77 | else: 78 | active_settings[key] = value 79 | active_settings.update(setting_overrides) 80 | 81 | if _should_skip(filename, active_settings.get('skip', [])): 82 | if active_settings.get('directly_being_checked', None) == 1: 83 | reporter.flake(FileSkipped(filename)) 84 | return 1 85 | elif active_settings.get('verbose', False): 86 | ignore = active_settings.get('ignore_frosted_errors', []) 87 | if(not "W200" in ignore and not "W201" in ignore): 88 | reporter.flake(FileSkipped(filename, None, verbose=active_settings.get('verbose'))) 89 | return 0 90 | 91 | # First, compile into an AST and handle syntax errors. 92 | try: 93 | tree = compile(codeString, filename, "exec", _ast.PyCF_ONLY_AST) 94 | except SyntaxError: 95 | value = sys.exc_info()[1] 96 | msg = value.args[0] 97 | 98 | (lineno, offset, text) = value.lineno, value.offset, value.text 99 | 100 | # If there's an encoding problem with the file, the text is None. 101 | if text is None: 102 | # Avoid using msg, since for the only known case, it contains a 103 | # bogus message that claims the encoding the file declared was 104 | # unknown. 105 | reporter.unexpected_error(filename, 'problem decoding source') 106 | else: 107 | reporter.flake(PythonSyntaxError(filename, msg, lineno, offset, text, 108 | verbose=active_settings.get('verbose'))) 109 | return 1 110 | except Exception: 111 | reporter.unexpected_error(filename, 'problem decoding source') 112 | return 1 113 | # Okay, it's syntactically valid. Now check it. 114 | w = checker.Checker(tree, filename, None, ignore_lines=_noqa_lines(codeString), **active_settings) 115 | w.messages.sort(key=lambda m: m.lineno) 116 | for warning in w.messages: 117 | reporter.flake(warning) 118 | return len(w.messages) 119 | 120 | 121 | def check_path(filename, reporter=modReporter.Default, settings_path=None, **setting_overrides): 122 | """Check the given path, printing out any warnings detected.""" 123 | try: 124 | with open(filename, 'U') as f: 125 | codestr = f.read() + '\n' 126 | except UnicodeError: 127 | reporter.unexpected_error(filename, 'problem decoding source') 128 | return 1 129 | except IOError: 130 | msg = sys.exc_info()[1] 131 | reporter.unexpected_error(filename, msg.args[1]) 132 | return 1 133 | return check(codestr, filename, reporter, settings_path, **setting_overrides) 134 | 135 | 136 | def iter_source_code(paths): 137 | """Iterate over all Python source files defined in paths.""" 138 | for path in paths: 139 | if os.path.isdir(path): 140 | for dirpath, dirnames, filenames in os.walk(path): 141 | for filename in filenames: 142 | if filename.endswith('.py'): 143 | yield os.path.join(dirpath, filename) 144 | else: 145 | yield path 146 | 147 | 148 | def check_recursive(paths, reporter=modReporter.Default, settings_path=None, **setting_overrides): 149 | """Recursively check all source files defined in paths.""" 150 | warnings = 0 151 | for source_path in iter_source_code(paths): 152 | warnings += check_path(source_path, reporter, settings_path=None, **setting_overrides) 153 | return warnings 154 | -------------------------------------------------------------------------------- /frosted/checker.py: -------------------------------------------------------------------------------- 1 | """frosted/checker.py. 2 | 3 | The core functionality of frosted lives here. Implements the core checking capability models Bindings and Scopes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 7 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 8 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or 11 | substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 14 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 15 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 16 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 17 | OTHER DEALINGS IN THE SOFTWARE. 18 | 19 | """ 20 | from __future__ import absolute_import, division, print_function, unicode_literals 21 | 22 | import builtins 23 | import doctest 24 | import itertools 25 | import os 26 | import pkg_resources 27 | import sys 28 | 29 | from pies import ast 30 | from pies.overrides import * 31 | 32 | from frosted import messages 33 | 34 | PY34_GTE = sys.version_info >= (3, 4) 35 | FROSTED_BUILTINS = set(dir(builtins) + ['__file__', '__builtins__', '__debug__', '__name__', 'WindowsError', 36 | '__import__'] + 37 | os.environ.get('PYFLAKES_BUILTINS', '').split(',')) 38 | 39 | def node_name(node): 40 | """ 41 | Convenience function: Returns node.id, or node.name, or None 42 | """ 43 | return hasattr(node, 'id') and node.id or hasattr(node, 'name') and node.name 44 | 45 | 46 | class Binding(object): 47 | """Represents the binding of a value to a name. 48 | 49 | The checker uses this to keep track of which names have been bound and which names have not. See Assignment for a 50 | special type of binding that is checked with stricter rules. 51 | 52 | """ 53 | __slots__ = ('name', 'source', 'used') 54 | 55 | def __init__(self, name, source): 56 | self.name = name 57 | self.source = source 58 | self.used = False 59 | 60 | def __str__(self): 61 | return self.name 62 | 63 | def __repr__(self): 64 | return '<%s object %r from line %r at 0x%x>' % (self.__class__.__name__, 65 | self.name, 66 | self.source.lineno, 67 | id(self)) 68 | 69 | 70 | class Importation(Binding): 71 | """A binding created by an import statement.""" 72 | __slots__ = ('fullName', ) 73 | 74 | def __init__(self, name, source): 75 | self.fullName = name 76 | name = name.split('.')[0] 77 | super(Importation, self).__init__(name, source) 78 | 79 | 80 | class Argument(Binding): 81 | """Represents binding a name as an argument.""" 82 | __slots__ = () 83 | 84 | 85 | class Definition(Binding): 86 | """A binding that defines a function or a class.""" 87 | __slots__ = () 88 | 89 | 90 | class Assignment(Binding): 91 | """Represents binding a name with an explicit assignment. 92 | 93 | The checker will raise warnings for any Assignment that isn't used. Also, the checker does not consider assignments 94 | in tuple/list unpacking to be Assignments, rather it treats them as simple Bindings. 95 | 96 | """ 97 | __slots__ = () 98 | 99 | 100 | class FunctionDefinition(Definition): 101 | __slots__ = ('signature', ) 102 | 103 | def __init__(self, name, source): 104 | super(FunctionDefinition, self).__init__(name, source) 105 | self.signature = FunctionSignature(source) 106 | 107 | 108 | class ClassDefinition(Definition): 109 | __slots__ = () 110 | 111 | 112 | class ExportBinding(Binding): 113 | """A binding created by an __all__ assignment. If the names in the list 114 | can be determined statically, they will be treated as names for export and 115 | additional checking applied to them. 116 | 117 | The only __all__ assignment that can be recognized is one which takes 118 | the value of a literal list containing literal strings. For example: 119 | 120 | __all__ = ["foo", "bar"] 121 | 122 | Names which are imported and not otherwise used but appear in the value of 123 | __all__ will not have an unused import warning reported for them. 124 | 125 | """ 126 | __slots__ = () 127 | 128 | def names(self): 129 | """Return a list of the names referenced by this binding.""" 130 | names = [] 131 | if isinstance(self.source, ast.List): 132 | for node in self.source.elts: 133 | if isinstance(node, ast.Str): 134 | names.append(node.s) 135 | return names 136 | 137 | 138 | class Scope(dict): 139 | importStarred = False # set to True when import * is found 140 | 141 | def __repr__(self): 142 | scope_cls = self.__class__.__name__ 143 | return '<%s at 0x%x %s>' % (scope_cls, id(self), dict.__repr__(self)) 144 | 145 | 146 | class ClassScope(Scope): 147 | pass 148 | 149 | 150 | class FunctionScope(Scope): 151 | """Represents the name scope for a function.""" 152 | uses_locals = False 153 | always_used = set(['__tracebackhide__', '__traceback_info__', '__traceback_supplement__']) 154 | 155 | def __init__(self): 156 | Scope.__init__(self) 157 | self.globals = self.always_used.copy() 158 | 159 | def unusedAssignments(self): 160 | """Return a generator for the assignments which have not been used.""" 161 | for name, binding in self.items(): 162 | if (not binding.used and name not in self.globals 163 | and not self.uses_locals 164 | and isinstance(binding, Assignment)): 165 | yield name, binding 166 | 167 | 168 | class GeneratorScope(Scope): 169 | pass 170 | 171 | 172 | class ModuleScope(Scope): 173 | pass 174 | 175 | 176 | class FunctionSignature(object): 177 | __slots__ = ('decorated', 'argument_names', 'default_count', 'kw_only_argument_names', 'default_count', 178 | 'kw_only_argument_names', 'kw_only_default_count', 'has_var_arg', 'has_kw_arg') 179 | 180 | def __init__(self, node): 181 | self.decorated = bool(any(node.decorator_list)) 182 | self.argument_names = ast.argument_names(node) 183 | self.default_count = len(node.args.defaults) 184 | self.kw_only_argument_names = ast.kw_only_argument_names(node) 185 | self.kw_only_default_count = ast.kw_only_default_count(node) 186 | self.has_var_arg = node.args.vararg is not None 187 | self.has_kw_arg = node.args.kwarg is not None 188 | 189 | def min_argument_count(self): 190 | return len(self.argument_names) - self.default_count 191 | 192 | def maxArgumentCount(self): 193 | return len(self.argument_names) 194 | 195 | def checkCall(self, call_node, reporter, name): 196 | if self.decorated: 197 | return 198 | 199 | filledSlots = set() 200 | filledKwOnlySlots = set() 201 | for item, arg in enumerate(call_node.args): 202 | if item >= len(self.argument_names): 203 | if not self.has_var_arg: 204 | return reporter.report(messages.TooManyArguments, call_node, name, self.maxArgumentCount()) 205 | break 206 | filledSlots.add(item) 207 | 208 | for kw in call_node.keywords: 209 | slots = None 210 | try: 211 | argIndex = self.argument_names.index(kw.arg) 212 | slots = filledSlots 213 | except ValueError: 214 | try: 215 | argIndex = self.kw_only_argument_names.index(kw.arg) 216 | slots = filledKwOnlySlots 217 | except ValueError: 218 | if self.has_kw_arg: 219 | continue 220 | else: 221 | return reporter.report(messages.UnexpectedArgument, call_node, name, kw.arg) 222 | if argIndex in slots: 223 | return reporter.report(messages.MultipleValuesForArgument, call_node, name, kw.arg) 224 | slots.add(argIndex) 225 | 226 | filledSlots.update(range(len(self.argument_names) - self.default_count, len(self.argument_names))) 227 | filledKwOnlySlots.update(range(len(self.kw_only_argument_names) - self.kw_only_default_count, 228 | len(self.kw_only_argument_names))) 229 | 230 | if (len(filledSlots) < len(self.argument_names) and not call_node.starargs and not call_node.kwargs): 231 | return reporter.report(messages.TooFewArguments, call_node, name, self.min_argument_count()) 232 | if (len(filledKwOnlySlots) < len(self.kw_only_argument_names) and not call_node.kwargs): 233 | missing_arguments = [repr(arg) for i, arg in enumerate(self.kw_only_argument_names) 234 | if i not in filledKwOnlySlots] 235 | return reporter.report(messages.NeedKwOnlyArgument, call_node, name, ', '.join(missing_arguments)) 236 | 237 | 238 | class Checker(object): 239 | """The core of frosted, checks the cleanliness and sanity of Python code.""" 240 | 241 | node_depth = 0 242 | offset = None 243 | trace_tree = False 244 | frosted_builtins = FROSTED_BUILTINS 245 | 246 | def __init__(self, tree, filename='(none)', builtins=None, ignore_lines=(), **settings): 247 | self.settings = settings 248 | self.ignore_errors = settings.get('ignore_frosted_errors', []) 249 | self.ignore_lines = ignore_lines 250 | file_specific_ignores = settings.get('ignore_frosted_errors_for_' + (os.path.basename(filename) or ""), None) 251 | if file_specific_ignores: 252 | self.ignore_errors += file_specific_ignores 253 | 254 | self._node_handlers = {} 255 | self._deferred_functions = [] 256 | self._deferred_assignments = [] 257 | self.dead_scopes = [] 258 | self.messages = [] 259 | self.filename = filename 260 | if builtins: 261 | self.frosted_builtins = self.frosted_builtins.union(builtins) 262 | self.scope_stack = [ModuleScope()] 263 | self.except_handlers = [()] 264 | self.futures_allowed = True 265 | self.root = tree 266 | self.handle_children(tree) 267 | self.run_deferred(self._deferred_functions) 268 | self._deferred_functions = None 269 | self.run_deferred(self._deferred_assignments) 270 | self._deferred_assignments = None 271 | del self.scope_stack[1:] 272 | self.pop_scope() 273 | self.check_dead_scopes() 274 | self.check_plugins() 275 | 276 | def check_plugins(self): 277 | """ collect plugins from entry point 'frosted.plugins' 278 | 279 | and run their check() method, passing the filename 280 | """ 281 | checkers = {} 282 | for ep in pkg_resources.iter_entry_points(group='frosted.plugins'): 283 | checkers.update({ep.name: ep.load()}) 284 | 285 | for plugin_name, plugin in checkers.items(): 286 | if self.filename != '(none)': 287 | messages = plugin.check(self.filename) 288 | for message, loc, args, kwargs in messages: 289 | self.report(message, loc, *args, **kwargs) 290 | 291 | def defer_function(self, callable): 292 | """Schedule a function handler to be called just before completion. 293 | 294 | This is used for handling function bodies, which must be deferred because code later in the file might modify 295 | the global scope. When 'callable' is called, the scope at the time this is called will be restored, however it 296 | will contain any new bindings added to it. 297 | 298 | """ 299 | self._deferred_functions.append((callable, self.scope_stack[:], self.offset)) 300 | 301 | def defer_assignment(self, callable): 302 | """Schedule an assignment handler to be called just after deferred 303 | function handlers.""" 304 | self._deferred_assignments.append((callable, self.scope_stack[:], self.offset)) 305 | 306 | def run_deferred(self, deferred): 307 | """Run the callables in deferred using their associated scope stack.""" 308 | for handler, scope, offset in deferred: 309 | self.scope_stack = scope 310 | self.offset = offset 311 | handler() 312 | 313 | @property 314 | def scope(self): 315 | return self.scope_stack[-1] 316 | 317 | def pop_scope(self): 318 | self.dead_scopes.append(self.scope_stack.pop()) 319 | 320 | def check_dead_scopes(self): 321 | """Look at scopes which have been fully examined and report names in 322 | them which were imported but unused.""" 323 | for scope in self.dead_scopes: 324 | export = isinstance(scope.get('__all__'), ExportBinding) 325 | if export: 326 | all = scope['__all__'].names() 327 | # Look for possible mistakes in the export list 328 | if not scope.importStarred and os.path.basename(self.filename) != '__init__.py': 329 | undefined = set(all) - set(scope) 330 | for name in undefined: 331 | self.report(messages.UndefinedExport, scope['__all__'].source, name) 332 | else: 333 | all = [] 334 | 335 | # Look for imported names that aren't used without checking imports in namespace definition 336 | for importation in scope.values(): 337 | if isinstance(importation, Importation) and not importation.used and importation.name not in all: 338 | self.report(messages.UnusedImport, importation.source, importation.name) 339 | 340 | def push_scope(self, scope_class=FunctionScope): 341 | self.scope_stack.append(scope_class()) 342 | 343 | def push_function_scope(self): # XXX Deprecated 344 | self.push_scope(FunctionScope) 345 | 346 | def push_class_scope(self): # XXX Deprecated 347 | self.push_scope(ClassScope) 348 | 349 | def report(self, message_class, *args, **kwargs): 350 | error_code = message_class.error_code 351 | 352 | if(not error_code[:2] + "00" in self.ignore_errors and not error_code in self.ignore_errors and not 353 | str(message_class.error_number) in self.ignore_errors): 354 | kwargs['verbose'] = self.settings.get('verbose') 355 | message = message_class(self.filename, *args, **kwargs) 356 | if message.lineno not in self.ignore_lines: 357 | self.messages.append(message) 358 | 359 | def has_parent(self, node, kind): 360 | while hasattr(node, 'parent'): 361 | node = node.parent 362 | if isinstance(node, kind): 363 | return True 364 | 365 | def get_common_ancestor(self, lnode, rnode, stop=None): 366 | stop = stop or self.root 367 | if lnode is rnode: 368 | return lnode 369 | if stop in (lnode, rnode): 370 | return stop 371 | 372 | if not hasattr(lnode, 'parent') or not hasattr(rnode, 'parent'): 373 | return 374 | if (lnode.level > rnode.level): 375 | return self.get_common_ancestor(lnode.parent, rnode, stop) 376 | if (rnode.level > lnode.level): 377 | return self.get_common_ancestor(lnode, rnode.parent, stop) 378 | return self.get_common_ancestor(lnode.parent, rnode.parent, stop) 379 | 380 | def descendant_of(self, node, ancestors, stop=None): 381 | for ancestor in ancestors: 382 | if self.get_common_ancestor(node, ancestor, stop) not in (stop, None): 383 | return True 384 | return False 385 | 386 | def on_fork(self, parent, lnode, rnode, items): 387 | return (self.descendant_of(lnode, items, parent) ^ self.descendant_of(rnode, items, parent)) 388 | 389 | def different_forks(self, lnode, rnode): 390 | """True, if lnode and rnode are located on different forks of 391 | IF/TRY.""" 392 | ancestor = self.get_common_ancestor(lnode, rnode) 393 | if isinstance(ancestor, ast.If): 394 | for fork in (ancestor.body, ancestor.orelse): 395 | if self.on_fork(ancestor, lnode, rnode, fork): 396 | return True 397 | elif isinstance(ancestor, ast.Try): 398 | body = ancestor.body + ancestor.orelse 399 | for fork in [body] + [[hdl] for hdl in ancestor.handlers]: 400 | if self.on_fork(ancestor, lnode, rnode, fork): 401 | return True 402 | elif isinstance(ancestor, ast.TryFinally): 403 | if self.on_fork(ancestor, lnode, rnode, ancestor.body): 404 | return True 405 | return False 406 | 407 | def add_binding(self, node, value, report_redef=True): 408 | """Called when a binding is altered. 409 | 410 | - `node` is the statement responsible for the change 411 | - `value` is the optional new value, a Binding instance, associated 412 | with the binding; if None, the binding is deleted if it exists. 413 | - if `report_redef` is True (default), rebinding while unused will be 414 | reported. 415 | 416 | """ 417 | redefinedWhileUnused = False 418 | if not isinstance(self.scope, ClassScope): 419 | for scope in self.scope_stack[::-1]: 420 | existing = scope.get(value.name) 421 | if (isinstance(existing, Importation) 422 | and not existing.used 423 | and (not isinstance(value, Importation) or 424 | value.fullName == existing.fullName) 425 | and report_redef 426 | and not self.different_forks(node, existing.source)): 427 | redefinedWhileUnused = True 428 | self.report(messages.RedefinedWhileUnused, 429 | node, value.name, existing.source) 430 | 431 | existing = self.scope.get(value.name) 432 | if not redefinedWhileUnused and self.has_parent(value.source, ast.ListComp): 433 | if (existing and report_redef 434 | and not self.has_parent(existing.source, (ast.For, ast.ListComp)) 435 | and not self.different_forks(node, existing.source)): 436 | self.report(messages.RedefinedInListComp, 437 | node, value.name, existing.source) 438 | 439 | if (isinstance(existing, Definition) 440 | and not existing.used 441 | and not self.different_forks(node, existing.source)): 442 | self.report(messages.RedefinedWhileUnused, 443 | node, value.name, existing.source) 444 | else: 445 | self.scope[value.name] = value 446 | 447 | def get_node_handler(self, node_class): 448 | try: 449 | return self._node_handlers[node_class] 450 | except KeyError: 451 | nodeType = str(node_class.__name__).upper() 452 | self._node_handlers[node_class] = handler = getattr(self, nodeType) 453 | return handler 454 | 455 | def iter_visible_scopes(self): 456 | outerScopes = itertools.islice(self.scope_stack, len(self.scope_stack) - 1) 457 | scopes = [scope for scope in outerScopes 458 | if isinstance(scope, (FunctionScope, ModuleScope))] 459 | if (isinstance(self.scope, GeneratorScope) 460 | and scopes[-1] != self.scope_stack[-2]): 461 | scopes.append(self.scope_stack[-2]) 462 | scopes.append(self.scope_stack[-1]) 463 | return iter(reversed(scopes)) 464 | 465 | def handle_node_load(self, node): 466 | name = node_name(node) 467 | if not name: 468 | return 469 | 470 | importStarred = False 471 | for scope in self.iter_visible_scopes(): 472 | importStarred = importStarred or scope.importStarred 473 | try: 474 | scope[name].used = (self.scope, node) 475 | except KeyError: 476 | pass 477 | else: 478 | return 479 | 480 | # look in the built-ins 481 | if importStarred or name in self.frosted_builtins: 482 | return 483 | if name == '__path__' and os.path.basename(self.filename) == '__init__.py': 484 | # the special name __path__ is valid only in packages 485 | return 486 | 487 | # protected with a NameError handler? 488 | if 'NameError' not in self.except_handlers[-1]: 489 | self.report(messages.UndefinedName, node, name) 490 | 491 | def handle_node_store(self, node): 492 | name = node_name(node) 493 | if not name: 494 | return 495 | # if the name hasn't already been defined in the current scope 496 | if isinstance(self.scope, FunctionScope) and name not in self.scope: 497 | # for each function or module scope above us 498 | for scope in self.scope_stack[:-1]: 499 | if not isinstance(scope, (FunctionScope, ModuleScope)): 500 | continue 501 | # if the name was defined in that scope, and the name has 502 | # been accessed already in the current scope, and hasn't 503 | # been declared global 504 | used = name in scope and scope[name].used 505 | if used and used[0] is self.scope and name not in self.scope.globals: 506 | # then it's probably a mistake 507 | self.report(messages.UndefinedLocal, 508 | scope[name].used[1], name, scope[name].source) 509 | break 510 | 511 | parent = getattr(node, 'parent', None) 512 | if isinstance(parent, (ast.For, ast.comprehension, ast.Tuple, ast.List)): 513 | binding = Binding(name, node) 514 | elif (parent is not None and name == '__all__' and 515 | isinstance(self.scope, ModuleScope)): 516 | binding = ExportBinding(name, parent.value) 517 | else: 518 | binding = Assignment(name, node) 519 | if name in self.scope: 520 | binding.used = self.scope[name].used 521 | self.add_binding(node, binding) 522 | 523 | def handle_node_delete(self, node): 524 | name = node_name(node) 525 | if not name: 526 | return 527 | if isinstance(self.scope, FunctionScope) and name in self.scope.globals: 528 | self.scope.globals.remove(name) 529 | else: 530 | try: 531 | del self.scope[name] 532 | except KeyError: 533 | self.report(messages.UndefinedName, node, name) 534 | 535 | def handle_children(self, tree): 536 | for node in ast.iter_child_nodes(tree): 537 | self.handleNode(node, tree) 538 | 539 | def is_docstring(self, node): 540 | """Determine if the given node is a docstring, as long as it is at the 541 | correct place in the node tree.""" 542 | return isinstance(node, ast.Str) or (isinstance(node, ast.Expr) and 543 | isinstance(node.value, ast.Str)) 544 | 545 | def docstring(self, node): 546 | if isinstance(node, ast.Expr): 547 | node = node.value 548 | if not isinstance(node, ast.Str): 549 | return (None, None) 550 | # Computed incorrectly if the docstring has backslash 551 | doctest_lineno = node.lineno - node.s.count('\n') - 1 552 | return (node.s, doctest_lineno) 553 | 554 | def handleNode(self, node, parent): 555 | if node is None: 556 | return 557 | if self.offset and getattr(node, 'lineno', None) is not None: 558 | node.lineno += self.offset[0] 559 | node.col_offset += self.offset[1] 560 | if self.trace_tree: 561 | print(' ' * self.node_depth + node.__class__.__name__) 562 | if self.futures_allowed and not (isinstance(node, ast.ImportFrom) or 563 | self.is_docstring(node)): 564 | self.futures_allowed = False 565 | self.node_depth += 1 566 | node.level = self.node_depth 567 | node.parent = parent 568 | try: 569 | handler = self.get_node_handler(node.__class__) 570 | handler(node) 571 | finally: 572 | self.node_depth -= 1 573 | if self.trace_tree: 574 | print(' ' * self.node_depth + 'end ' + node.__class__.__name__) 575 | 576 | _get_doctest_examples = doctest.DocTestParser().get_examples 577 | 578 | def handle_doctests(self, node): 579 | try: 580 | docstring, node_lineno = self.docstring(node.body[0]) 581 | if not docstring: 582 | return 583 | examples = self._get_doctest_examples(docstring) 584 | except (ValueError, IndexError): 585 | # e.g. line 6 of the docstring for has inconsistent 586 | # leading whitespace: ... 587 | return 588 | node_offset = self.offset or (0, 0) 589 | self.push_scope() 590 | for example in examples: 591 | try: 592 | tree = compile(example.source, "", "exec", ast.PyCF_ONLY_AST) 593 | except SyntaxError: 594 | e = sys.exc_info()[1] 595 | position = (node_lineno + example.lineno + e.lineno, 596 | example.indent + 4 + (e.offset or 0)) 597 | self.report(messages.DoctestSyntaxError, node, position) 598 | else: 599 | self.offset = (node_offset[0] + node_lineno + example.lineno, 600 | node_offset[1] + example.indent + 4) 601 | self.handle_children(tree) 602 | self.offset = node_offset 603 | self.pop_scope() 604 | 605 | def find_return_with_argument(self, node): 606 | """Finds and returns a return statment that has an argument. 607 | 608 | Note that we should use node.returns in Python 3, but this method is never called in Python 3 so we don't bother 609 | checking. 610 | 611 | """ 612 | for item in node.body: 613 | if isinstance(item, ast.Return) and item.value: 614 | return item 615 | elif not isinstance(item, ast.FunctionDef) and hasattr(item, 'body'): 616 | return_with_argument = self.find_return_with_argument(item) 617 | if return_with_argument: 618 | return return_with_argument 619 | 620 | def is_generator(self, node): 621 | """Checks whether a function is a generator by looking for a yield 622 | statement or expression.""" 623 | if not isinstance(node.body, list): 624 | # lambdas can not be generators 625 | return False 626 | for item in node.body: 627 | if isinstance(item, (ast.Assign, ast.Expr)): 628 | if isinstance(item.value, ast.Yield): 629 | return True 630 | elif not isinstance(item, ast.FunctionDef) and hasattr(item, 'body'): 631 | if self.is_generator(item): 632 | return True 633 | return False 634 | 635 | def ignore(self, node): 636 | pass 637 | 638 | # "stmt" type nodes 639 | RETURN = DELETE = PRINT = WHILE = IF = WITH = WITHITEM = RAISE = TRYFINALLY = ASSERT = EXEC = EXPR = handle_children 640 | 641 | CONTINUE = BREAK = PASS = ignore 642 | 643 | # "expr" type nodes 644 | BOOLOP = BINOP = UNARYOP = IFEXP = DICT = SET = YIELD = YIELDFROM = COMPARE = REPR = ATTRIBUTE = SUBSCRIPT = \ 645 | LIST = TUPLE = STARRED = NAMECONSTANT = handle_children 646 | 647 | NUM = STR = BYTES = ELLIPSIS = ignore 648 | 649 | # "slice" type nodes 650 | SLICE = EXTSLICE = INDEX = handle_children 651 | 652 | # expression contexts are node instances too, though being constants 653 | LOAD = STORE = DEL = AUGLOAD = AUGSTORE = PARAM = ignore 654 | 655 | # same for operators 656 | AND = OR = ADD = SUB = MULT = DIV = MOD = POW = LSHIFT = RSHIFT = BITOR = BITXOR = BITAND = FLOORDIV = INVERT = \ 657 | NOT = UADD = USUB = EQ = NOTEQ = LT = LTE = GT = GTE = IS = ISNOT = IN = NOTIN = ignore 658 | 659 | # additional node types 660 | COMPREHENSION = KEYWORD = handle_children 661 | 662 | def GLOBAL(self, node): 663 | """Keep track of globals declarations.""" 664 | if isinstance(self.scope, FunctionScope): 665 | self.scope.globals.update(node.names) 666 | 667 | NONLOCAL = GLOBAL 668 | 669 | def LISTCOMP(self, node): 670 | # handle generators before element 671 | for gen in node.generators: 672 | self.handleNode(gen, node) 673 | self.handleNode(node.elt, node) 674 | 675 | def GENERATOREXP(self, node): 676 | self.push_scope(GeneratorScope) 677 | # handle generators before element 678 | for gen in node.generators: 679 | self.handleNode(gen, node) 680 | self.handleNode(node.elt, node) 681 | self.pop_scope() 682 | 683 | SETCOMP = GENERATOREXP 684 | 685 | def DICTCOMP(self, node): 686 | self.push_scope(GeneratorScope) 687 | for gen in node.generators: 688 | self.handleNode(gen, node) 689 | self.handleNode(node.key, node) 690 | self.handleNode(node.value, node) 691 | self.pop_scope() 692 | 693 | def FOR(self, node): 694 | """Process bindings for loop variables.""" 695 | vars = [] 696 | 697 | def collectLoopVars(n): 698 | if isinstance(n, ast.Name): 699 | vars.append(n.id) 700 | elif isinstance(n, ast.expr_context): 701 | return 702 | else: 703 | for c in ast.iter_child_nodes(n): 704 | collectLoopVars(c) 705 | 706 | collectLoopVars(node.target) 707 | for varn in vars: 708 | if (isinstance(self.scope.get(varn), Importation) 709 | # unused ones will get an unused import warning 710 | and self.scope[varn].used): 711 | self.report(messages.ImportShadowedByLoopVar, 712 | node, varn, self.scope[varn].source) 713 | 714 | self.handle_children(node) 715 | 716 | def NAME(self, node): 717 | """Handle occurrence of Name (which can be a load/store/delete 718 | access.)""" 719 | # Locate the name in locals / function / globals scopes. 720 | if isinstance(node.ctx, (ast.Load, ast.AugLoad)): 721 | self.handle_node_load(node) 722 | if (node.id == 'locals' and isinstance(self.scope, FunctionScope) 723 | and isinstance(node.parent, ast.Call)): 724 | # we are doing locals() call in current scope 725 | self.scope.uses_locals = True 726 | elif isinstance(node.ctx, (ast.Store, ast.AugStore)): 727 | self.handle_node_store(node) 728 | elif isinstance(node.ctx, ast.Del): 729 | self.handle_node_delete(node) 730 | else: 731 | # must be a Param context -- this only happens for names in function 732 | # arguments, but these aren't dispatched through here 733 | raise RuntimeError("Got impossible expression context: %r" % (node.ctx,)) 734 | 735 | def CALL(self, node): 736 | f = node.func 737 | if isinstance(f, ast.Name): 738 | for scope in self.iter_visible_scopes(): 739 | definition = scope.get(f.id) 740 | if definition: 741 | if isinstance(definition, FunctionDefinition): 742 | definition.signature.checkCall(node, self, f.id) 743 | break 744 | 745 | 746 | self.handle_children(node) 747 | 748 | def FUNCTIONDEF(self, node): 749 | for deco in node.decorator_list: 750 | self.handleNode(deco, node) 751 | self.add_binding(node, FunctionDefinition(node.name, node)) 752 | self.LAMBDA(node) 753 | if self.settings.get('run_doctests', False): 754 | self.defer_function(lambda: self.handle_doctests(node)) 755 | 756 | def LAMBDA(self, node): 757 | args = [] 758 | annotations = [] 759 | 760 | if PY2: 761 | def addArgs(arglist): 762 | for arg in arglist: 763 | if isinstance(arg, ast.Tuple): 764 | addArgs(arg.elts) 765 | else: 766 | if arg.id in args: 767 | self.report(messages.DuplicateArgument, 768 | node, arg.id) 769 | args.append(arg.id) 770 | addArgs(node.args.args) 771 | defaults = node.args.defaults 772 | else: 773 | for arg in node.args.args + node.args.kwonlyargs: 774 | annotations.append(arg.annotation) 775 | args.append(arg.arg) 776 | defaults = node.args.defaults + node.args.kw_defaults 777 | 778 | # Only for Python3 FunctionDefs 779 | is_py3_func = hasattr(node, 'returns') 780 | 781 | for arg_name in ('vararg', 'kwarg'): 782 | wildcard = getattr(node.args, arg_name) 783 | if not wildcard: 784 | continue 785 | args.append(getattr(wildcard, 'arg', wildcard)) 786 | if is_py3_func: 787 | if PY34_GTE: 788 | annotations.append(wildcard.annotation) 789 | else: 790 | argannotation = arg_name + 'annotation' 791 | annotations.append(getattr(node.args, argannotation)) 792 | if is_py3_func: 793 | annotations.append(node.returns) 794 | 795 | if PY3: 796 | if len(set(args)) < len(args): 797 | for (idx, arg) in enumerate(args): 798 | if arg in args[:idx]: 799 | self.report(messages.DuplicateArgument, node, arg) 800 | 801 | for child in annotations + defaults: 802 | if child: 803 | self.handleNode(child, node) 804 | 805 | def runFunction(): 806 | 807 | self.push_scope() 808 | for name in args: 809 | self.add_binding(node, Argument(name, node), report_redef=False) 810 | if isinstance(node.body, list): 811 | # case for FunctionDefs 812 | for stmt in node.body: 813 | self.handleNode(stmt, node) 814 | else: 815 | # case for Lambdas 816 | self.handleNode(node.body, node) 817 | 818 | def checkUnusedAssignments(): 819 | """Check to see if any assignments have not been used.""" 820 | for name, binding in self.scope.unusedAssignments(): 821 | self.report(messages.UnusedVariable, binding.source, name) 822 | self.defer_assignment(checkUnusedAssignments) 823 | 824 | if PY2: 825 | def checkReturnWithArgumentInsideGenerator(): 826 | """Check to see if there are any return statements with 827 | arguments but the function is a generator.""" 828 | if self.is_generator(node): 829 | stmt = self.find_return_with_argument(node) 830 | if stmt is not None: 831 | self.report(messages.ReturnWithArgsInsideGenerator, stmt) 832 | self.defer_assignment(checkReturnWithArgumentInsideGenerator) 833 | self.pop_scope() 834 | 835 | self.defer_function(runFunction) 836 | 837 | def CLASSDEF(self, node): 838 | """Check names used in a class definition, including its decorators, 839 | base classes, and the body of its definition. 840 | 841 | Additionally, add its name to the current scope. 842 | 843 | """ 844 | for deco in node.decorator_list: 845 | self.handleNode(deco, node) 846 | for baseNode in node.bases: 847 | self.handleNode(baseNode, node) 848 | if not PY2: 849 | for keywordNode in node.keywords: 850 | self.handleNode(keywordNode, node) 851 | self.push_scope(ClassScope) 852 | if self.settings.get('run_doctests', False): 853 | self.defer_function(lambda: self.handle_doctests(node)) 854 | for stmt in node.body: 855 | self.handleNode(stmt, node) 856 | self.pop_scope() 857 | self.add_binding(node, ClassDefinition(node.name, node)) 858 | 859 | def ASSIGN(self, node): 860 | self.handleNode(node.value, node) 861 | for target in node.targets: 862 | self.handleNode(target, node) 863 | 864 | def AUGASSIGN(self, node): 865 | self.handle_node_load(node.target) 866 | self.handleNode(node.value, node) 867 | self.handleNode(node.target, node) 868 | 869 | def IMPORT(self, node): 870 | for alias in node.names: 871 | name = alias.asname or alias.name 872 | importation = Importation(name, node) 873 | self.add_binding(node, importation) 874 | 875 | def IMPORTFROM(self, node): 876 | if node.module == '__future__': 877 | if not self.futures_allowed: 878 | self.report(messages.LateFutureImport, 879 | node, [n.name for n in node.names]) 880 | else: 881 | self.futures_allowed = False 882 | 883 | for alias in node.names: 884 | if alias.name == '*': 885 | self.scope.importStarred = True 886 | self.report(messages.ImportStarUsed, node, node.module) 887 | continue 888 | name = alias.asname or alias.name 889 | importation = Importation(name, node) 890 | if node.module == '__future__': 891 | importation.used = (self.scope, node) 892 | self.add_binding(node, importation) 893 | 894 | def TRY(self, node): 895 | handler_names = [] 896 | # List the exception handlers 897 | for handler in node.handlers: 898 | if isinstance(handler.type, ast.Tuple): 899 | for exc_type in handler.type.elts: 900 | handler_names.append(node_name(exc_type)) 901 | elif handler.type: 902 | handler_names.append(node_name(handler.type)) 903 | # Memorize the except handlers and process the body 904 | self.except_handlers.append(handler_names) 905 | for child in node.body: 906 | self.handleNode(child, node) 907 | self.except_handlers.pop() 908 | # Process the other nodes: "except:", "else:", "finally:" 909 | for child in ast.iter_child_nodes(node): 910 | if child not in node.body: 911 | self.handleNode(child, node) 912 | 913 | TRYEXCEPT = TRY 914 | 915 | def EXCEPTHANDLER(self, node): 916 | # 3.x: in addition to handling children, we must handle the name of 917 | # the exception, which is not a Name node, but a simple string. 918 | if node.type is None: 919 | self.report(messages.BareExcept, node) 920 | if isinstance(node.name, str): 921 | self.handle_node_store(node) 922 | self.handle_children(node) 923 | -------------------------------------------------------------------------------- /frosted/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ Implementation of the command-line frosted tool. 3 | 4 | """ 5 | from __future__ import absolute_import, division, print_function, unicode_literals 6 | 7 | import argparse 8 | import sys 9 | 10 | from pies.overrides import * 11 | 12 | from frosted import __version__ 13 | from frosted.api import check, check_path, check_recursive 14 | 15 | 16 | def main(): 17 | warnings = 0 18 | 19 | parser = argparse.ArgumentParser(description='Quickly check the correctness of your Python scripts.') 20 | parser.add_argument('files', nargs='+', help='One file or a list of Python source files to check the syntax of.') 21 | parser.add_argument('-r', '--recursive', dest='recursive', action='store_true', 22 | help='Recursively look for Python files to check') 23 | parser.add_argument('-s', '--skip', help='Files that frosted should skip over.', dest='skip', action='append') 24 | parser.add_argument('-d', '--with-doctests', help='Run frosted against doctests', dest='run_doctests', 25 | action='store_true') 26 | parser.add_argument('-i', '--ignore', help='Specify error codes that should be ignored.', 27 | dest='ignore_frosted_errors', action='append') 28 | parser.add_argument('-di', '--dont-ignore', help='Specify error codes that should not be ignored in any case.', 29 | dest='not_ignore_frosted_errors', action='append') 30 | parser.add_argument('-vb', '--verbose', help='Explicitly separate each section of data when displaying errors.', 31 | dest='verbose', action='store_true') 32 | parser.add_argument('-v', '--version', action='version', version='frosted {0}'.format(__version__)) 33 | arguments = dict((key, value) for (key, value) in itemsview(vars(parser.parse_args())) if value) 34 | file_names = arguments.pop('files', []) 35 | if file_names == ['-']: 36 | check(sys.stdin.read(), '', **arguments) 37 | elif arguments.get('recursive'): 38 | warnings = check_recursive(file_names, **arguments) 39 | else: 40 | warnings = 0 41 | for file_path in file_names: 42 | try: 43 | warnings += check_path(file_path, directly_being_checked=len(file_names), **arguments) 44 | except IOError as e: 45 | print("WARNING: Unable to parse file {0} due to {1}".format(file_name, e)) 46 | 47 | raise SystemExit(warnings > 0) 48 | 49 | 50 | if __name__ == "__main__": 51 | main() 52 | -------------------------------------------------------------------------------- /frosted/messages.py: -------------------------------------------------------------------------------- 1 | """frosted/reporter.py. 2 | 3 | Defines the error messages that frosted can output 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 7 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 8 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or 11 | substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 14 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 15 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 16 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 17 | 18 | """ 19 | 20 | from __future__ import absolute_import, division, print_function, unicode_literals 21 | 22 | import re 23 | from collections import namedtuple 24 | 25 | from pies.overrides import * 26 | 27 | BY_CODE = {} 28 | _ERROR_INDEX = 100 29 | 30 | AbstractMessageType = namedtuple('AbstractMessageType', ('error_code', 'name', 'template', 31 | 'keyword', 'error_number')) 32 | 33 | 34 | class MessageType(AbstractMessageType): 35 | 36 | class Message(namedtuple('Message', ('message', 'type', 'lineno', 'col'))): 37 | 38 | def __str__(self): 39 | return self.message 40 | 41 | def __new__(cls, error_code, name, template, keyword='{0!s}'): 42 | global _ERROR_INDEX 43 | new_instance = AbstractMessageType.__new__(cls, error_code, name, template, 44 | keyword, _ERROR_INDEX) 45 | _ERROR_INDEX += 1 46 | BY_CODE[error_code] = new_instance 47 | return new_instance 48 | 49 | def __call__(self, filename, loc=None, *kargs, **kwargs): 50 | values = {'filename': filename, 'lineno': 0, 'col': 0} 51 | if loc: 52 | values['lineno'] = loc.lineno 53 | values['col'] = getattr(loc, 'col_offset', 0) 54 | values.update(kwargs) 55 | 56 | message = self.template.format(*kargs, **values) 57 | if kwargs.get('verbose', False): 58 | keyword = self.keyword.format(*kargs, **values) 59 | return self.Message('{0}:{1}:{2}:{3}:{4}:{5}'.format(filename, values['lineno'], values['col'], 60 | self.error_code, keyword, message), 61 | self, values['lineno'], values['col']) 62 | return self.Message('{0}:{1}: {2}'.format(filename, values['lineno'], message), 63 | self, values['lineno'], values['col']) 64 | 65 | 66 | 67 | class OffsetMessageType(MessageType): 68 | def __call__(self, filename, loc, position=None, *kargs, **kwargs): 69 | if position: 70 | kwargs.update({'lineno': position[0], 'col': position[1]}) 71 | return MessageType.__call__(self, filename, loc, *kargs, **kwargs) 72 | 73 | 74 | class SyntaxErrorType(MessageType): 75 | def __call__(self, filename, msg, lineno, offset, text, *kargs, **kwargs): 76 | kwargs['lineno'] = lineno 77 | line = text.splitlines()[-1] 78 | msg += "\n" + str(line) 79 | if offset is not None: 80 | offset = offset - (len(text) - len(line)) 81 | kwargs['col'] = offset 82 | msg += "\n" + re.sub(r'\S',' ', line[:offset]) + "^" 83 | 84 | return MessageType.__call__(self, filename, None, msg, *kargs, **kwargs) 85 | 86 | 87 | Message = MessageType('I101', 'Generic', '{0}', '') 88 | UnusedImport = MessageType('E101', 'UnusedImport', '{0} imported but unused') 89 | RedefinedWhileUnused = MessageType('E301', 'RedefinedWhileUnused', 90 | 'redefinition of {0!r} from line {1.lineno!r}') 91 | RedefinedInListComp = MessageType('E302', 'RedefinedInListComp', 92 | 'list comprehension redefines {0!r} from line {1.lineno!r}') 93 | ImportShadowedByLoopVar = MessageType('E102', 'ImportShadowedByLoopVar', 94 | 'import {0!r} from line {1.lineno!r} shadowed by loop variable') 95 | ImportStarUsed = MessageType('E103', 'ImportStarUsed', 96 | "'from {0!s} import *' used; unable to detect undefined names", '*') 97 | UndefinedName = MessageType('E303', 'UndefinedName', "undefined name {0!r}") 98 | DoctestSyntaxError = OffsetMessageType('E401', 'DoctestSyntaxError', "syntax error in doctest", '') 99 | UndefinedExport = MessageType('E304', 'UndefinedExport', "undefined name {0!r} in __all__") 100 | UndefinedLocal = MessageType('E305', 'UndefinedLocal', 101 | 'local variable {0!r} (defined in enclosing scope on line {1.lineno!r}) referenced before assignment') 102 | DuplicateArgument = MessageType('E206', 'DuplicateArgument', "duplicate argument {0!r} in function definition") 103 | Redefined = MessageType('E306', 'Redefined', "redefinition of {0!r} from line {1.lineno!r}") 104 | LateFutureImport = MessageType('E207', 'LateFutureImport', "future import(s) {0!r} after other statements") 105 | UnusedVariable = MessageType('E307', 'UnusedVariable', "local variable {0!r} is assigned to but never used") 106 | MultipleValuesForArgument = MessageType('E201', 'MultipleValuesForArgument', 107 | "{0!s}() got multiple values for argument {1!r}") 108 | TooFewArguments = MessageType('E202', 'TooFewArguments', "{0!s}() takes at least {1:d} argument(s)") 109 | TooManyArguments = MessageType('E203', 'TooManyArguments', "{0!s}() takes at most {1:d} argument(s)") 110 | UnexpectedArgument = MessageType('E204', 'UnexpectedArgument', "{0!s}() got unexpected keyword argument: {1!r}") 111 | NeedKwOnlyArgument = MessageType('E205', 'NeedKwOnlyArgument', "{0!s}() needs kw-only argument(s): {1!s}") 112 | ReturnWithArgsInsideGenerator = MessageType('E208', 'ReturnWithArgsInsideGenerator', 113 | "'return' with argument inside generator", 'return') 114 | BareExcept = MessageType('W101', 'BareExcept', "bare except used: this is dangerous and should be avoided", 'except') 115 | FileSkipped = MessageType('W201', 'FileSkipped', "Skipped because of the current configuration", 'skipped') 116 | PythonSyntaxError = SyntaxErrorType('E402', 'PythonSyntaxError', "{0!s}", "") 117 | -------------------------------------------------------------------------------- /frosted/reporter.py: -------------------------------------------------------------------------------- 1 | """frosted/reporter.py. 2 | 3 | Defines how errors found by frosted should be displayed to the user 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 7 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 8 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or 11 | substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 14 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 15 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 16 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 17 | 18 | """ 19 | from __future__ import absolute_import, division, print_function, unicode_literals 20 | 21 | import sys 22 | from collections import namedtuple 23 | 24 | from pies.overrides import * 25 | 26 | 27 | class Reporter(namedtuple('Reporter', ('stdout', 'stderr'))): 28 | """Formats the results of frosted checks and then presents them to the user.""" 29 | 30 | def unexpected_error(self, filename, msg): 31 | """Output an unexpected_error specific to the provided filename.""" 32 | self.stderr.write("%s: %s\n" % (filename, msg)) 33 | 34 | def flake(self, message): 35 | """Print an error message to stdout.""" 36 | self.stdout.write(str(message)) 37 | self.stdout.write('\n') 38 | 39 | Default = Reporter(sys.stdout, sys.stderr) 40 | -------------------------------------------------------------------------------- /frosted/settings.py: -------------------------------------------------------------------------------- 1 | """frosted/settings.py. 2 | 3 | Defines how the default settings for frosted should be loaded 4 | 5 | (First from the default setting dictionary at the top of the file, then overridden by any settings 6 | in ~/.frosted.conf if there are any) 7 | 8 | Copyright (C) 2013 Timothy Edmund Crosley 9 | 10 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 11 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 12 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 13 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all copies or 16 | substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 20 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 21 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | """ 25 | from __future__ import absolute_import, division, print_function, unicode_literals 26 | 27 | import os 28 | 29 | from pies.functools import lru_cache 30 | from pies.overrides import * 31 | 32 | try: 33 | import configparser 34 | except ImportError: 35 | import ConfigParser as configparser 36 | 37 | MAX_CONFIG_SEARCH_DEPTH = 25 # The number of parent directories frosted will look for a config file within 38 | 39 | # Note that none of these lists must be complete as they are simply fallbacks for when included auto-detection fails. 40 | default = {'skip': [], 41 | 'ignore_frosted_errors': ['W201'], 42 | 'ignore_frosted_errors_for__init__.py': ['E101', 'E103'], 43 | 'verbose': False, 44 | 'run_doctests': False} 45 | 46 | 47 | @lru_cache() 48 | def from_path(path): 49 | computed_settings = default.copy() 50 | _update_settings_with_config(path, '.editorconfig', '~/.editorconfig', ('*', '*.py', '**.py'), computed_settings) 51 | _update_settings_with_config(path, '.frosted.cfg', '~/.frosted.cfg', ('settings', ), computed_settings) 52 | _update_settings_with_config(path, 'setup.cfg', None, ('frosted', ), computed_settings) 53 | return computed_settings 54 | 55 | 56 | def _update_settings_with_config(path, name, default, sections, computed_settings): 57 | editor_config_file = default and os.path.expanduser(default) 58 | tries = 0 59 | current_directory = path 60 | while current_directory and tries < MAX_CONFIG_SEARCH_DEPTH: 61 | potential_path = os.path.join(current_directory, native_str(name)) 62 | if os.path.exists(potential_path): 63 | editor_config_file = potential_path 64 | break 65 | 66 | current_directory = os.path.split(current_directory)[0] 67 | tries += 1 68 | 69 | if editor_config_file and os.path.exists(editor_config_file): 70 | _update_with_config_file(computed_settings, editor_config_file, sections) 71 | 72 | 73 | def _update_with_config_file(computed_settings, file_path, sections): 74 | settings = _get_config_data(file_path, sections) 75 | if not settings: 76 | return 77 | 78 | for key, value in settings.items(): 79 | access_key = key.replace('not_', '').lower() 80 | if key.startswith('ignore_frosted_errors_for'): 81 | existing_value_type = list 82 | else: 83 | existing_value_type = type(default.get(access_key, '')) 84 | if existing_value_type in (list, tuple): 85 | existing_data = set(computed_settings.get(access_key, default.get(access_key)) or ()) 86 | if key.startswith('not_'): 87 | computed_settings[access_key] = list(existing_data.difference(value.split(","))) 88 | else: 89 | computed_settings[access_key] = list(existing_data.union(value.split(","))) 90 | elif existing_value_type == bool and value.lower().strip() == "false": 91 | computed_settings[access_key] = False 92 | else: 93 | computed_settings[access_key] = existing_value_type(value) 94 | 95 | 96 | @lru_cache() 97 | def _get_config_data(file_path, sections): 98 | with open(file_path) as config_file: 99 | if file_path.endswith(".editorconfig"): 100 | line = "\n" 101 | last_position = config_file.tell() 102 | while line: 103 | line = config_file.readline() 104 | if "[" in line: 105 | config_file.seek(last_position) 106 | break 107 | last_position = config_file.tell() 108 | 109 | config = configparser.SafeConfigParser() 110 | config.readfp(config_file) 111 | settings = dict() 112 | for section in sections: 113 | if config.has_section(section): 114 | settings.update(dict(config.items(section))) 115 | 116 | return settings 117 | 118 | return None 119 | -------------------------------------------------------------------------------- /frosted/test/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function, unicode_literals 2 | 3 | from pies.overrides import * 4 | -------------------------------------------------------------------------------- /frosted/test/test_api.py: -------------------------------------------------------------------------------- 1 | """frosted/test/test_api.py. 2 | 3 | Tests all major functionality of the Frosted API 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 7 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 8 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or 11 | substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 14 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 15 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 16 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 17 | OTHER DEALINGS IN THE SOFTWARE. 18 | 19 | """ 20 | from __future__ import absolute_import, division, print_function, unicode_literals 21 | 22 | import os 23 | import sys 24 | import tempfile 25 | from io import StringIO 26 | 27 | import pytest 28 | from pies.overrides import * 29 | 30 | from frosted.api import check_path, check_recursive 31 | from frosted.messages import PythonSyntaxError, UnusedImport 32 | from frosted.reporter import Reporter 33 | 34 | from .utils import LoggingReporter, Node 35 | 36 | 37 | def test_syntax_error(): 38 | """syntax_error reports that there was a syntax error in the source file. 39 | 40 | It reports to the error stream and includes the filename, line number, error message, actual line of source and a 41 | caret pointing to where the error is 42 | 43 | """ 44 | err = StringIO() 45 | reporter = Reporter(err, err) 46 | reporter.flake(PythonSyntaxError('foo.py', 'a problem', 3, 7, 'bad line of source', verbose=True)) 47 | assert ("foo.py:3:7:E402::a problem\n" 48 | "bad line of source\n" 49 | " ^\n") == err.getvalue() 50 | 51 | 52 | def test_syntax_errorNoOffset(): 53 | """syntax_error doesn't include a caret pointing to the error if offset is passed as None.""" 54 | err = StringIO() 55 | reporter = Reporter(err, err) 56 | reporter.flake(PythonSyntaxError('foo.py', 'a problem', 3, None, 'bad line of source', verbose=True)) 57 | assert ("foo.py:3:0:E402::a problem\n" 58 | "bad line of source\n") == err.getvalue() 59 | 60 | 61 | def test_multiLineSyntaxError(): 62 | """ If there's a multi-line syntax error, then we only report the last line. 63 | 64 | The offset is adjusted so that it is relative to the start of the last line 65 | 66 | """ 67 | err = StringIO() 68 | lines = ['bad line of source', 'more bad lines of source'] 69 | reporter = Reporter(err, err) 70 | reporter.flake(PythonSyntaxError('foo.py', 'a problem', 3, len(lines[0]) + 7, '\n'.join(lines), verbose=True)) 71 | assert ("foo.py:3:6:E402::a problem\n" + 72 | lines[-1] + "\n" + 73 | " ^\n") == err.getvalue() 74 | 75 | 76 | def test_unexpected_error(): 77 | """unexpected_error reports an error processing a source file.""" 78 | err = StringIO() 79 | reporter = Reporter(None, err) 80 | reporter.unexpected_error('source.py', 'error message') 81 | assert 'source.py: error message\n' == err.getvalue() 82 | 83 | 84 | def test_flake(): 85 | """flake reports a code warning from Frosted. 86 | 87 | It is exactly the str() of a frosted.messages.Message 88 | 89 | """ 90 | out = StringIO() 91 | reporter = Reporter(out, None) 92 | message = UnusedImport('foo.py', Node(42), 'bar') 93 | reporter.flake(message) 94 | assert out.getvalue() == "%s\n" % (message,) 95 | 96 | 97 | def make_temp_file(content): 98 | """Make a temporary file containing C{content} and return a path to it.""" 99 | _, fpath = tempfile.mkstemp() 100 | if not hasattr(content, 'decode'): 101 | content = content.encode('ascii') 102 | fd = open(fpath, 'wb') 103 | fd.write(content) 104 | fd.close() 105 | return fpath 106 | 107 | 108 | def assert_contains_output(path, flakeList): 109 | """Assert that provided causes at minimal the errors provided in the error list.""" 110 | out = StringIO() 111 | count = check_path(path, Reporter(out, out), verbose=True) 112 | out_string = out.getvalue() 113 | assert len(flakeList) >= count 114 | for flake in flakeList: 115 | assert flake in out_string 116 | 117 | 118 | def get_errors(path): 119 | """Get any warnings or errors reported by frosted for the file at path.""" 120 | log = [] 121 | reporter = LoggingReporter(log) 122 | count = check_path(path, reporter) 123 | return count, log 124 | 125 | 126 | def test_missingTrailingNewline(): 127 | """Source which doesn't end with a newline shouldn't cause any exception to 128 | 129 | be raised nor an error indicator to be returned by check. 130 | 131 | """ 132 | fName = make_temp_file("def foo():\n\tpass\n\t") 133 | assert_contains_output(fName, []) 134 | 135 | 136 | def test_check_pathNonExisting(): 137 | """check_path handles non-existing files""" 138 | count, errors = get_errors('extremo') 139 | assert count == 1 140 | assert errors == [('unexpected_error', 'extremo', 'No such file or directory')] 141 | 142 | 143 | def test_multilineSyntaxError(): 144 | """Source which includes a syntax error which results in the raised SyntaxError. 145 | 146 | text containing multiple lines of source are reported with only 147 | the last line of that source. 148 | 149 | """ 150 | source = """\ 151 | def foo(): 152 | ''' 153 | 154 | def bar(): 155 | pass 156 | 157 | def baz(): 158 | '''quux''' 159 | """ 160 | # Sanity check - SyntaxError.text should be multiple lines, if it 161 | # isn't, something this test was unprepared for has happened. 162 | def evaluate(source): 163 | exec(source) 164 | try: 165 | evaluate(source) 166 | except SyntaxError: 167 | e = sys.exc_info()[1] 168 | assert e.text.count('\n') > 1 169 | else: 170 | assert False 171 | 172 | sourcePath = make_temp_file(source) 173 | assert_contains_output( 174 | sourcePath, 175 | ["""\ 176 | %s:8:10:E402::invalid syntax 177 | '''quux''' 178 | ^ 179 | """ % (sourcePath,)]) 180 | 181 | 182 | def test_eofSyntaxError(): 183 | """The error reported for source files which end prematurely causing a 184 | syntax error reflects the cause for the syntax error. 185 | 186 | """ 187 | sourcePath = make_temp_file("def foo(") 188 | assert_contains_output(sourcePath, ["""\ 189 | %s:1:8:E402::unexpected EOF while parsing 190 | def foo( 191 | ^ 192 | """ % (sourcePath,)]) 193 | 194 | 195 | def test_nonDefaultFollowsDefaultSyntaxError(): 196 | """ Source which has a non-default argument following a default argument 197 | 198 | should include the line number of the syntax error 199 | However these exceptions do not include an offset 200 | 201 | """ 202 | source = """\ 203 | def foo(bar=baz, bax): 204 | pass 205 | """ 206 | sourcePath = make_temp_file(source) 207 | last_line = ' ^\n' if sys.version_info >= (3, 2) else '' 208 | column = '7:' if sys.version_info >= (3, 2) else '0:' 209 | assert_contains_output(sourcePath, ["""\ 210 | %s:1:%sE402::non-default argument follows default argument 211 | def foo(bar=baz, bax): 212 | %s""" % (sourcePath, column, last_line)]) 213 | 214 | 215 | def test_nonKeywordAfterKeywordSyntaxError(): 216 | """Source which has a non-keyword argument after a keyword argument 217 | 218 | should include the line number of the syntax error 219 | However these exceptions do not include an offset 220 | """ 221 | source = """\ 222 | foo(bar=baz, bax) 223 | """ 224 | sourcePath = make_temp_file(source) 225 | last_line = ' ^\n' if sys.version_info >= (3, 2) else '' 226 | column = '12:' if sys.version_info >= (3, 2) else '0:' 227 | assert_contains_output( 228 | sourcePath, 229 | ["""\ 230 | %s:1:%sE402::non-keyword arg after keyword arg 231 | foo(bar=baz, bax) 232 | %s""" % (sourcePath, column, last_line)]) 233 | 234 | 235 | def test_invalidEscape(): 236 | """The invalid escape syntax raises ValueError in Python 2.""" 237 | sourcePath = make_temp_file(r"foo = '\xyz'") 238 | if PY2: 239 | decoding_error = "%s: problem decoding source\n" % (sourcePath,) 240 | else: 241 | decoding_error = "(unicode error) 'unicodeescape' codec can't decode bytes" 242 | assert_contains_output(sourcePath, (decoding_error, )) 243 | 244 | 245 | def test_permissionDenied(): 246 | """If the source file is not readable, this is reported on standard error.""" 247 | sourcePath = make_temp_file('') 248 | os.chmod(sourcePath, 0) 249 | count, errors = get_errors(sourcePath) 250 | assert count == 1 251 | assert errors == [('unexpected_error', sourcePath, "Permission denied")] 252 | 253 | 254 | def test_frostedWarning(): 255 | """If the source file has a frosted warning, this is reported as a 'flake'.""" 256 | sourcePath = make_temp_file("import foo") 257 | count, errors = get_errors(sourcePath) 258 | assert count == 1 259 | assert errors == [('flake', str(UnusedImport(sourcePath, Node(1), 'foo')))] 260 | 261 | 262 | @pytest.mark.skipif("PY3") 263 | def test_misencodedFileUTF8(): 264 | """If a source file contains bytes which cannot be decoded, this is reported on stderr.""" 265 | SNOWMAN = chr(0x2603) 266 | source = ("""\ 267 | # coding: ascii 268 | x = "%s" 269 | """ % SNOWMAN).encode('utf-8') 270 | sourcePath = make_temp_file(source) 271 | assert_contains_output(sourcePath, ["%s: problem decoding source\n" % (sourcePath, )]) 272 | 273 | 274 | def test_misencodedFileUTF16(): 275 | """If a source file contains bytes which cannot be decoded, this is reported on stderr.""" 276 | SNOWMAN = chr(0x2603) 277 | source = ("""\ 278 | # coding: ascii 279 | x = "%s" 280 | """ % SNOWMAN).encode('utf-16') 281 | sourcePath = make_temp_file(source) 282 | assert_contains_output(sourcePath, ["%s: problem decoding source\n" % (sourcePath,)]) 283 | 284 | 285 | def test_check_recursive(): 286 | """check_recursive descends into each directory, finding Python files and reporting problems.""" 287 | tempdir = tempfile.mkdtemp() 288 | os.mkdir(os.path.join(tempdir, 'foo')) 289 | file1 = os.path.join(tempdir, 'foo', 'bar.py') 290 | fd = open(file1, 'wb') 291 | fd.write("import baz\n".encode('ascii')) 292 | fd.close() 293 | file2 = os.path.join(tempdir, 'baz.py') 294 | fd = open(file2, 'wb') 295 | fd.write("import contraband".encode('ascii')) 296 | fd.close() 297 | log = [] 298 | reporter = LoggingReporter(log) 299 | warnings = check_recursive([tempdir], reporter) 300 | assert warnings == 2 301 | assert sorted(log) == sorted([('flake', str(UnusedImport(file1, Node(1), 'baz'))), 302 | ('flake', str(UnusedImport(file2, Node(1), 'contraband')))]) 303 | -------------------------------------------------------------------------------- /frosted/test/test_doctests.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function, unicode_literals 2 | 3 | import textwrap 4 | 5 | import pytest 6 | from pies.overrides import * 7 | 8 | from frosted import messages as m 9 | 10 | from .utils import flakes 11 | 12 | 13 | def doctestify(input): 14 | lines = [] 15 | for line in textwrap.dedent(input).splitlines(): 16 | if line.strip() == '': 17 | pass 18 | elif (line.startswith(' ') or 19 | line.startswith('except:') or 20 | line.startswith('except ') or 21 | line.startswith('finally:') or 22 | line.startswith('else:') or 23 | line.startswith('elif ')): 24 | line = "... %s" % line 25 | else: 26 | line = ">>> %s" % line 27 | lines.append(line) 28 | doctestificator = textwrap.dedent('''\ 29 | def doctest_something(): 30 | """ 31 | %s 32 | """ 33 | ''') 34 | return doctestificator % "\n ".join(lines) 35 | 36 | 37 | def test_doubleNestingReportsClosestName(): 38 | """Lines in doctest are a bit different so we can't use the test from TestUndefinedNames.""" 39 | exc = flakes(''' 40 | def doctest_stuff(): 41 | """ 42 | >>> def a(): 43 | ... x = 1 44 | ... def b(): 45 | ... x = 2 # line 7 in the file 46 | ... def c(): 47 | ... x 48 | ... x = 3 49 | ... return x 50 | ... return x 51 | ... return x 52 | 53 | """ 54 | ''', m.UndefinedLocal, run_doctests=True).messages[0] 55 | 56 | assert "local variable 'x'" in exc.message and 'line 7' in exc.message 57 | 58 | 59 | def test_importBeforeDoctest(): 60 | flakes(""" 61 | import foo 62 | 63 | def doctest_stuff(): 64 | ''' 65 | >>> foo 66 | ''' 67 | """, run_doctests=True) 68 | 69 | 70 | @pytest.mark.skipif("'todo'") 71 | def test_importBeforeAndInDoctest(): 72 | flakes(''' 73 | import foo 74 | 75 | def doctest_stuff(): 76 | """ 77 | >>> import foo 78 | >>> foo 79 | """ 80 | 81 | foo 82 | ''', m.Redefined, run_doctests=True) 83 | 84 | 85 | def test_importInDoctestAndAfter(): 86 | flakes(''' 87 | def doctest_stuff(): 88 | """ 89 | >>> import foo 90 | >>> foo 91 | """ 92 | 93 | import foo 94 | foo() 95 | ''', run_doctests=True) 96 | 97 | 98 | def test_offsetInDoctests(): 99 | exc = flakes(''' 100 | 101 | def doctest_stuff(): 102 | """ 103 | >>> x # line 5 104 | """ 105 | 106 | ''', m.UndefinedName, run_doctests=True).messages[0] 107 | assert exc.lineno == 5 108 | assert exc.col == 12 109 | 110 | 111 | def test_ignoreErrorsByDefault(): 112 | flakes(''' 113 | 114 | def doctest_stuff(): 115 | """ 116 | >>> x # line 5 117 | """ 118 | 119 | ''') 120 | 121 | def test_offsetInLambdasInDoctests(): 122 | exc = flakes(''' 123 | 124 | def doctest_stuff(): 125 | """ 126 | >>> lambda: x # line 5 127 | """ 128 | 129 | ''', m.UndefinedName, run_doctests=True).messages[0] 130 | assert exc.lineno == 5 131 | assert exc.col == 20 132 | 133 | 134 | def test_offsetAfterDoctests(): 135 | exc = flakes(''' 136 | 137 | def doctest_stuff(): 138 | """ 139 | >>> x = 5 140 | """ 141 | 142 | x 143 | 144 | ''', m.UndefinedName, run_doctests=True).messages[0] 145 | assert exc.lineno == 8 146 | assert exc.col == 0 147 | 148 | 149 | def test_syntax_errorInDoctest(): 150 | exceptions = flakes( 151 | ''' 152 | def doctest_stuff(): 153 | """ 154 | >>> from # line 4 155 | >>> fortytwo = 42 156 | >>> except Exception: 157 | """ 158 | ''', 159 | m.DoctestSyntaxError, 160 | m.DoctestSyntaxError, run_doctests=True).messages 161 | exc = exceptions[0] 162 | assert exc.lineno == 4 163 | assert exc.col == 26 164 | exc = exceptions[1] 165 | assert exc.lineno == 6 166 | assert exc.col == 18 167 | 168 | 169 | def test_indentationErrorInDoctest(): 170 | exc = flakes(''' 171 | def doctest_stuff(): 172 | """ 173 | >>> if True: 174 | ... pass 175 | """ 176 | ''', m.DoctestSyntaxError, run_doctests=True).messages[0] 177 | assert exc.lineno == 5 178 | assert exc.col == 16 179 | 180 | 181 | def test_offsetWithMultiLineArgs(): 182 | (exc1, exc2) = flakes( 183 | ''' 184 | def doctest_stuff(arg1, 185 | arg2, 186 | arg3): 187 | """ 188 | >>> assert 189 | >>> this 190 | """ 191 | ''', 192 | m.DoctestSyntaxError, 193 | m.UndefinedName, run_doctests=True).messages 194 | assert exc1.lineno == 6 195 | assert exc1.col == 19 196 | assert exc2.lineno == 7 197 | assert exc2.col == 12 198 | 199 | 200 | def test_doctestCanReferToFunction(): 201 | flakes(""" 202 | def foo(): 203 | ''' 204 | >>> foo 205 | ''' 206 | """, run_doctests=True) 207 | 208 | 209 | def test_doctestCanReferToClass(): 210 | flakes(""" 211 | class Foo(): 212 | ''' 213 | >>> Foo 214 | ''' 215 | def bar(self): 216 | ''' 217 | >>> Foo 218 | ''' 219 | """, run_doctests=True) 220 | 221 | 222 | def test_noOffsetSyntaxErrorInDoctest(): 223 | exceptions = flakes(''' 224 | def buildurl(base, *args, **kwargs): 225 | """ 226 | >>> buildurl('/blah.php', ('a', '&'), ('b', '=') 227 | '/blah.php?a=%26&b=%3D' 228 | >>> buildurl('/blah.php', a='&', 'b'='=') 229 | '/blah.php?b=%3D&a=%26' 230 | """ 231 | pass 232 | ''', 233 | m.DoctestSyntaxError, 234 | m.DoctestSyntaxError, run_doctests=True).messages 235 | exc = exceptions[0] 236 | assert exc.lineno == 4 237 | exc = exceptions[1] 238 | assert exc.lineno == 6 239 | -------------------------------------------------------------------------------- /frosted/test/test_function_calls.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | from __future__ import absolute_import, division, print_function, unicode_literals 5 | 6 | import sys 7 | 8 | from pies.overrides import * 9 | 10 | from frosted import messages as m 11 | 12 | from .utils import flakes 13 | 14 | 15 | def test_ok(): 16 | flakes(''' 17 | def foo(a): 18 | pass 19 | foo(5) 20 | ''') 21 | 22 | flakes(''' 23 | def foo(a, b=2): 24 | pass 25 | foo(5, b=1) 26 | ''') 27 | 28 | 29 | def test_noCheckDecorators(): 30 | flakes(''' 31 | def decorator(f): 32 | return f 33 | @decorator 34 | def foo(): 35 | pass 36 | foo(42) 37 | ''') 38 | 39 | 40 | def test_tooManyArguments(): 41 | flakes(''' 42 | def foo(): 43 | pass 44 | foo(5) 45 | ''', m.TooManyArguments) 46 | flakes(''' 47 | def foo(a, b): 48 | pass 49 | foo(5, 6, 7) 50 | ''', m.TooManyArguments) 51 | 52 | 53 | def test_tooManyArgumentsVarargs(): 54 | flakes(''' 55 | def foo(a, *args): 56 | pass 57 | foo(1, 2, 3) 58 | ''') 59 | 60 | 61 | def test_unexpectedArgument(): 62 | flakes(''' 63 | def foo(a): 64 | pass 65 | foo(1, b=3) 66 | ''', m.UnexpectedArgument) 67 | 68 | flakes(''' 69 | def foo(a, *args): 70 | pass 71 | foo(1, b=3) 72 | ''', m.UnexpectedArgument) 73 | 74 | flakes(''' 75 | def foo(a, **kwargs): 76 | pass 77 | foo(1, b=3) 78 | ''') 79 | 80 | 81 | def test_multipleValuesForArgument(): 82 | flakes(''' 83 | def foo(a): 84 | pass 85 | foo(5, a=5) 86 | ''', m.MultipleValuesForArgument) 87 | 88 | 89 | def test_tooFewArguments(): 90 | flakes(''' 91 | def foo(a): 92 | pass 93 | foo() 94 | ''', m.TooFewArguments) 95 | 96 | flakes(''' 97 | def foo(a): 98 | pass 99 | foo(*[]) 100 | ''') 101 | 102 | flakes(''' 103 | def foo(a): 104 | pass 105 | foo(**{}) 106 | ''') 107 | 108 | 109 | def test_tooFewArgumentsVarArgs(): 110 | flakes(''' 111 | def foo(a, b, *args): 112 | pass 113 | foo(1) 114 | ''', m.TooFewArguments) 115 | 116 | 117 | if PY3: 118 | def test_kwOnlyArguments(): 119 | flakes(''' 120 | def foo(a, *, b=0): 121 | pass 122 | foo(5, b=2) 123 | ''') 124 | 125 | flakes(''' 126 | def foo(a, *, b=0): 127 | pass 128 | foo(5) 129 | ''') 130 | 131 | flakes(''' 132 | def foo(a, *, b): 133 | pass 134 | foo(5, b=2) 135 | ''') 136 | 137 | flakes(''' 138 | def foo(a, *, b): 139 | pass 140 | foo(5, **{}) 141 | ''') 142 | 143 | flakes(''' 144 | def foo(a, *, b): 145 | pass 146 | foo(1) 147 | ''', m.NeedKwOnlyArgument) 148 | 149 | flakes(''' 150 | def foo(a, *args, b): 151 | pass 152 | foo(1, 2, 3, 4) 153 | ''', m.NeedKwOnlyArgument) 154 | elif PY2: 155 | def test_compoundArguments(): 156 | flakes(''' 157 | def foo(a, (b, c)): 158 | pass 159 | foo(1, [])''') 160 | 161 | flakes(''' 162 | def foo(a, (b, c)): 163 | pass 164 | foo(1, 2, 3)''', m.TooManyArguments) 165 | 166 | flakes(''' 167 | def foo(a, (b, c)): 168 | pass 169 | foo(1)''', m.TooFewArguments) 170 | 171 | flakes(''' 172 | def foo(a, (b, c)): 173 | pass 174 | foo(1, b=2, c=3)''', m.UnexpectedArgument) 175 | -------------------------------------------------------------------------------- /frosted/test/test_imports.py: -------------------------------------------------------------------------------- 1 | 2 | from __future__ import absolute_import, division, print_function, unicode_literals 3 | 4 | from sys import version_info 5 | 6 | import pytest 7 | from pies.overrides import * 8 | 9 | from frosted import messages as m 10 | 11 | from .utils import flakes 12 | 13 | 14 | def test_unusedImport(): 15 | flakes('import fu, bar', m.UnusedImport, m.UnusedImport) 16 | flakes('from baz import fu, bar', m.UnusedImport, m.UnusedImport) 17 | 18 | 19 | def test_aliasedImport(): 20 | flakes('import fu as FU, bar as FU', 21 | m.RedefinedWhileUnused, m.UnusedImport) 22 | flakes('from moo import fu as FU, bar as FU', 23 | m.RedefinedWhileUnused, m.UnusedImport) 24 | 25 | 26 | def test_usedImport(): 27 | flakes('import fu; print(fu)') 28 | flakes('from baz import fu; print(fu)') 29 | flakes('import fu; del fu') 30 | 31 | 32 | def test_redefinedWhileUnused(): 33 | flakes('import fu; fu = 3', m.RedefinedWhileUnused) 34 | flakes('import fu; fu, bar = 3', m.RedefinedWhileUnused) 35 | flakes('import fu; [fu, bar] = 3', m.RedefinedWhileUnused) 36 | 37 | 38 | def test_redefinedIf(): 39 | """Test that importing a module twice within an if block does raise a warning.""" 40 | flakes(''' 41 | i = 2 42 | if i==1: 43 | import os 44 | import os 45 | os.path''', m.RedefinedWhileUnused) 46 | 47 | 48 | def test_redefinedIfElse(): 49 | """Test that importing a module twice in if and else blocks does not raise a warning.""" 50 | flakes(''' 51 | i = 2 52 | if i==1: 53 | import os 54 | else: 55 | import os 56 | os.path''') 57 | 58 | 59 | def test_redefinedTry(): 60 | """Test that importing a module twice in an try block does raise a warning.""" 61 | flakes(''' 62 | try: 63 | import os 64 | import os 65 | except Exception: 66 | pass 67 | os.path''', m.RedefinedWhileUnused) 68 | 69 | 70 | def test_redefinedTryExcept(): 71 | """Test that importing a module twice in an try and except block does not raise a warning.""" 72 | flakes(''' 73 | try: 74 | import os 75 | except Exception: 76 | import os 77 | os.path''') 78 | 79 | 80 | def test_redefinedTryNested(): 81 | """Test that importing a module twice using a nested try/except and if blocks does not issue a warning.""" 82 | flakes(''' 83 | try: 84 | if True: 85 | if True: 86 | import os 87 | except Exception: 88 | import os 89 | os.path''') 90 | 91 | 92 | def test_redefinedTryExceptMulti(): 93 | flakes(""" 94 | try: 95 | from aa import mixer 96 | except AttributeError: 97 | from bb import mixer 98 | except RuntimeError: 99 | from cc import mixer 100 | except Exception: 101 | from dd import mixer 102 | mixer(123) 103 | """) 104 | 105 | 106 | def test_redefinedTryElse(): 107 | flakes(""" 108 | try: 109 | from aa import mixer 110 | except ImportError: 111 | pass 112 | else: 113 | from bb import mixer 114 | mixer(123) 115 | """, m.RedefinedWhileUnused) 116 | 117 | 118 | def test_redefinedTryExceptElse(): 119 | flakes(""" 120 | try: 121 | import funca 122 | except ImportError: 123 | from bb import funca 124 | from bb import funcb 125 | else: 126 | from bbb import funcb 127 | print(funca, funcb) 128 | """) 129 | 130 | 131 | def test_redefinedTryExceptFinally(): 132 | flakes(""" 133 | try: 134 | from aa import a 135 | except ImportError: 136 | from bb import a 137 | finally: 138 | a = 42 139 | print(a) 140 | """) 141 | 142 | 143 | def test_redefinedTryExceptElseFinally(): 144 | flakes(""" 145 | try: 146 | import b 147 | except ImportError: 148 | b = Ellipsis 149 | from bb import a 150 | else: 151 | from aa import a 152 | finally: 153 | a = 42 154 | print(a, b) 155 | """) 156 | 157 | 158 | def test_redefinedByFunction(): 159 | flakes(''' 160 | import fu 161 | def fu(): 162 | pass 163 | ''', m.RedefinedWhileUnused) 164 | 165 | 166 | def test_redefinedInNestedFunction(): 167 | """Test that shadowing a global name with a nested function definition generates a warning.""" 168 | flakes(''' 169 | import fu 170 | def bar(): 171 | def baz(): 172 | def fu(): 173 | pass 174 | ''', m.RedefinedWhileUnused, m.UnusedImport) 175 | 176 | 177 | def test_redefinedByClass(): 178 | flakes(''' 179 | import fu 180 | class fu: 181 | pass 182 | ''', m.RedefinedWhileUnused) 183 | 184 | 185 | def test_redefinedBySubclass(): 186 | """If an imported name is redefined by a class statement 187 | 188 | which also uses that name in the bases list, no warning is emitted. 189 | 190 | """ 191 | flakes(''' 192 | from fu import bar 193 | class bar(bar): 194 | pass 195 | ''') 196 | 197 | 198 | def test_redefinedInClass(): 199 | """Test that shadowing a global with a class attribute does not produce a warning.""" 200 | flakes(''' 201 | import fu 202 | class bar: 203 | fu = 1 204 | print(fu) 205 | ''') 206 | 207 | 208 | def test_usedInFunction(): 209 | flakes(''' 210 | import fu 211 | def fun(): 212 | print(fu) 213 | ''') 214 | 215 | 216 | def test_shadowedByParameter(): 217 | flakes(''' 218 | import fu 219 | def fun(fu): 220 | print(fu) 221 | ''', m.UnusedImport) 222 | 223 | flakes(''' 224 | import fu 225 | def fun(fu): 226 | print(fu) 227 | print(fu) 228 | ''') 229 | 230 | 231 | def test_newAssignment(): 232 | flakes('fu = None') 233 | 234 | 235 | def test_usedInGetattr(): 236 | flakes('import fu; fu.bar.baz') 237 | flakes('import fu; "bar".fu.baz', m.UnusedImport) 238 | 239 | 240 | def test_usedInSlice(): 241 | flakes('import fu; print(fu.bar[1:])') 242 | 243 | 244 | def test_usedInIfBody(): 245 | flakes(''' 246 | import fu 247 | if True: print(fu) 248 | ''') 249 | 250 | 251 | def test_usedInIfConditional(): 252 | flakes(''' 253 | import fu 254 | if fu: pass 255 | ''') 256 | 257 | 258 | def test_usedInElifConditional(): 259 | flakes(''' 260 | import fu 261 | if False: pass 262 | elif fu: pass 263 | ''') 264 | 265 | 266 | def test_usedInElse(): 267 | flakes(''' 268 | import fu 269 | if False: pass 270 | else: print(fu) 271 | ''') 272 | 273 | 274 | def test_usedInCall(): 275 | flakes('import fu; fu.bar()') 276 | 277 | 278 | def test_usedInClass(): 279 | flakes(''' 280 | import fu 281 | class bar: 282 | bar = fu 283 | ''') 284 | 285 | 286 | def test_usedInClassBase(): 287 | flakes(''' 288 | import fu 289 | class bar(object, fu.baz): 290 | pass 291 | ''') 292 | 293 | 294 | def test_notUsedInNestedScope(): 295 | flakes(''' 296 | import fu 297 | def bleh(): 298 | pass 299 | print(fu) 300 | ''') 301 | 302 | 303 | def test_usedInFor(): 304 | flakes(''' 305 | import fu 306 | for bar in range(9): 307 | print(fu) 308 | ''') 309 | 310 | 311 | def test_usedInForElse(): 312 | flakes(''' 313 | import fu 314 | for bar in range(10): 315 | pass 316 | else: 317 | print(fu) 318 | ''') 319 | 320 | 321 | def test_redefinedByFor(): 322 | flakes(''' 323 | import fu 324 | for fu in range(2): 325 | pass 326 | ''', m.RedefinedWhileUnused) 327 | 328 | 329 | def test_shadowedByFor(): 330 | """Test that shadowing a global name with a for loop variable generates a warning.""" 331 | flakes(''' 332 | import fu 333 | fu.bar() 334 | for fu in (): 335 | pass 336 | ''', m.ImportShadowedByLoopVar) 337 | 338 | 339 | def test_shadowedByForDeep(): 340 | """Test that shadowing a global name with a for loop variable nested in a tuple unpack generates a warning.""" 341 | flakes(''' 342 | import fu 343 | fu.bar() 344 | for (x, y, z, (a, b, c, (fu,))) in (): 345 | pass 346 | ''', m.ImportShadowedByLoopVar) 347 | 348 | 349 | def test_usedInReturn(): 350 | flakes(''' 351 | import fu 352 | def fun(): 353 | return fu 354 | ''') 355 | 356 | 357 | def test_usedInOperators(): 358 | flakes('import fu; 3 + fu.bar') 359 | flakes('import fu; 3 % fu.bar') 360 | flakes('import fu; 3 - fu.bar') 361 | flakes('import fu; 3 * fu.bar') 362 | flakes('import fu; 3 ** fu.bar') 363 | flakes('import fu; 3 / fu.bar') 364 | flakes('import fu; 3 // fu.bar') 365 | flakes('import fu; -fu.bar') 366 | flakes('import fu; ~fu.bar') 367 | flakes('import fu; 1 == fu.bar') 368 | flakes('import fu; 1 | fu.bar') 369 | flakes('import fu; 1 & fu.bar') 370 | flakes('import fu; 1 ^ fu.bar') 371 | flakes('import fu; 1 >> fu.bar') 372 | flakes('import fu; 1 << fu.bar') 373 | 374 | 375 | def test_usedInAssert(): 376 | flakes('import fu; assert fu.bar') 377 | 378 | 379 | def test_usedInSubscript(): 380 | flakes('import fu; fu.bar[1]') 381 | 382 | 383 | def test_usedInLogic(): 384 | flakes('import fu; fu and False') 385 | flakes('import fu; fu or False') 386 | flakes('import fu; not fu.bar') 387 | 388 | 389 | def test_usedInList(): 390 | flakes('import fu; [fu]') 391 | 392 | 393 | def test_usedInTuple(): 394 | flakes('import fu; (fu,)') 395 | 396 | 397 | def test_usedInTry(): 398 | flakes(''' 399 | import fu 400 | try: fu 401 | except Exception: pass 402 | ''') 403 | 404 | 405 | def test_usedInExcept(): 406 | flakes(''' 407 | import fu 408 | try: fu 409 | except Exception: pass 410 | ''') 411 | 412 | 413 | def test_redefinedByExcept(): 414 | as_exc = ', ' if version_info < (2, 6) else ' as ' 415 | flakes(''' 416 | import fu 417 | try: pass 418 | except Exception%sfu: pass 419 | ''' % as_exc, m.RedefinedWhileUnused) 420 | 421 | 422 | def test_usedInRaise(): 423 | flakes(''' 424 | import fu 425 | raise fu.bar 426 | ''') 427 | 428 | 429 | def test_usedInYield(): 430 | flakes(''' 431 | import fu 432 | def gen(): 433 | yield fu 434 | ''') 435 | 436 | 437 | def test_usedInDict(): 438 | flakes('import fu; {fu:None}') 439 | flakes('import fu; {1:fu}') 440 | 441 | 442 | def test_usedInParameterDefault(): 443 | flakes(''' 444 | import fu 445 | def f(bar=fu): 446 | pass 447 | ''') 448 | 449 | 450 | def test_usedInAttributeAssign(): 451 | flakes('import fu; fu.bar = 1') 452 | 453 | 454 | def test_usedInKeywordArg(): 455 | flakes('import fu; fu.bar(stuff=fu)') 456 | 457 | 458 | def test_usedInAssignment(): 459 | flakes('import fu; bar=fu') 460 | flakes('import fu; n=0; n+=fu') 461 | 462 | 463 | def test_usedInListComp(): 464 | flakes('import fu; [fu for _ in range(1)]') 465 | flakes('import fu; [1 for _ in range(1) if fu]') 466 | 467 | 468 | def test_redefinedByListComp(): 469 | flakes('import fu; [1 for fu in range(1)]', m.RedefinedWhileUnused) 470 | 471 | 472 | def test_usedInTryFinally(): 473 | flakes(''' 474 | import fu 475 | try: pass 476 | finally: fu 477 | ''') 478 | 479 | flakes(''' 480 | import fu 481 | try: fu 482 | finally: pass 483 | ''') 484 | 485 | 486 | def test_usedInWhile(): 487 | flakes(''' 488 | import fu 489 | while 0: 490 | fu 491 | ''') 492 | 493 | flakes(''' 494 | import fu 495 | while fu: pass 496 | ''') 497 | 498 | 499 | def test_usedInGlobal(): 500 | flakes(''' 501 | import fu 502 | def f(): global fu 503 | ''', m.UnusedImport) 504 | 505 | 506 | @pytest.mark.skipif("version_info >= (3,)") 507 | def test_usedInBackquote(): 508 | flakes('import fu; `fu`') 509 | 510 | 511 | def test_usedInExec(): 512 | if version_info < (3,): 513 | exec_stmt = 'exec "print 1" in fu.bar' 514 | else: 515 | exec_stmt = 'exec("print(1)", fu.bar)' 516 | flakes('import fu; %s' % exec_stmt) 517 | 518 | 519 | def test_usedInLambda(): 520 | flakes('import fu; lambda: fu') 521 | 522 | 523 | def test_shadowedByLambda(): 524 | flakes('import fu; lambda fu: fu', m.UnusedImport) 525 | 526 | 527 | def test_usedInSliceObj(): 528 | flakes('import fu; "meow"[::fu]') 529 | 530 | 531 | def test_unusedInNestedScope(): 532 | flakes(''' 533 | def bar(): 534 | import fu 535 | fu 536 | ''', m.UnusedImport, m.UndefinedName) 537 | 538 | 539 | def test_methodsDontUseClassScope(): 540 | flakes(''' 541 | class bar: 542 | import fu 543 | def fun(): 544 | fu 545 | ''', m.UnusedImport, m.UndefinedName) 546 | 547 | 548 | def test_nestedFunctionsNestScope(): 549 | flakes(''' 550 | def a(): 551 | def b(): 552 | fu 553 | import fu 554 | ''') 555 | 556 | 557 | def test_nestedClassAndFunctionScope(): 558 | flakes(''' 559 | def a(): 560 | import fu 561 | class b: 562 | def c(): 563 | print(fu) 564 | ''') 565 | 566 | 567 | def test_importStar(): 568 | flakes('from fu import *', m.ImportStarUsed, ignore_frosted_errors=[]) 569 | 570 | 571 | def test_packageImport(): 572 | """If a dotted name is imported and used, no warning is reported.""" 573 | flakes(''' 574 | import fu.bar 575 | fu.bar 576 | ''') 577 | 578 | 579 | def test_unusedPackageImport(): 580 | """If a dotted name is imported and not used, an unused import warning is reported.""" 581 | flakes('import fu.bar', m.UnusedImport) 582 | 583 | 584 | def test_duplicateSubmoduleImport(): 585 | """If a submodule of a package is imported twice, an unused 586 | 587 | import warning and a redefined while unused warning are reported. 588 | 589 | """ 590 | flakes(''' 591 | import fu.bar, fu.bar 592 | fu.bar 593 | ''', m.RedefinedWhileUnused) 594 | flakes(''' 595 | import fu.bar 596 | import fu.bar 597 | fu.bar 598 | ''', m.RedefinedWhileUnused) 599 | 600 | 601 | def test_differentSubmoduleImport(): 602 | """If two different submodules of a package are imported, 603 | 604 | no duplicate import warning is reported for the package. 605 | 606 | """ 607 | flakes(''' 608 | import fu.bar, fu.baz 609 | fu.bar, fu.baz 610 | ''') 611 | flakes(''' 612 | import fu.bar 613 | import fu.baz 614 | fu.bar, fu.baz 615 | ''') 616 | 617 | 618 | def test_assignRHSFirst(): 619 | flakes('import fu; fu = fu') 620 | flakes('import fu; fu, bar = fu') 621 | flakes('import fu; [fu, bar] = fu') 622 | flakes('import fu; fu += fu') 623 | 624 | 625 | def test_tryingMultipleImports(): 626 | flakes(''' 627 | try: 628 | import fu 629 | except ImportError: 630 | import bar as fu 631 | fu 632 | ''') 633 | 634 | 635 | def test_nonGlobalDoesNotRedefine(): 636 | flakes(''' 637 | import fu 638 | def a(): 639 | fu = 3 640 | return fu 641 | fu 642 | ''') 643 | 644 | 645 | def test_functionsRunLater(): 646 | flakes(''' 647 | def a(): 648 | fu 649 | import fu 650 | ''') 651 | 652 | 653 | def test_functionNamesAreBoundNow(): 654 | flakes(''' 655 | import fu 656 | def fu(): 657 | fu 658 | fu 659 | ''', m.RedefinedWhileUnused) 660 | 661 | 662 | def test_ignoreNonImportRedefinitions(): 663 | flakes('a = 1; a = 2') 664 | 665 | 666 | @pytest.mark.skipif("'todo'") 667 | def test_importingForImportError(): 668 | flakes(''' 669 | try: 670 | import fu 671 | except ImportError: 672 | pass 673 | ''') 674 | 675 | 676 | @pytest.mark.skipif("'todo: requires evaluating attribute access'") 677 | def test_importedInClass(): 678 | """Imports in class scope can be used through.""" 679 | flakes(''' 680 | class c: 681 | import i 682 | def __init__(): 683 | i 684 | ''') 685 | 686 | 687 | def test_futureImport(): 688 | """__future__ is special.""" 689 | flakes('from __future__ import division') 690 | flakes(''' 691 | "docstring is allowed before future import" 692 | from __future__ import division 693 | ''') 694 | 695 | 696 | def test_futureImportFirst(): 697 | """__future__ imports must come before anything else.""" 698 | flakes(''' 699 | x = 5 700 | from __future__ import division 701 | ''', m.LateFutureImport) 702 | flakes(''' 703 | from foo import bar 704 | from __future__ import division 705 | bar 706 | ''', m.LateFutureImport) 707 | 708 | 709 | def test_ignoredInFunction(): 710 | """An C{__all__} definition does not suppress unused import warnings in a function scope.""" 711 | flakes(''' 712 | def foo(): 713 | import bar 714 | __all__ = ["bar"] 715 | ''', m.UnusedImport, m.UnusedVariable) 716 | 717 | 718 | def test_ignoredInClass(): 719 | """An C{__all__} definition does not suppress unused import warnings in a class scope.""" 720 | flakes(''' 721 | class foo: 722 | import bar 723 | __all__ = ["bar"] 724 | ''', m.UnusedImport) 725 | 726 | 727 | def test_warningSuppressed(): 728 | """If a name is imported and unused but is named in C{__all__}, no warning is reported.""" 729 | flakes(''' 730 | import foo 731 | __all__ = ["foo"] 732 | ''') 733 | 734 | 735 | def test_unrecognizable(): 736 | """If C{__all__} is defined in a way that can't be recognized statically, it is ignored.""" 737 | flakes(''' 738 | import foo 739 | __all__ = ["f" + "oo"] 740 | ''', m.UnusedImport) 741 | flakes(''' 742 | import foo 743 | __all__ = [] + ["foo"] 744 | ''', m.UnusedImport) 745 | 746 | 747 | def test_unboundExported(): 748 | """If C{__all__} includes a name which is not bound, a warning is emitted.""" 749 | flakes(''' 750 | __all__ = ["foo"] 751 | ''', m.UndefinedExport) 752 | 753 | # Skip this in __init__.py though, since the rules there are a little 754 | # different. 755 | for filename in ["foo/__init__.py", "__init__.py"]: 756 | flakes(''' 757 | __all__ = ["foo"] 758 | ''', filename=filename, **{'ignore_frosted_errors_for___init__.py': ['E101', 'E103']}) 759 | 760 | 761 | def test_importStarExported(): 762 | """Do not report undefined if import * is used""" 763 | flakes(''' 764 | from foolib import * 765 | __all__ = ["foo"] 766 | ''', m.ImportStarUsed) 767 | 768 | 769 | def test_usedInGenExp(): 770 | """Using a global in a generator expression results in no warnings.""" 771 | flakes('import fu; (fu for _ in range(1))') 772 | flakes('import fu; (1 for _ in range(1) if fu)') 773 | 774 | 775 | def test_redefinedByGenExp(): 776 | """ Re-using a global name as the loop variable for a generator 777 | 778 | expression results in a redefinition warning. 779 | 780 | """ 781 | flakes('import fu; (1 for fu in range(1))', m.RedefinedWhileUnused, m.UnusedImport) 782 | 783 | 784 | def test_usedAsDecorator(): 785 | """Using a global name in a decorator statement results in no warnings, but 786 | 787 | using an undefined name in a decorator statement results in an undefined 788 | name warning. 789 | 790 | """ 791 | flakes(''' 792 | from interior import decorate 793 | @decorate 794 | def f(): 795 | return "hello" 796 | ''') 797 | 798 | flakes(''' 799 | from interior import decorate 800 | @decorate('value') 801 | def f(): 802 | return "hello" 803 | ''') 804 | 805 | flakes(''' 806 | @decorate 807 | def f(): 808 | return "hello" 809 | ''', m.UndefinedName) 810 | 811 | 812 | def test_usedAsClassDecorator(): 813 | """Using an imported name as a class decorator results in no warnings 814 | 815 | but using an undefined name as a class decorator results in an undefined name warning. 816 | 817 | """ 818 | flakes(''' 819 | from interior import decorate 820 | @decorate 821 | class foo: 822 | pass 823 | ''') 824 | 825 | flakes(''' 826 | from interior import decorate 827 | @decorate("foo") 828 | class bar: 829 | pass 830 | ''') 831 | 832 | flakes(''' 833 | @decorate 834 | class foo: 835 | pass 836 | ''', m.UndefinedName) 837 | -------------------------------------------------------------------------------- /frosted/test/test_noqa.py: -------------------------------------------------------------------------------- 1 | from frosted import messages as m 2 | from frosted.api import _noqa_lines, _re_noqa, check 3 | from frosted.reporter import Reporter 4 | 5 | from .utils import LoggingReporter, flakes 6 | 7 | 8 | def test_regex(): 9 | # simple format 10 | assert _re_noqa.search('#noqa') 11 | assert _re_noqa.search('# noqa') 12 | # simple format is strict, must be at start of comment 13 | assert not _re_noqa.search('# foo noqa') 14 | 15 | # verbose format (not strict like simple format) 16 | assert _re_noqa.search('#frosted:noqa') 17 | assert _re_noqa.search('# frosted: noqa') 18 | assert _re_noqa.search('# foo frosted: noqa') 19 | 20 | 21 | def test_checker_ignore_lines(): 22 | # ignore same line 23 | flakes('from fu import *', ignore_lines=[1]) 24 | # don't ignore different line 25 | flakes('from fu import *', m.ImportStarUsed, ignore_lines=[2]) 26 | 27 | 28 | def test_noqa_lines(): 29 | assert _noqa_lines('from fu import bar; bar') == [] 30 | assert _noqa_lines('from fu import * # noqa; bar') == [1] 31 | assert _noqa_lines('from fu import * #noqa\nbar\nfoo # frosted: noqa') == [1, 3] 32 | 33 | 34 | def test_check_integration(): 35 | """ make sure all the above logic comes together correctly in the check() function """ 36 | output = [] 37 | reporter = LoggingReporter(output) 38 | 39 | result = check('from fu import *', 'test', reporter, not_ignore_frosted_errors=['E103']) 40 | 41 | # errors reported 42 | assert result == 1 43 | assert "unable to detect undefined names" in output.pop(0)[1] 44 | 45 | # same test, but with ignore set 46 | output = [] 47 | reporter = LoggingReporter(output) 48 | 49 | result = check('from fu import * # noqa', 'test', reporter) 50 | 51 | # errors reported 52 | assert result == 0 53 | assert len(output) == 0 54 | -------------------------------------------------------------------------------- /frosted/test/test_other.py: -------------------------------------------------------------------------------- 1 | """Tests for various Frosted behavior.""" 2 | 3 | from __future__ import absolute_import, division, print_function, unicode_literals 4 | 5 | from sys import version_info 6 | 7 | import pytest 8 | from pies.overrides import * 9 | 10 | from frosted import messages as m 11 | 12 | from .utils import flakes 13 | 14 | 15 | def test_duplicateArgs(): 16 | flakes('def fu(bar, bar): pass', m.DuplicateArgument) 17 | 18 | 19 | @pytest.mark.skipif("'todo: this requires finding all assignments in the function body first'") 20 | def test_localReferencedBeforeAssignment(): 21 | flakes(''' 22 | a = 1 23 | def f(): 24 | a; a=1 25 | f() 26 | ''', m.UndefinedName) 27 | 28 | 29 | def test_redefinedInListComp(): 30 | """Test that shadowing a variable in a list comprehension raises a warning.""" 31 | flakes(''' 32 | a = 1 33 | [1 for a, b in [(1, 2)]] 34 | ''', m.RedefinedInListComp) 35 | flakes(''' 36 | class A: 37 | a = 1 38 | [1 for a, b in [(1, 2)]] 39 | ''', m.RedefinedInListComp) 40 | flakes(''' 41 | def f(): 42 | a = 1 43 | [1 for a, b in [(1, 2)]] 44 | ''', m.RedefinedInListComp) 45 | flakes(''' 46 | [1 for a, b in [(1, 2)]] 47 | [1 for a, b in [(1, 2)]] 48 | ''') 49 | flakes(''' 50 | for a, b in [(1, 2)]: 51 | pass 52 | [1 for a, b in [(1, 2)]] 53 | ''') 54 | 55 | 56 | def test_redefinedInGenerator(): 57 | """Test that reusing a variable in a generator does not raise a warning.""" 58 | flakes(''' 59 | a = 1 60 | (1 for a, b in [(1, 2)]) 61 | ''') 62 | flakes(''' 63 | class A: 64 | a = 1 65 | list(1 for a, b in [(1, 2)]) 66 | ''') 67 | flakes(''' 68 | def f(): 69 | a = 1 70 | (1 for a, b in [(1, 2)]) 71 | ''', m.UnusedVariable) 72 | flakes(''' 73 | (1 for a, b in [(1, 2)]) 74 | (1 for a, b in [(1, 2)]) 75 | ''') 76 | flakes(''' 77 | for a, b in [(1, 2)]: 78 | pass 79 | (1 for a, b in [(1, 2)]) 80 | ''') 81 | 82 | 83 | @pytest.mark.skipif('''version_info < (2, 7)''') 84 | def test_redefinedInSetComprehension(): 85 | """Test that reusing a variable in a set comprehension does not raise a warning.""" 86 | flakes(''' 87 | a = 1 88 | {1 for a, b in [(1, 2)]} 89 | ''') 90 | flakes(''' 91 | class A: 92 | a = 1 93 | {1 for a, b in [(1, 2)]} 94 | ''') 95 | flakes(''' 96 | def f(): 97 | a = 1 98 | {1 for a, b in [(1, 2)]} 99 | ''', m.UnusedVariable) 100 | flakes(''' 101 | {1 for a, b in [(1, 2)]} 102 | {1 for a, b in [(1, 2)]} 103 | ''') 104 | flakes(''' 105 | for a, b in [(1, 2)]: 106 | pass 107 | {1 for a, b in [(1, 2)]} 108 | ''') 109 | 110 | 111 | @pytest.mark.skipif('''version_info < (2, 7)''') 112 | def test_redefinedInDictComprehension(): 113 | """Test that reusing a variable in a dict comprehension does not raise a warning.""" 114 | flakes(''' 115 | a = 1 116 | {1: 42 for a, b in [(1, 2)]} 117 | ''') 118 | flakes(''' 119 | class A: 120 | a = 1 121 | {1: 42 for a, b in [(1, 2)]} 122 | ''') 123 | flakes(''' 124 | def f(): 125 | a = 1 126 | {1: 42 for a, b in [(1, 2)]} 127 | ''', m.UnusedVariable) 128 | flakes(''' 129 | {1: 42 for a, b in [(1, 2)]} 130 | {1: 42 for a, b in [(1, 2)]} 131 | ''') 132 | flakes(''' 133 | for a, b in [(1, 2)]: 134 | pass 135 | {1: 42 for a, b in [(1, 2)]} 136 | ''') 137 | 138 | 139 | def test_redefinedFunction(): 140 | """Test that shadowing a function definition with another one raises a warning.""" 141 | flakes(''' 142 | def a(): pass 143 | def a(): pass 144 | ''', m.RedefinedWhileUnused) 145 | 146 | 147 | def test_redefinedClassFunction(): 148 | """Test that shadowing a function definition in a class suite with another one raises a warning.""" 149 | flakes(''' 150 | class A: 151 | def a(): pass 152 | def a(): pass 153 | ''', m.RedefinedWhileUnused) 154 | 155 | 156 | def test_redefinedIfElseFunction(): 157 | """Test that shadowing a function definition twice in an if and else block does not raise a warning.""" 158 | flakes(''' 159 | if True: 160 | def a(): pass 161 | else: 162 | def a(): pass 163 | ''') 164 | 165 | 166 | def test_redefinedIfFunction(): 167 | """Test that shadowing a function definition within an if block raises a warning.""" 168 | flakes(''' 169 | if True: 170 | def a(): pass 171 | def a(): pass 172 | ''', m.RedefinedWhileUnused) 173 | 174 | 175 | def test_redefinedTryExceptFunction(): 176 | """Test that shadowing a function definition twice in try and except block does not raise a warning.""" 177 | flakes(''' 178 | try: 179 | def a(): pass 180 | except Exception: 181 | def a(): pass 182 | ''') 183 | 184 | 185 | def test_redefinedTryFunction(): 186 | """Test that shadowing a function definition within a try block raises a warning.""" 187 | flakes(''' 188 | try: 189 | def a(): pass 190 | def a(): pass 191 | except Exception: 192 | pass 193 | ''', m.RedefinedWhileUnused) 194 | 195 | 196 | def test_redefinedIfElseInListComp(): 197 | """Test that shadowing a variable in a list comprehension in an if and else block does not raise a warning.""" 198 | flakes(''' 199 | if False: 200 | a = 1 201 | else: 202 | [a for a in '12'] 203 | ''') 204 | 205 | 206 | def test_redefinedElseInListComp(): 207 | """Test that shadowing a variable in a list comprehension in an else (or if) block raises a warning.""" 208 | flakes(''' 209 | if False: 210 | pass 211 | else: 212 | a = 1 213 | [a for a in '12'] 214 | ''', m.RedefinedInListComp) 215 | 216 | 217 | def test_functionDecorator(): 218 | """Test that shadowing a function definition with a decorated version of that function does not raise a warning.""" 219 | flakes(''' 220 | from somewhere import somedecorator 221 | 222 | def a(): pass 223 | a = somedecorator(a) 224 | ''') 225 | 226 | 227 | def test_classFunctionDecorator(): 228 | """Test that shadowing a function definition in a class suite with a 229 | decorated version of that function does not raise a warning. 230 | 231 | """ 232 | flakes(''' 233 | class A: 234 | def a(): pass 235 | a = classmethod(a) 236 | ''') 237 | 238 | 239 | @pytest.mark.skipif('''version_info < (2, 6)''') 240 | def test_modernProperty(): 241 | flakes(""" 242 | class A: 243 | @property 244 | def t(): 245 | pass 246 | @t.setter 247 | def t(self, value): 248 | pass 249 | @t.deleter 250 | def t(): 251 | pass 252 | """) 253 | 254 | 255 | def test_unaryPlus(): 256 | """Don't die on unary +.""" 257 | flakes('+1') 258 | 259 | 260 | def test_undefinedBaseClass(): 261 | """If a name in the base list of a class definition is undefined, a warning is emitted.""" 262 | flakes(''' 263 | class foo(foo): 264 | pass 265 | ''', m.UndefinedName) 266 | 267 | 268 | def test_classNameUndefinedInClassBody(): 269 | """If a class name is used in the body of that class's definition and the 270 | 271 | name is not already defined, a warning is emitted. 272 | 273 | """ 274 | flakes(''' 275 | class foo: 276 | foo 277 | ''', m.UndefinedName) 278 | 279 | 280 | def test_classNameDefinedPreviously(): 281 | """If a class name is used in the body of that class's definition and the 282 | name was previously defined in some other way, no warning is emitted. 283 | 284 | """ 285 | flakes(''' 286 | foo = None 287 | class foo: 288 | foo 289 | ''') 290 | 291 | 292 | def test_classRedefinition(): 293 | """If a class is defined twice in the same module, a warning is emitted.""" 294 | flakes(''' 295 | class Foo: 296 | pass 297 | class Foo: 298 | pass 299 | ''', m.RedefinedWhileUnused) 300 | 301 | 302 | def test_functionRedefinedAsClass(): 303 | """If a function is redefined as a class, a warning is emitted.""" 304 | flakes(''' 305 | def Foo(): 306 | pass 307 | class Foo: 308 | pass 309 | ''', m.RedefinedWhileUnused) 310 | 311 | 312 | def test_classRedefinedAsFunction(): 313 | """If a class is redefined as a function, a warning is emitted.""" 314 | flakes(''' 315 | class Foo: 316 | pass 317 | def Foo(): 318 | pass 319 | ''', m.RedefinedWhileUnused) 320 | 321 | 322 | @pytest.mark.skipif("'todo: Too hard to make this warn but other cases stay silent'") 323 | def test_doubleAssignment(): 324 | """If a variable is re-assigned to without being used, no warning is emitted.""" 325 | flakes(''' 326 | x = 10 327 | x = 20 328 | ''', m.RedefinedWhileUnused) 329 | 330 | 331 | def test_doubleAssignmentConditionally(): 332 | """If a variable is re-assigned within a conditional, no warning is emitted.""" 333 | flakes(''' 334 | x = 10 335 | if True: 336 | x = 20 337 | ''') 338 | 339 | 340 | def test_doubleAssignmentWithUse(): 341 | """If a variable is re-assigned to after being used, no warning is emitted.""" 342 | flakes(''' 343 | x = 10 344 | y = x * 2 345 | x = 20 346 | ''') 347 | 348 | 349 | def test_comparison(): 350 | """If a defined name is used on either side of any of the six comparison operators, no warning is emitted.""" 351 | flakes(''' 352 | x = 10 353 | y = 20 354 | x < y 355 | x <= y 356 | x == y 357 | x != y 358 | x >= y 359 | x > y 360 | ''') 361 | 362 | 363 | def test_identity(): 364 | """If a defined name is used on either side of an identity test, no warning is emitted.""" 365 | flakes(''' 366 | x = 10 367 | y = 20 368 | x is y 369 | x is not y 370 | ''') 371 | 372 | 373 | def test_containment(): 374 | """If a defined name is used on either side of a containment test, no warning is emitted.""" 375 | flakes(''' 376 | x = 10 377 | y = 20 378 | x in y 379 | x not in y 380 | ''') 381 | 382 | 383 | def test_loopControl(): 384 | """break and continue statements are supported.""" 385 | flakes(''' 386 | for x in [1, 2]: 387 | break 388 | ''') 389 | flakes(''' 390 | for x in [1, 2]: 391 | continue 392 | ''') 393 | 394 | 395 | def test_ellipsis(): 396 | """Ellipsis in a slice is supported.""" 397 | flakes(''' 398 | [1, 2][...] 399 | ''') 400 | 401 | 402 | def test_extendedSlice(): 403 | """Extended slices are supported.""" 404 | flakes(''' 405 | x = 3 406 | [1, 2][x,:] 407 | ''') 408 | 409 | 410 | def test_varAugmentedAssignment(): 411 | """Augmented assignment of a variable is supported. 412 | 413 | We don't care about var refs. 414 | 415 | """ 416 | flakes(''' 417 | foo = 0 418 | foo += 1 419 | ''') 420 | 421 | 422 | def test_attrAugmentedAssignment(): 423 | """Augmented assignment of attributes is supported. 424 | 425 | We don't care about attr refs. 426 | 427 | """ 428 | flakes(''' 429 | foo = None 430 | foo.bar += foo.baz 431 | ''') 432 | 433 | 434 | def test_unusedVariable(): 435 | """Warn when a variable in a function is assigned a value that's never used.""" 436 | flakes(''' 437 | def a(): 438 | b = 1 439 | ''', m.UnusedVariable) 440 | 441 | 442 | def test_unusedVariableAsLocals(): 443 | """Using locals() it is perfectly valid to have unused variables.""" 444 | flakes(''' 445 | def a(): 446 | b = 1 447 | return locals() 448 | ''') 449 | 450 | 451 | def test_unusedVariableNoLocals(): 452 | """Using locals() in wrong scope should not matter.""" 453 | flakes(''' 454 | def a(): 455 | locals() 456 | def a(): 457 | b = 1 458 | return 459 | ''', m.UnusedVariable) 460 | 461 | 462 | def test_assignToGlobal(): 463 | """Assigning to a global and then not using that global is perfectly 464 | acceptable. 465 | 466 | Do not mistake it for an unused local variable. 467 | 468 | """ 469 | flakes(''' 470 | b = 0 471 | def a(): 472 | global b 473 | b = 1 474 | ''') 475 | 476 | 477 | @pytest.mark.skipif('''version_info < (3,)''') 478 | def test_assignToNonlocal(): 479 | """Assigning to a nonlocal and then not using that binding is perfectly 480 | acceptable. 481 | 482 | Do not mistake it for an unused local variable. 483 | 484 | """ 485 | flakes(''' 486 | b = b'0' 487 | def a(): 488 | nonlocal b 489 | b = b'1' 490 | ''') 491 | 492 | 493 | def test_assignToMember(): 494 | """Assigning to a member of another object and then not using that member 495 | variable is perfectly acceptable. 496 | 497 | Do not mistake it for an unused local variable. 498 | 499 | """ 500 | # XXX: Adding this test didn't generate a failure. Maybe not 501 | # necessary? 502 | flakes(''' 503 | class b: 504 | pass 505 | def a(): 506 | b.foo = 1 507 | ''') 508 | 509 | 510 | def test_assignInForLoop(): 511 | """Don't warn when a variable in a for loop is assigned to but not used.""" 512 | flakes(''' 513 | def f(): 514 | for i in range(10): 515 | pass 516 | ''') 517 | 518 | 519 | def test_assignInListComprehension(): 520 | """Don't warn when a variable in a list comprehension is assigned to but not used.""" 521 | flakes(''' 522 | def f(): 523 | [None for i in range(10)] 524 | ''') 525 | 526 | 527 | def test_generatorExpression(): 528 | """Don't warn when a variable in a generator expression is assigned to but not used.""" 529 | flakes(''' 530 | def f(): 531 | (None for i in range(10)) 532 | ''') 533 | 534 | 535 | def test_assignmentInsideLoop(): 536 | """Don't warn when a variable assignment occurs lexically after its use.""" 537 | flakes(''' 538 | def f(): 539 | x = None 540 | for i in range(10): 541 | if i > 2: 542 | return x 543 | x = i * 2 544 | ''') 545 | 546 | 547 | def test_tupleUnpacking(): 548 | """Don't warn when a variable included in tuple unpacking is unused. 549 | 550 | It's very common for variables in a tuple unpacking assignment to be unused in good Python code, so warning will 551 | only create false positives. 552 | 553 | """ 554 | flakes(''' 555 | def f(): 556 | (x, y) = 1, 2 557 | ''') 558 | 559 | 560 | def test_listUnpacking(): 561 | """Don't warn when a variable included in list unpacking is unused.""" 562 | flakes(''' 563 | def f(): 564 | [x, y] = [1, 2] 565 | ''') 566 | 567 | 568 | def test_closedOver(): 569 | """Don't warn when the assignment is used in an inner function.""" 570 | flakes(''' 571 | def barMaker(): 572 | foo = 5 573 | def bar(): 574 | return foo 575 | return bar 576 | ''') 577 | 578 | 579 | def test_doubleClosedOver(): 580 | """Don't warn when the assignment is used in an inner function, even if 581 | that inner function itself is in an inner function.""" 582 | flakes(''' 583 | def barMaker(): 584 | foo = 5 585 | def bar(): 586 | def baz(): 587 | return foo 588 | return bar 589 | ''') 590 | 591 | 592 | def test_tracebackhideSpecialVariable(): 593 | """Do not warn about unused local variable __tracebackhide__, which is a 594 | special variable for py.test.""" 595 | flakes(""" 596 | def helper(): 597 | __tracebackhide__ = True 598 | """) 599 | 600 | 601 | def test_ifexp(): 602 | """Test C{foo if bar else baz} statements.""" 603 | flakes("a = 'moo' if True else 'oink'") 604 | flakes("a = foo if True else 'oink'", m.UndefinedName) 605 | flakes("a = 'moo' if True else bar", m.UndefinedName) 606 | 607 | 608 | def test_withStatementNoNames(): 609 | """No warnings are emitted for using inside or after a nameless statement a name defined beforehand.""" 610 | flakes(''' 611 | from __future__ import with_statement 612 | bar = None 613 | with open("foo"): 614 | bar 615 | bar 616 | ''') 617 | 618 | 619 | def test_withStatementSingleName(): 620 | """No warnings are emitted for using a name defined by a statement within the suite or afterwards.""" 621 | flakes(''' 622 | from __future__ import with_statement 623 | with open('foo') as bar: 624 | bar 625 | bar 626 | ''') 627 | 628 | 629 | def test_withStatementAttributeName(): 630 | """No warnings are emitted for using an attribute as the target of a statement.""" 631 | flakes(''' 632 | from __future__ import with_statement 633 | import foo 634 | with open('foo') as foo.bar: 635 | pass 636 | ''') 637 | 638 | 639 | def test_withStatementSubscript(): 640 | """No warnings are emitted for using a subscript as the target of a statement.""" 641 | flakes(''' 642 | from __future__ import with_statement 643 | import foo 644 | with open('foo') as foo[0]: 645 | pass 646 | ''') 647 | 648 | 649 | def test_withStatementSubscriptUndefined(): 650 | """An undefined name warning is emitted if the subscript used as the target of a with statement is not defined.""" 651 | flakes(''' 652 | from __future__ import with_statement 653 | import foo 654 | with open('foo') as foo[bar]: 655 | pass 656 | ''', m.UndefinedName) 657 | 658 | 659 | def test_withStatementTupleNames(): 660 | """No warnings are emitted for using any of the tuple of names defined 661 | 662 | by a statement within the suite or afterwards. 663 | 664 | """ 665 | flakes(''' 666 | from __future__ import with_statement 667 | with open('foo') as (bar, baz): 668 | bar, baz 669 | bar, baz 670 | ''') 671 | 672 | 673 | def test_withStatementListNames(): 674 | """No warnings are emitted for using any of the list of names defined by 675 | 676 | a statement within the suite or afterwards. 677 | 678 | """ 679 | flakes(''' 680 | from __future__ import with_statement 681 | with open('foo') as [bar, baz]: 682 | bar, baz 683 | bar, baz 684 | ''') 685 | 686 | 687 | def test_withStatementComplicatedTarget(): 688 | """ If the target of a statement uses any or all of the valid forms 689 | for that part of the grammar 690 | (See: http://docs.python.org/reference/compound_stmts.html#the-with-statement), 691 | the names involved are checked both for definedness and any bindings 692 | created are respected in the suite of the statement and afterwards. 693 | 694 | """ 695 | flakes(''' 696 | from __future__ import with_statement 697 | c = d = e = g = h = i = None 698 | with open('foo') as [(a, b), c[d], e.f, g[h:i]]: 699 | a, b, c, d, e, g, h, i 700 | a, b, c, d, e, g, h, i 701 | ''') 702 | 703 | 704 | def test_withStatementSingleNameUndefined(): 705 | """An undefined name warning is emitted if the name first defined by a statement is used before the statement.""" 706 | flakes(''' 707 | from __future__ import with_statement 708 | bar 709 | with open('foo') as bar: 710 | pass 711 | ''', m.UndefinedName) 712 | 713 | 714 | def test_withStatementTupleNamesUndefined(): 715 | """ An undefined name warning is emitted if a name first defined by a the 716 | tuple-unpacking form of the statement is used before the statement. 717 | 718 | """ 719 | flakes(''' 720 | from __future__ import with_statement 721 | baz 722 | with open('foo') as (bar, baz): 723 | pass 724 | ''', m.UndefinedName) 725 | 726 | 727 | def test_withStatementSingleNameRedefined(): 728 | """A redefined name warning is emitted if a name bound by an import is 729 | rebound by the name defined by a statement. 730 | 731 | """ 732 | flakes(''' 733 | from __future__ import with_statement 734 | import bar 735 | with open('foo') as bar: 736 | pass 737 | ''', m.RedefinedWhileUnused) 738 | 739 | 740 | def test_withStatementTupleNamesRedefined(): 741 | """ A redefined name warning is emitted if a name bound by an import is 742 | rebound by one of the names defined by the tuple-unpacking form of a 743 | statement. 744 | 745 | """ 746 | flakes(''' 747 | from __future__ import with_statement 748 | import bar 749 | with open('foo') as (bar, baz): 750 | pass 751 | ''', m.RedefinedWhileUnused) 752 | 753 | 754 | def test_withStatementUndefinedInside(): 755 | """An undefined name warning is emitted if a name is used inside the body 756 | of a statement without first being bound. 757 | 758 | """ 759 | flakes(''' 760 | from __future__ import with_statement 761 | with open('foo') as bar: 762 | baz 763 | ''', m.UndefinedName) 764 | 765 | 766 | def test_withStatementNameDefinedInBody(): 767 | """A name defined in the body of a statement can be used after the body ends without warning.""" 768 | flakes(''' 769 | from __future__ import with_statement 770 | with open('foo') as bar: 771 | baz = 10 772 | baz 773 | ''') 774 | 775 | 776 | def test_withStatementUndefinedInExpression(): 777 | """An undefined name warning is emitted if a name in the I{test} expression of a statement is undefined.""" 778 | flakes(''' 779 | from __future__ import with_statement 780 | with bar as baz: 781 | pass 782 | ''', m.UndefinedName) 783 | 784 | flakes(''' 785 | from __future__ import with_statement 786 | with bar as bar: 787 | pass 788 | ''', m.UndefinedName) 789 | 790 | 791 | @pytest.mark.skipif('''version_info < (2, 7)''') 792 | def test_dictComprehension(): 793 | """Dict comprehensions are properly handled.""" 794 | flakes(''' 795 | a = {1: x for x in range(10)} 796 | ''') 797 | 798 | 799 | @pytest.mark.skipif('''version_info < (2, 7)''') 800 | def test_setComprehensionAndLiteral(): 801 | """Set comprehensions are properly handled.""" 802 | flakes(''' 803 | a = {1, 2, 3} 804 | b = {x for x in range(10)} 805 | ''') 806 | 807 | 808 | def test_exceptionUsedInExcept(): 809 | as_exc = ', ' if version_info < (2, 6) else ' as ' 810 | flakes(''' 811 | try: pass 812 | except Exception%se: e 813 | ''' % as_exc) 814 | 815 | flakes(''' 816 | def download_review(): 817 | try: pass 818 | except Exception%se: e 819 | ''' % as_exc) 820 | 821 | 822 | def test_exceptWithoutNameInFunction(): 823 | """Don't issue false warning when an unnamed exception is used. 824 | 825 | Previously, there would be a false warning, but only when the try..except was in a function 826 | 827 | """ 828 | flakes(''' 829 | import tokenize 830 | def foo(): 831 | try: pass 832 | except tokenize.TokenError: pass 833 | ''') 834 | 835 | 836 | def test_exceptWithoutNameInFunctionTuple(): 837 | """Don't issue false warning when an unnamed exception is used. 838 | 839 | This example catches a tuple of exception types. 840 | 841 | """ 842 | flakes(''' 843 | import tokenize 844 | def foo(): 845 | try: pass 846 | except (tokenize.TokenError, IndentationError): pass 847 | ''') 848 | 849 | 850 | def test_augmentedAssignmentImportedFunctionCall(): 851 | """Consider a function that is called on the right part of an augassign operation to be used.""" 852 | flakes(''' 853 | from foo import bar 854 | baz = 0 855 | baz += bar() 856 | ''') 857 | 858 | 859 | @pytest.mark.skipif('''version_info < (3, 3)''') 860 | def test_yieldFromUndefined(): 861 | """Test yield from statement.""" 862 | flakes(''' 863 | def bar(): 864 | yield from foo() 865 | ''', m.UndefinedName) 866 | 867 | 868 | def test_bareExcept(): 869 | """ 870 | Issue a warning when using bare except:. 871 | """ 872 | flakes(''' 873 | try: 874 | pass 875 | except: 876 | pass 877 | ''', m.BareExcept) 878 | 879 | 880 | def test_access_debug(): 881 | """Test accessing __debug__ returns no errors""" 882 | flakes(''' 883 | if __debug__: 884 | print("success!") 885 | print(__debug__) 886 | ''') 887 | -------------------------------------------------------------------------------- /frosted/test/test_plugins.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function, unicode_literals 2 | 3 | import textwrap 4 | import pkg_resources 5 | from mock import MagicMock, patch 6 | 7 | from pies.overrides import * 8 | 9 | from frosted import checker 10 | 11 | PyCF_ONLY_AST = 1024 12 | 13 | @patch.object(checker.Checker, 'report') 14 | def test_plugins(m_report): 15 | """ Plugins should be invoked by their "check" method 16 | """ 17 | tree = compile(textwrap.dedent(""), "", "exec", PyCF_ONLY_AST) 18 | m_report.return_value = None 19 | m_check = MagicMock(name="check", return_value=[(MagicMock(), None, (), {})]) 20 | m_checker = MagicMock(name="checker", check=m_check) 21 | m_load = MagicMock(name="load", return_value=m_checker) 22 | m_plugin = MagicMock(name="plugin", load=m_load) 23 | 24 | with patch.object(pkg_resources, 'iter_entry_points', return_value=[m_plugin]) as mock_ep: 25 | checker.Checker(tree, "") 26 | 27 | assert m_check.assert_called() 28 | assert m_report.assert_called() 29 | -------------------------------------------------------------------------------- /frosted/test/test_return_with_arguments_inside_generator.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function, unicode_literals 2 | 3 | from sys import version_info 4 | 5 | import pytest 6 | from pies.overrides import * 7 | 8 | from frosted import messages as m 9 | 10 | from .utils import flakes 11 | 12 | 13 | @pytest.mark.skipif("version_info >= (3,)") 14 | def test_return(): 15 | flakes(''' 16 | class a: 17 | def b(): 18 | for x in a.c: 19 | if x: 20 | yield x 21 | return a 22 | ''', m.ReturnWithArgsInsideGenerator) 23 | 24 | 25 | @pytest.mark.skipif("version_info >= (3,)") 26 | def test_returnNone(): 27 | flakes(''' 28 | def a(): 29 | yield 12 30 | return None 31 | ''', m.ReturnWithArgsInsideGenerator) 32 | 33 | 34 | @pytest.mark.skipif("version_info >= (3,)") 35 | def test_returnYieldExpression(): 36 | flakes(''' 37 | def a(): 38 | b = yield a 39 | return b 40 | ''', m.ReturnWithArgsInsideGenerator) 41 | 42 | 43 | @pytest.mark.skipif("version_info >= (3,)") 44 | def test_return_with_args_inside_generator_not_duplicated(): 45 | # doubly nested - should still only complain once 46 | flakes(''' 47 | def f0(): 48 | def f1(): 49 | yield None 50 | return None 51 | ''', m.ReturnWithArgsInsideGenerator) 52 | 53 | # triple nested - should still only complain once 54 | flakes(''' 55 | def f0(): 56 | def f1(): 57 | def f2(): 58 | yield None 59 | return None 60 | ''', m.ReturnWithArgsInsideGenerator) 61 | 62 | 63 | @pytest.mark.skipif("version_info >= (3,)") 64 | def test_no_false_positives_for_return_inside_generator(): 65 | # doubly nested - should still only complain once 66 | flakes(''' 67 | def f(): 68 | def g(): 69 | yield None 70 | return g 71 | ''') 72 | -------------------------------------------------------------------------------- /frosted/test/test_script.py: -------------------------------------------------------------------------------- 1 | """frosted/test/test_script.py. 2 | 3 | Tests functionality (integration testing) that require starting a full frosted instance against input files 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 6 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 7 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 8 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all copies or 11 | substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 14 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 15 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 16 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 17 | OTHER DEALINGS IN THE SOFTWARE. 18 | 19 | """ 20 | 21 | from __future__ import absolute_import, division, print_function 22 | 23 | import os 24 | import shutil 25 | import subprocess 26 | import sys 27 | import tempfile 28 | 29 | import pytest 30 | from pies.overrides import * 31 | 32 | import frosted 33 | from frosted.api import iter_source_code 34 | from frosted.messages import UnusedImport 35 | 36 | from .utils import Node 37 | 38 | FROSTED_BINARY = os.path.join(os.path.dirname(frosted.__file__), 'main.py') 39 | 40 | 41 | def setup_function(function): 42 | globals()['TEMP_DIR'] = tempfile.mkdtemp() 43 | globals()['TEMP_FILE_PATH'] = os.path.join(TEMP_DIR, 'temp') 44 | 45 | 46 | def teardown_function(function): 47 | shutil.rmtree(TEMP_DIR) 48 | 49 | 50 | def make_empty_file(*parts): 51 | assert parts 52 | fpath = os.path.join(TEMP_DIR, *parts) 53 | fd = open(fpath, 'a') 54 | fd.close() 55 | return fpath 56 | 57 | 58 | def test_emptyDirectory(): 59 | """There are no Python files in an empty directory.""" 60 | assert list(iter_source_code([TEMP_DIR])) == [] 61 | 62 | 63 | def test_singleFile(): 64 | """If the directory contains one Python file - iter_source_code will find it""" 65 | childpath = make_empty_file('foo.py') 66 | assert list(iter_source_code([TEMP_DIR])) == [childpath] 67 | 68 | 69 | def test_onlyPythonSource(): 70 | """Files that are not Python source files are not included.""" 71 | make_empty_file('foo.pyc') 72 | assert list(iter_source_code([TEMP_DIR])) == [] 73 | 74 | 75 | def test_recurses(): 76 | """If the Python files are hidden deep down in child directories, we will find them.""" 77 | os.mkdir(os.path.join(TEMP_DIR, 'foo')) 78 | apath = make_empty_file('foo', 'a.py') 79 | os.mkdir(os.path.join(TEMP_DIR, 'bar')) 80 | bpath = make_empty_file('bar', 'b.py') 81 | cpath = make_empty_file('c.py') 82 | assert sorted(iter_source_code([TEMP_DIR])) == sorted([apath, bpath, cpath]) 83 | 84 | 85 | def test_multipleDirectories(): 86 | """iter_source_code can be given multiple directories - it will recurse into each of them""" 87 | foopath = os.path.join(TEMP_DIR, 'foo') 88 | barpath = os.path.join(TEMP_DIR, 'bar') 89 | os.mkdir(foopath) 90 | apath = make_empty_file('foo', 'a.py') 91 | os.mkdir(barpath) 92 | bpath = make_empty_file('bar', 'b.py') 93 | assert sorted(iter_source_code([foopath, barpath])) == sorted([apath, bpath]) 94 | 95 | 96 | def test_explicitFiles(): 97 | """If one of the paths given to iter_source_code is not a directory but a 98 | file, it will include that in its output.""" 99 | epath = make_empty_file('e.py') 100 | assert list(iter_source_code([epath])) == [epath] 101 | 102 | 103 | def run_frosted(paths, stdin=None): 104 | """Launch a subprocess running frosted.""" 105 | env = native_dict(os.environ) 106 | env['PYTHONPATH'] = os.pathsep.join(sys.path) 107 | command = [sys.executable, FROSTED_BINARY] 108 | command.extend(paths) 109 | if stdin: 110 | p = subprocess.Popen(command, env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 111 | (stdout, stderr) = p.communicate(stdin) 112 | else: 113 | p = subprocess.Popen(command, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 114 | (stdout, stderr) = p.communicate() 115 | rv = p.wait() 116 | if PY3: 117 | stdout = stdout.decode('utf-8') 118 | stderr = stderr.decode('utf-8') 119 | 120 | return (stdout, stderr, rv) 121 | 122 | 123 | def test_goodFile(): 124 | """When a Python source file is all good, the return code is zero and no 125 | 126 | messages are printed to either stdout or stderr. 127 | 128 | """ 129 | fd = open(TEMP_FILE_PATH, 'a') 130 | fd.close() 131 | d = run_frosted([TEMP_FILE_PATH]) 132 | assert d == ('', '', 0) 133 | 134 | 135 | def test_fileWithFlakes(): 136 | """ When a Python source file has warnings, 137 | 138 | the return code is non-zero and the warnings are printed to stdout 139 | 140 | """ 141 | fd = open(TEMP_FILE_PATH, 'wb') 142 | fd.write("import contraband\n".encode('ascii')) 143 | fd.close() 144 | d = run_frosted([TEMP_FILE_PATH]) 145 | expected = UnusedImport(TEMP_FILE_PATH, Node(1), 'contraband') 146 | assert d[0].strip() == expected.message.strip() 147 | 148 | 149 | @pytest.mark.skipif("sys.version_info >= (3,)") 150 | def test_non_unicode_slash_u(): 151 | """ Ensure \ u doesn't cause a unicode decode error """ 152 | fd = open(TEMP_FILE_PATH, 'wb') 153 | fd.write('"""Example: C:\\foobar\\unit-tests\\test.py"""'.encode('ascii')) 154 | fd.close() 155 | d = run_frosted([TEMP_FILE_PATH]) 156 | assert d == ('', '', 0) 157 | 158 | 159 | def test_errors(): 160 | """ When frosted finds errors with the files it's given, (if they don't exist, say), 161 | 162 | then the return code is non-zero and the errors are printed to stderr 163 | 164 | """ 165 | d = run_frosted([TEMP_FILE_PATH, '-r']) 166 | error_msg = '%s: No such file or directory\n' % (TEMP_FILE_PATH,) 167 | assert d == ('', error_msg, 1) 168 | 169 | 170 | def test_readFromStdin(): 171 | """If no arguments are passed to C{frosted} then it reads from stdin.""" 172 | d = run_frosted(['-'], stdin='import contraband'.encode('ascii')) 173 | 174 | expected = UnusedImport('', Node(1), 'contraband') 175 | assert d[0].strip() == expected.message.strip() 176 | 177 | 178 | @pytest.mark.skipif("sys.version_info >= (3,)") 179 | def test_print_statement_python2(): 180 | d = run_frosted(['-'], stdin='print "Hello, Frosted"'.encode('ascii')) 181 | assert d == ('', '', 0) 182 | -------------------------------------------------------------------------------- /frosted/test/test_undefined_names.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function, unicode_literals 2 | 3 | from sys import version_info 4 | 5 | import pytest 6 | from pies.overrides import * 7 | 8 | from _ast import PyCF_ONLY_AST 9 | from frosted import messages as m 10 | from frosted import checker 11 | 12 | from.utils import flakes 13 | 14 | 15 | def test_undefined(): 16 | flakes('bar', m.UndefinedName) 17 | 18 | 19 | def test_definedInListComp(): 20 | flakes('[a for a in range(10) if a]') 21 | 22 | 23 | def test_functionsNeedGlobalScope(): 24 | flakes(''' 25 | class a: 26 | def b(): 27 | fu 28 | fu = 1 29 | ''') 30 | 31 | 32 | def test_builtins(): 33 | flakes('range(10)') 34 | 35 | 36 | def test_builtinWindowsError(): 37 | """WindowsError is sometimes a builtin name, so no warning is emitted for using it.""" 38 | flakes('WindowsError') 39 | 40 | 41 | def test_magicGlobalsFile(): 42 | """Use of the __file magic global should not emit an undefined name 43 | warning.""" 44 | flakes('__file__') 45 | 46 | 47 | def test_magicGlobalsBuiltins(): 48 | """Use of the __builtins magic global should not emit an undefined name warning.""" 49 | flakes('__builtins__') 50 | 51 | 52 | def test_magicGlobalImport(): 53 | """Use of the __import__ magic global should not emit an undefined name warning.""" 54 | flakes('__import__') 55 | 56 | def test_magicGlobalsName(): 57 | """Use of the __name magic global should not emit an undefined name warning.""" 58 | flakes('__name__') 59 | 60 | 61 | def test_magicGlobalsPath(): 62 | """Use of the __path magic global should not emit an undefined name warning, 63 | 64 | if you refer to it from a file called __init__.py. 65 | 66 | """ 67 | flakes('__path__', m.UndefinedName) 68 | flakes('__path__', filename='package/__init__.py') 69 | 70 | 71 | def test_globalImportStar(): 72 | """Can't find undefined names with import *.""" 73 | flakes('from fu import *; bar', m.ImportStarUsed) 74 | 75 | 76 | def test_localImportStar(): 77 | """A local import * still allows undefined names to be found in upper scopes.""" 78 | flakes(''' 79 | def a(): 80 | from fu import * 81 | bar 82 | ''', m.ImportStarUsed, m.UndefinedName) 83 | 84 | 85 | @pytest.mark.skipif("version_info >= (3,)") 86 | def test_unpackedParameter(): 87 | """Unpacked function parameters create bindings.""" 88 | flakes(''' 89 | def a((bar, baz)): 90 | bar; baz 91 | ''') 92 | 93 | 94 | @pytest.mark.skipif("'todo'") 95 | def test_definedByGlobal(): 96 | """"global" can make an otherwise undefined name in another function defined.""" 97 | flakes(''' 98 | def a(): global fu; fu = 1 99 | def b(): fu 100 | ''') 101 | 102 | 103 | def test_globalInGlobalScope(): 104 | """A global statement in the global scope is ignored.""" 105 | flakes(''' 106 | global x 107 | def foo(): 108 | print(x) 109 | ''', m.UndefinedName) 110 | 111 | 112 | def test_del(): 113 | """Del deletes bindings.""" 114 | flakes('a = 1; del a; a', m.UndefinedName) 115 | 116 | 117 | def test_delGlobal(): 118 | """Del a global binding from a function.""" 119 | flakes(''' 120 | a = 1 121 | def f(): 122 | global a 123 | del a 124 | a 125 | ''') 126 | 127 | 128 | def test_delUndefined(): 129 | """Del an undefined name.""" 130 | flakes('del a', m.UndefinedName) 131 | 132 | 133 | def test_globalFromNestedScope(): 134 | """Global names are available from nested scopes.""" 135 | flakes(''' 136 | a = 1 137 | def b(): 138 | def c(): 139 | a 140 | ''') 141 | 142 | 143 | def test_laterRedefinedGlobalFromNestedScope(): 144 | """Test that referencing a local name that shadows a global, before it is 145 | defined, generates a warning.""" 146 | flakes(''' 147 | a = 1 148 | def fun(): 149 | a 150 | a = 2 151 | return a 152 | ''', m.UndefinedLocal) 153 | 154 | 155 | def test_laterRedefinedGlobalFromNestedScope2(): 156 | """Test that referencing a local name in a nested scope that shadows a 157 | global declared in an enclosing scope, before it is defined, generates a 158 | warning.""" 159 | flakes(''' 160 | a = 1 161 | def fun(): 162 | global a 163 | def fun2(): 164 | a 165 | a = 2 166 | return a 167 | ''', m.UndefinedLocal) 168 | 169 | 170 | def test_intermediateClassScopeIgnored(): 171 | """If a name defined in an enclosing scope is shadowed by a local variable 172 | and the name is used locally before it is bound, an unbound local warning 173 | is emitted, even if there is a class scope between the enclosing scope and 174 | the local scope.""" 175 | flakes(''' 176 | def f(): 177 | x = 1 178 | class g: 179 | def h(): 180 | a = x 181 | x = None 182 | print(x, a) 183 | print(x) 184 | ''', m.UndefinedLocal) 185 | 186 | 187 | def test_doubleNestingReportsClosestName(): 188 | """Test that referencing a local name in a nested scope that shadows a 189 | variable declared in two different outer scopes before it is defined in the 190 | innermost scope generates an UnboundLocal warning which refers to the 191 | nearest shadowed name.""" 192 | exc = flakes(''' 193 | def a(): 194 | x = 1 195 | def b(): 196 | x = 2 # line 5 197 | def c(): 198 | x 199 | x = 3 200 | return x 201 | return x 202 | return x 203 | ''', m.UndefinedLocal).messages[0] 204 | assert 'x' in exc.message 205 | assert str(5) in exc.message 206 | 207 | 208 | def test_laterRedefinedGlobalFromNestedScope3(): 209 | """Test that referencing a local name in a nested scope that shadows a 210 | global, before it is defined, generates a warning.""" 211 | flakes(''' 212 | def fun(): 213 | a = 1 214 | def fun2(): 215 | a 216 | a = 1 217 | return a 218 | return a 219 | ''', m.UndefinedLocal) 220 | 221 | 222 | def test_undefinedAugmentedAssignment(): 223 | flakes( 224 | ''' 225 | def f(seq): 226 | a = 0 227 | seq[a] += 1 228 | seq[b] /= 2 229 | c[0] *= 2 230 | a -= 3 231 | d += 4 232 | e[any] = 5 233 | ''', 234 | m.UndefinedName, # b 235 | m.UndefinedName, # c 236 | m.UndefinedName, m.UnusedVariable, # d 237 | m.UndefinedName, # e 238 | ) 239 | 240 | 241 | def test_nestedClass(): 242 | """Nested classes can access enclosing scope.""" 243 | flakes(''' 244 | def f(foo): 245 | class C: 246 | bar = foo 247 | def f(): 248 | return foo 249 | return C() 250 | 251 | f(123).f() 252 | ''') 253 | 254 | 255 | def test_badNestedClass(): 256 | """Free variables in nested classes must bind at class creation.""" 257 | flakes(''' 258 | def f(): 259 | class C: 260 | bar = foo 261 | foo = 456 262 | return foo 263 | f() 264 | ''', m.UndefinedName) 265 | 266 | 267 | def test_definedAsStarArgs(): 268 | """Star and double-star arg names are defined.""" 269 | flakes(''' 270 | def f(a, *b, **c): 271 | print(a, b, c) 272 | ''') 273 | 274 | 275 | @pytest.mark.skipif("version_info < (3,)") 276 | def test_definedAsStarUnpack(): 277 | """Star names in unpack are defined.""" 278 | flakes(''' 279 | a, *b = range(10) 280 | print(a, b) 281 | ''') 282 | flakes(''' 283 | *a, b = range(10) 284 | print(a, b) 285 | ''') 286 | flakes(''' 287 | a, *b, c = range(10) 288 | print(a, b, c) 289 | ''') 290 | 291 | 292 | @pytest.mark.skipif("version_info < (3,)") 293 | def test_keywordOnlyArgs(): 294 | """Keyword-only arg names are defined.""" 295 | flakes(''' 296 | def f(*, a, b=None): 297 | print(a, b) 298 | ''') 299 | 300 | flakes(''' 301 | import default_b 302 | def f(*, a, b=default_b): 303 | print(a, b) 304 | ''') 305 | 306 | 307 | @pytest.mark.skipif("version_info < (3,)") 308 | def test_keywordOnlyArgsUndefined(): 309 | """Typo in kwonly name.""" 310 | flakes(''' 311 | def f(*, a, b=default_c): 312 | print(a, b) 313 | ''', m.UndefinedName) 314 | 315 | 316 | @pytest.mark.skipif("version_info < (3,)") 317 | def test_annotationUndefined(): 318 | """Undefined annotations.""" 319 | flakes(''' 320 | from abc import note1, note2, note3, note4, note5 321 | def func(a: note1, *args: note2, 322 | b: note3=12, **kw: note4) -> note5: pass 323 | ''') 324 | 325 | flakes(''' 326 | def func(): 327 | d = e = 42 328 | def func(a: {1, d}) -> (lambda c: e): pass 329 | ''') 330 | 331 | 332 | @pytest.mark.skipif("version_info < (3,)") 333 | def test_metaClassUndefined(): 334 | flakes(''' 335 | from abc import ABCMeta 336 | class A(metaclass=ABCMeta): pass 337 | ''') 338 | 339 | 340 | def test_definedInGenExp(): 341 | """Using the loop variable of a generator expression results in no 342 | warnings.""" 343 | flakes('(a for a in %srange(10) if a)' % 344 | ('x' if version_info < (3,) else '')) 345 | 346 | 347 | def test_undefinedWithErrorHandler(): 348 | """Some compatibility code checks explicitly for NameError. 349 | 350 | It should not trigger warnings. 351 | 352 | """ 353 | flakes(''' 354 | try: 355 | socket_map 356 | except NameError: 357 | socket_map = {} 358 | ''') 359 | flakes(''' 360 | try: 361 | _memoryview.contiguous 362 | except (NameError, AttributeError): 363 | raise RuntimeError("Python >= 3.3 is required") 364 | ''') 365 | # If NameError is not explicitly handled, generate a warning 366 | flakes(''' 367 | try: 368 | socket_map 369 | except Exception: 370 | socket_map = {} 371 | ''', m.UndefinedName) 372 | flakes(''' 373 | try: 374 | socket_map 375 | except Exception: 376 | socket_map = {} 377 | ''', m.UndefinedName) 378 | 379 | 380 | def test_definedInClass(): 381 | """Defined name for generator expressions and dict/set comprehension.""" 382 | flakes(''' 383 | class A: 384 | T = range(10) 385 | 386 | Z = (x for x in T) 387 | L = [x for x in T] 388 | B = dict((i, str(i)) for i in T) 389 | ''') 390 | 391 | if version_info >= (2, 7): 392 | flakes(''' 393 | class A: 394 | T = range(10) 395 | 396 | X = {x for x in T} 397 | Y = {x:x for x in T} 398 | ''') 399 | 400 | 401 | def test_impossibleContext(): 402 | """A Name node with an unrecognized context results in a RuntimeError being raised.""" 403 | tree = compile("x = 10", "", "exec", PyCF_ONLY_AST) 404 | # Make it into something unrecognizable. 405 | tree.body[0].targets[0].ctx = object() 406 | with pytest.raises(RuntimeError): 407 | checker.Checker(tree) 408 | -------------------------------------------------------------------------------- /frosted/test/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, division, print_function, unicode_literals 2 | 3 | import textwrap 4 | from collections import namedtuple 5 | 6 | from pies.overrides import * 7 | 8 | from frosted import checker 9 | 10 | PyCF_ONLY_AST = 1024 11 | __all__ = ['flakes', 'Node', 'LoggingReporter'] 12 | 13 | 14 | def flakes(input, *expectedOutputs, **kw): 15 | tree = compile(textwrap.dedent(input), "", "exec", PyCF_ONLY_AST) 16 | results = checker.Checker(tree, **kw) 17 | outputs = [message.type for message in results.messages] 18 | expectedOutputs = list(expectedOutputs) 19 | outputs.sort(key=lambda t: t.name) 20 | expectedOutputs.sort(key=lambda t: t.name) 21 | assert outputs == expectedOutputs, ('\n' 22 | 'for input:\n' 23 | '%s\n' 24 | 'expected outputs:\n' 25 | '%r\n' 26 | 'but got:\n' 27 | '%s') % (input, expectedOutputs, 28 | '\n'.join([str(o) for o in results.messages])) 29 | return results 30 | 31 | 32 | class Node(namedtuple('Node', ['lineno', 'col_offset'])): 33 | """A mock AST Node.""" 34 | 35 | def __new__(cls, lineno, col_offset=0): 36 | return super(Node, cls).__new__(cls, lineno, col_offset) 37 | 38 | 39 | class LoggingReporter(namedtuple('LoggingReporter', ['log'])): 40 | """A mock Reporter implementation.""" 41 | 42 | def flake(self, message): 43 | self.log.append(('flake', str(message))) 44 | 45 | def unexpected_error(self, filename, message): 46 | self.log.append(('unexpected_error', filename, message)) 47 | 48 | def syntax_error(self, filename, msg, lineno, offset, line): 49 | self.log.append(('syntax_error', filename, msg, lineno, offset, line)) 50 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/timothycrosley/deprecated.frosted/61ba7f341fc55676c3580c8c4e52117986cd5e12/logo.png -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """frosted/setup.py. 2 | 3 | Defines how frosted should be installed on a standard Python system. 4 | 5 | Copyright (C) 2005-2011 Divmod, Inc. 6 | Copyright (C) 2013 Florent Xicluna. 7 | Copyright (C) 2014 Timothy Edmund Crosley 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 10 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 11 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 12 | to permit persons to whom the Software is furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all copies or 15 | substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 18 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 19 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 20 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 21 | OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | """ 24 | 25 | import os 26 | import subprocess 27 | import sys 28 | 29 | try: 30 | from setuptools import setup 31 | from setuptools.command.test import test as TestCommand 32 | 33 | class PyTest(TestCommand): 34 | extra_kwargs = {'tests_require': ['pytest', 'mock']} 35 | 36 | def finalize_options(self): 37 | TestCommand.finalize_options(self) 38 | self.test_args = ['frosted/test'] 39 | self.test_suite = True 40 | 41 | def run_tests(self): 42 | import pytest 43 | os.chdir 44 | sys.exit(pytest.main(self.test_args)) 45 | 46 | except ImportError: 47 | from distutils.core import setup, Command 48 | 49 | class PyTest(Command): 50 | extra_kwargs = {} 51 | user_options = [] 52 | 53 | def initialize_options(self): 54 | pass 55 | 56 | def finalize_options(self): 57 | pass 58 | 59 | def run(self): 60 | raise SystemExit(subprocess.call([sys.executable, 'runtests.py', 'frosted/test'])) 61 | 62 | try: 63 | import pypandoc 64 | readme = pypandoc.convert('README.md', 'rst') 65 | except (IOError, ImportError, OSError, RuntimeError): 66 | readme = '' 67 | 68 | setup(name="frosted", 69 | license="MIT", 70 | version="1.4.1", 71 | description="A passive Python syntax checker", 72 | long_description=readme, 73 | author="Phil Frost", 74 | author_email="indigo@bitglue.com", 75 | maintainer="Timothy Crosley", 76 | maintainer_email="timothy.crosley@gmail.com", 77 | url="https://github.com/timothycrosley/frosted", 78 | packages=["frosted", "frosted.test"], 79 | requires=['pies'], 80 | install_requires=['pies>=2.6.0'], 81 | entry_points={ 82 | 'console_scripts': [ 83 | 'frosted = frosted.main:main', 84 | ] 85 | }, 86 | cmdclass={'test': PyTest}, 87 | classifiers=['Development Status :: 5 - Production/Stable', 88 | 'Environment :: Console', 89 | 'Intended Audience :: Developers', 90 | 'Natural Language :: English', 91 | 'License :: OSI Approved :: MIT License', 92 | 'Programming Language :: Python', 93 | 'Programming Language :: Python :: 2', 94 | 'Programming Language :: Python :: 2.6', 95 | 'Programming Language :: Python :: 2.7', 96 | 'Programming Language :: Python :: 3', 97 | 'Programming Language :: Python :: 3.0', 98 | 'Programming Language :: Python :: 3.1', 99 | 'Programming Language :: Python :: 3.2', 100 | 'Programming Language :: Python :: 3.3', 101 | 'Topic :: Software Development', 102 | 'Topic :: Utilities'], 103 | **PyTest.extra_kwargs) 104 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py25,py26,py27,py32,py33,py34 4 | # pypy is not ready yet; run manually with tox -e pypy if you want to see 5 | # the failures 6 | 7 | [testenv] 8 | deps = 9 | commands = 10 | python setup.py test -q 11 | --------------------------------------------------------------------------------