diff -Nru pyflakes-1.6.0/debian/changelog pyflakes-2.1.1/debian/changelog --- pyflakes-1.6.0/debian/changelog 2017-12-10 12:51:05.000000000 +0000 +++ pyflakes-2.1.1/debian/changelog 2019-10-03 19:01:33.000000000 +0000 @@ -1,3 +1,40 @@ +pyflakes (2.1.1-1~cloud0) bionic-train; urgency=medium + + * New upstream release for the Ubuntu Cloud Archive. + + -- Openstack Ubuntu Testing Bot Thu, 03 Oct 2019 19:01:33 +0000 + +pyflakes (2.1.1-1) unstable; urgency=medium + + * Team upload. + + [ Ondřej Nový ] + * New upstream release + * Rename d/tests/control.autodep8 to d/tests/control. + * Bump debhelper compat level to 12 and use debhelper-compat + * Bump Standards-Version to 4.4.0 (no changes needed) + * d/rules: Install NEWS.rst instead of NEWS.txt, which doesn't exists in new + upstream release + + [ Dimitri John Ledkov ] + * Drop myself from uploaders. + + -- Ondřej Nový Tue, 16 Jul 2019 18:30:30 +0200 + +pyflakes (2.0.0-1) unstable; urgency=medium + + [ Ondřej Nový ] + * d/tests: Use AUTOPKGTEST_TMP instead of ADTTMP + * d/control: Remove ancient X-Python-Version field + * d/control: Remove ancient X-Python3-Version field + + [ Jelmer Vernooij ] + * Add basic debian/upstream/metadata. + * New upstream release. + * Team upload. + + -- Jelmer Vernooij Sat, 17 Nov 2018 18:39:39 +0000 + pyflakes (1.6.0-1) unstable; urgency=medium * Team upload. diff -Nru pyflakes-1.6.0/debian/compat pyflakes-2.1.1/debian/compat --- pyflakes-1.6.0/debian/compat 2017-12-10 12:33:11.000000000 +0000 +++ pyflakes-2.1.1/debian/compat 1970-01-01 00:00:00.000000000 +0000 @@ -1 +0,0 @@ -10 diff -Nru pyflakes-1.6.0/debian/control pyflakes-2.1.1/debian/control --- pyflakes-1.6.0/debian/control 2017-12-10 12:50:24.000000000 +0000 +++ pyflakes-2.1.1/debian/control 2019-07-16 16:22:34.000000000 +0000 @@ -3,9 +3,8 @@ Priority: optional Maintainer: Python Applications Packaging Team Uploaders: Varun Hiremath , - Dimitri John Ledkov , Barry Warsaw -Build-Depends: debhelper (>= 10), +Build-Depends: debhelper-compat (= 12), dh-python, python-all, python-nose, @@ -13,13 +12,11 @@ python3-all, python3-nose, python3-setuptools -X-Python-Version: >= 2.6 -X-Python3-Version: >= 3.2 -Standards-Version: 4.1.2 +Standards-Version: 4.4.0 Homepage: https://launchpad.net/pyflakes Testsuite: autopkgtest-pkg-python -Vcs-Svn: svn://anonscm.debian.org/python-apps/packages/pyflakes/trunk/ -Vcs-Browser: https://anonscm.debian.org/viewvc/python-apps/packages/pyflakes/trunk/ +Vcs-Git: https://salsa.debian.org/python-team/applications/pyflakes.git +Vcs-Browser: https://salsa.debian.org/python-team/applications/pyflakes Package: pyflakes Architecture: all diff -Nru pyflakes-1.6.0/debian/gbp.conf pyflakes-2.1.1/debian/gbp.conf --- pyflakes-1.6.0/debian/gbp.conf 1970-01-01 00:00:00.000000000 +0000 +++ pyflakes-2.1.1/debian/gbp.conf 2019-07-16 16:13:17.000000000 +0000 @@ -0,0 +1,2 @@ +[DEFAULT] +debian-branch=debian/master diff -Nru pyflakes-1.6.0/debian/rules pyflakes-2.1.1/debian/rules --- pyflakes-1.6.0/debian/rules 2017-12-10 12:33:11.000000000 +0000 +++ pyflakes-2.1.1/debian/rules 2019-07-16 16:26:15.000000000 +0000 @@ -15,7 +15,7 @@ dh $@ --with python2,python3 --buildsystem=pybuild override_dh_installchangelogs: - dh_installchangelogs NEWS.txt + dh_installchangelogs NEWS.rst override_dh_auto_install: dh_auto_install diff -Nru pyflakes-1.6.0/debian/tests/control pyflakes-2.1.1/debian/tests/control --- pyflakes-1.6.0/debian/tests/control 1970-01-01 00:00:00.000000000 +0000 +++ pyflakes-2.1.1/debian/tests/control 2019-07-16 16:15:48.000000000 +0000 @@ -0,0 +1,8 @@ +Test-Command: pyflakes --help +Depends: pyflakes + +Test-Command: pyflakes3 --help +Depends: pyflakes3 + +Tests: relative-import.sh +Depends: python-pyflakes, python3-pyflakes diff -Nru pyflakes-1.6.0/debian/tests/control.autodep8 pyflakes-2.1.1/debian/tests/control.autodep8 --- pyflakes-1.6.0/debian/tests/control.autodep8 2017-12-10 12:33:11.000000000 +0000 +++ pyflakes-2.1.1/debian/tests/control.autodep8 1970-01-01 00:00:00.000000000 +0000 @@ -1,8 +0,0 @@ -Test-Command: pyflakes --help -Depends: pyflakes - -Test-Command: pyflakes3 --help -Depends: pyflakes3 - -Tests: relative-import.sh -Depends: python-pyflakes, python3-pyflakes diff -Nru pyflakes-1.6.0/debian/tests/relative-import.sh pyflakes-2.1.1/debian/tests/relative-import.sh --- pyflakes-1.6.0/debian/tests/relative-import.sh 2017-12-10 12:33:11.000000000 +0000 +++ pyflakes-2.1.1/debian/tests/relative-import.sh 2019-07-16 16:13:17.000000000 +0000 @@ -1,6 +1,6 @@ # LP: #1560134 -cd $ADTTMP +cd $AUTOPKGTEST_TMP cat > relative.py <>`` operator +- Treat ``async for`` the same as a ``for`` loop for introducing variables +- Add detection for list concatenation in ``__all__`` +- Exempt ``@typing.overload`` from duplicate function declaration +- Importing a submodule of an ``as``-aliased ``import``-import is marked as + used +- Report undefined names from ``__all__`` as possibly coming from a ``*`` + import +- Add support for changes in Python 3.8-dev +- Add support for PEP 563 (``from __future__ import annotations``) +- Include Python version and platform information in ``pyflakes --version`` +- Recognize ``__annotations__`` as a valid magic global in Python 3.6+ +- Mark names used in PEP 484 ``# type: ...`` comments as used +- Add check for use of ``is`` operator with ``str``, ``bytes``, and ``int`` + literals + +2.0.0 (2018-05-20) + +- Drop support for EOL Python <2.7 and 3.2-3.3 +- Check for unused exception binding in ``except:`` block +- Handle string literal type annotations +- Ignore redefinitions of ``_``, unless originally defined by import +- Support ``__class__`` without ``self`` in Python 3 +- Issue an error for ``raise NotImplemented(...)`` + +1.6.0 (2017-08-03) + +- Process function scope variable annotations for used names +- Find Python files without extensions by their shebang + +1.5.0 (2017-01-09) + +- Enable support for PEP 526 annotated assignments + +1.4.0 (2016-12-30): + +- Change formatting of ImportStarMessage to be consistent with other errors +- Support PEP 498 "f-strings" + +1.3.0 (2016-09-01): + +- Fix PyPy2 Windows IntegrationTests +- Check for duplicate dictionary keys +- Fix TestMain tests on Windows +- Fix "continue" and "break" checks ignoring py3.5's "async for" loop + +1.2.3 (2016-05-12): + +- Fix TypeError when processing relative imports + +1.2.2 (2016-05-06): + +- Avoid traceback when exception is del-ed in except + +1.2.1 (2015-05-05): + +- Fix false RedefinedWhileUnused for submodule imports + +1.2.0 (2016-05-03): + +- Warn against reusing exception names after the except: block on Python 3 +- Improve the error messages for imports + +1.1.0 (2016-03-01): + +- Allow main() to accept arguments. +- Support @ matrix-multiplication operator +- Validate ``__future__`` imports +- Fix doctest scope testing +- Warn for tuple assertions which are always true +- Warn for "import \*" not at module level on Python 3 +- Catch many more kinds of SyntaxErrors +- Check PEP 498 f-strings +- (and a few more sundry bugfixes) + +1.0.0 (2015-09-20): + +- Python 3.5 support. async/await statements in particular. +- Fix test_api.py on Windows. +- Eliminate a false UnusedImport warning when the name has been + declared "global" + +0.9.2 (2015-06-17): + +- Fix a traceback when a global is defined in one scope, and used in another. + +0.9.1 (2015-06-09): + +- Update NEWS.txt to include 0.9.0, which had been forgotten. + +0.9.0 (2015-05-31): + +- Exit gracefully, not with a traceback, on SIGINT and SIGPIPE. +- Fix incorrect report of undefined name when using lambda expressions in + generator expressions. +- Don't crash on DOS line endings on Windows and Python 2.6. +- Don't report an undefined name if the 'del' which caused a name to become + undefined is only conditionally executed. +- Properly handle differences in list comprehension scope in Python 3. +- Improve handling of edge cases around 'global' defined variables. +- Report an error for 'return' outside a function. + +0.8.1 (2014-03-30): + +- Detect the declared encoding in Python 3. +- Do not report redefinition of import in a local scope, if the + global name is used elsewhere in the module. +- Catch undefined variable in loop generator when it is also used as + loop variable. +- Report undefined name for ``(a, b) = (1, 2)`` but not for the general + unpacking ``(a, b) = func()``. +- Correctly detect when an imported module is used in default arguments + of a method, when the method and the module use the same name. +- Distribute a universal wheel file. + +0.8.0 (2014-03-22): + +- Adapt for the AST in Python 3.4. +- Fix caret position on SyntaxError. +- Fix crash on Python 2.x with some doctest SyntaxError. +- Add tox.ini. +- The ``PYFLAKES_NODOCTEST`` environment variable has been replaced with the + ``PYFLAKES_DOCTEST`` environment variable (with the opposite meaning). + Doctest checking is now disabled by default; set the environment variable + to enable it. +- Correctly parse incremental ``__all__ += [...]``. +- Catch return with arguments inside a generator (Python <= 3.2). +- Do not complain about ``_`` in doctests. +- Drop deprecated methods ``pushFunctionScope`` and ``pushClassScope``. + +0.7.3 (2013-07-02): + +- Do not report undefined name for generator expression and dict or + set comprehension at class level. +- Deprecate ``Checker.pushFunctionScope`` and ``Checker.pushClassScope``: + use ``Checker.pushScope`` instead. +- Remove dependency on Unittest2 for the tests. + +0.7.2 (2013-04-24): + +- Fix computation of ``DoctestSyntaxError.lineno`` and ``col``. +- Add boolean attribute ``Checker.withDoctest`` to ignore doctests. +- If environment variable ``PYFLAKES_NODOCTEST`` is set, skip doctests. +- Environment variable ``PYFLAKES_BUILTINS`` accepts a comma-separated + list of additional built-in names. + +0.7.1 (2013-04-23): + +- File ``bin/pyflakes`` was missing in tarball generated with distribute. +- Fix reporting errors in non-ASCII filenames (Python 2.x). + +0.7.0 (2013-04-17): + +- Add --version and --help options. +- Support ``python -m pyflakes`` (Python 2.7 and Python 3.x). +- Add attribute ``Message.col`` to report column offset. +- Do not report redefinition of variable for a variable used in a list + comprehension in a conditional. +- Do not report redefinition of variable for generator expressions and + set or dict comprehensions. +- Do not report undefined name when the code is protected with a + ``NameError`` exception handler. +- Do not report redefinition of variable when unassigning a module imported + for its side-effect. +- Support special locals like ``__tracebackhide__`` for py.test. +- Support checking doctests. +- Fix issue with Turkish locale where ``'i'.upper() == 'i'`` in Python 2. + +0.6.1 (2013-01-29): + +- Fix detection of variables in augmented assignments. + +0.6.0 (2013-01-29): + +- Support Python 3 up to 3.3, based on the pyflakes3k project. +- Preserve compatibility with Python 2.5 and all recent versions of Python. +- Support custom reporters in addition to the default Reporter. +- Allow function redefinition for modern property construction via + property.setter/deleter. +- Fix spurious redefinition warnings in conditionals. +- Do not report undefined name in ``__all__`` if import * is used. +- Add WindowsError as a known built-in name on all platforms. +- Support specifying additional built-ins in the ``Checker`` constructor. +- Don't issue Unused Variable warning when using locals() in current scope. +- Handle problems with the encoding of source files. +- Remove dependency on Twisted for the tests. +- Support ``python setup.py test`` and ``python setup.py develop``. +- Create script using setuptools ``entry_points`` to support all platforms, + including Windows. + +0.5.0 (2011-09-02): + +- Convert pyflakes to use newer _ast infrastructure rather than compiler. +- Support for new syntax in 2.7 (including set literals, set comprehensions, + and dictionary comprehensions). +- Make sure class names don't get bound until after class definition. + +0.4.0 (2009-11-25): + +- Fix reporting for certain SyntaxErrors which lack line number + information. +- Check for syntax errors more rigorously. +- Support checking names used with the class decorator syntax in versions + of Python which have it. +- Detect local variables which are bound but never used. +- Handle permission errors when trying to read source files. +- Handle problems with the encoding of source files. +- Support importing dotted names so as not to incorrectly report them as + redefined unused names. +- Support all forms of the with statement. +- Consider static ``__all__`` definitions and avoid reporting unused names + if the names are listed there. +- Fix incorrect checking of class names with respect to the names of their + bases in the class statement. +- Support the ``__path__`` global in ``__init__.py``. + +0.3.0 (2009-01-30): + +- Display more informative SyntaxError messages. +- Don't hang flymake with unmatched triple quotes (only report a single + line of source for a multiline syntax error). +- Recognize ``__builtins__`` as a defined name. +- Improve pyflakes support for python versions 2.3-2.5 +- Support for if-else expressions and with statements. +- Warn instead of error on non-existent file paths. +- Check for ``__future__`` imports after other statements. +- Add reporting for some types of import shadowing. +- Improve reporting of unbound locals diff -Nru pyflakes-1.6.0/NEWS.txt pyflakes-2.1.1/NEWS.txt --- pyflakes-1.6.0/NEWS.txt 2017-08-03 14:36:44.000000000 +0000 +++ pyflakes-2.1.1/NEWS.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,179 +0,0 @@ -1.6.0 (2017-08-03) - - Process function scope variable annotations for used names - - Find Python files without extensions by their shebang - -1.5.0 (2017-01-09) - - Enable support for PEP 526 annotated assignments - -1.4.0 (2016-12-30): - - Change formatting of ImportStarMessage to be consistent with other errors - - Support PEP 498 "f-strings" - -1.3.0 (2016-09-01): - - Fix PyPy2 Windows IntegrationTests - - Check for duplicate dictionary keys - - Fix TestMain tests on Windows - - Fix "continue" and "break" checks ignoring py3.5's "async for" loop - -1.2.3 (2016-05-12): - - Fix TypeError when processing relative imports - -1.2.2 (2016-05-06): - - Avoid traceback when exception is del-ed in except - -1.2.1 (2015-05-05): - - Fix false RedefinedWhileUnused for submodule imports - -1.2.0 (2016-05-03): - - Warn against reusing exception names after the except: block on Python 3 - - Improve the error messages for imports - -1.1.0 (2016-03-01): - - Allow main() to accept arguments. - - Support @ matrix-multiplication operator - - Validate __future__ imports - - Fix doctest scope testing - - Warn for tuple assertions which are always true - - Warn for "import *" not at module level on Python 3 - - Catch many more kinds of SyntaxErrors - - Check PEP 498 f-strings - - (and a few more sundry bugfixes) - -1.0.0 (2015-09-20): - - Python 3.5 support. async/await statements in particular. - - Fix test_api.py on Windows. - - Eliminate a false UnusedImport warning when the name has been - declared "global" - -0.9.2 (2015-06-17): - - Fix a traceback when a global is defined in one scope, and used in another. - -0.9.1 (2015-06-09): - - Update NEWS.txt to include 0.9.0, which had been forgotten. - -0.9.0 (2015-05-31): - - Exit gracefully, not with a traceback, on SIGINT and SIGPIPE. - - Fix incorrect report of undefined name when using lambda expressions in - generator expressions. - - Don't crash on DOS line endings on Windows and Python 2.6. - - Don't report an undefined name if the 'del' which caused a name to become - undefined is only conditionally executed. - - Properly handle differences in list comprehension scope in Python 3. - - Improve handling of edge cases around 'global' defined variables. - - Report an error for 'return' outside a function. - -0.8.1 (2014-03-30): - - Detect the declared encoding in Python 3. - - Do not report redefinition of import in a local scope, if the - global name is used elsewhere in the module. - - Catch undefined variable in loop generator when it is also used as - loop variable. - - Report undefined name for `(a, b) = (1, 2)` but not for the general - unpacking `(a, b) = func()`. - - Correctly detect when an imported module is used in default arguments - of a method, when the method and the module use the same name. - - Distribute a universal wheel file. - -0.8.0 (2014-03-22): - - Adapt for the AST in Python 3.4. - - Fix caret position on SyntaxError. - - Fix crash on Python 2.x with some doctest SyntaxError. - - Add tox.ini. - - The `PYFLAKES_NODOCTEST` environment variable has been replaced with the - `PYFLAKES_DOCTEST` environment variable (with the opposite meaning). - Doctest checking is now disabled by default; set the environment variable - to enable it. - - Correctly parse incremental `__all__ += [...]`. - - Catch return with arguments inside a generator (Python <= 3.2). - - Do not complain about `_` in doctests. - - Drop deprecated methods `pushFunctionScope` and `pushClassScope`. - -0.7.3 (2013-07-02): - - Do not report undefined name for generator expression and dict or - set comprehension at class level. - - Deprecate `Checker.pushFunctionScope` and `Checker.pushClassScope`: - use `Checker.pushScope` instead. - - Remove dependency on Unittest2 for the tests. - -0.7.2 (2013-04-24): - - Fix computation of `DoctestSyntaxError.lineno` and `col`. - - Add boolean attribute `Checker.withDoctest` to ignore doctests. - - If environment variable `PYFLAKES_NODOCTEST` is set, skip doctests. - - Environment variable `PYFLAKES_BUILTINS` accepts a comma-separated - list of additional built-in names. - -0.7.1 (2013-04-23): - - File `bin/pyflakes` was missing in tarball generated with distribute. - - Fix reporting errors in non-ASCII filenames (Python 2.x). - -0.7.0 (2013-04-17): - - Add --version and --help options. - - Support `python -m pyflakes` (Python 2.7 and Python 3.x). - - Add attribute `Message.col` to report column offset. - - Do not report redefinition of variable for a variable used in a list - comprehension in a conditional. - - Do not report redefinition of variable for generator expressions and - set or dict comprehensions. - - Do not report undefined name when the code is protected with a - `NameError` exception handler. - - Do not report redefinition of variable when unassigning a module imported - for its side-effect. - - Support special locals like `__tracebackhide__` for py.test. - - Support checking doctests. - - Fix issue with Turkish locale where `'i'.upper() == 'i'` in Python 2. - -0.6.1 (2013-01-29): - - Fix detection of variables in augmented assignments. - -0.6.0 (2013-01-29): - - Support Python 3 up to 3.3, based on the pyflakes3k project. - - Preserve compatibility with Python 2.5 and all recent versions of Python. - - Support custom reporters in addition to the default Reporter. - - Allow function redefinition for modern property construction via - property.setter/deleter. - - Fix spurious redefinition warnings in conditionals. - - Do not report undefined name in __all__ if import * is used. - - Add WindowsError as a known built-in name on all platforms. - - Support specifying additional built-ins in the `Checker` constructor. - - Don't issue Unused Variable warning when using locals() in current scope. - - Handle problems with the encoding of source files. - - Remove dependency on Twisted for the tests. - - Support `python setup.py test` and `python setup.py develop`. - - Create script using setuptools `entry_points` to support all platforms, - including Windows. - -0.5.0 (2011-09-02): - - Convert pyflakes to use newer _ast infrastructure rather than compiler. - - Support for new syntax in 2.7 (including set literals, set comprehensions, - and dictionary comprehensions). - - Make sure class names don't get bound until after class definition. - -0.4.0 (2009-11-25): - - Fix reporting for certain SyntaxErrors which lack line number - information. - - Check for syntax errors more rigorously. - - Support checking names used with the class decorator syntax in versions - of Python which have it. - - Detect local variables which are bound but never used. - - Handle permission errors when trying to read source files. - - Handle problems with the encoding of source files. - - Support importing dotted names so as not to incorrectly report them as - redefined unused names. - - Support all forms of the with statement. - - Consider static `__all__` definitions and avoid reporting unused names - if the names are listed there. - - Fix incorrect checking of class names with respect to the names of their - bases in the class statement. - - Support the `__path__` global in `__init__.py`. - -0.3.0 (2009-01-30): - - Display more informative SyntaxError messages. - - Don't hang flymake with unmatched triple quotes (only report a single - line of source for a multiline syntax error). - - Recognize __builtins__ as a defined name. - - Improve pyflakes support for python versions 2.3-2.5 - - Support for if-else expressions and with statements. - - Warn instead of error on non-existent file paths. - - Check for __future__ imports after other statements. - - Add reporting for some types of import shadowing. - - Improve reporting of unbound locals diff -Nru pyflakes-1.6.0/PKG-INFO pyflakes-2.1.1/PKG-INFO --- pyflakes-1.6.0/PKG-INFO 2017-08-03 14:54:11.000000000 +0000 +++ pyflakes-2.1.1/PKG-INFO 2019-02-28 19:25:22.000000000 +0000 @@ -1,6 +1,6 @@ -Metadata-Version: 1.1 +Metadata-Version: 1.2 Name: pyflakes -Version: 1.6.0 +Version: 2.1.1 Summary: passive checker of Python programs Home-page: https://github.com/PyCQA/pyflakes Author: A lot of people @@ -16,8 +16,8 @@ parsing the source file, not importing it, so it is safe to use on modules with side effects. It's also much faster. - It is `available on PyPI `_ - and it supports all active versions of Python from 2.5 to 3.6. + It is `available on PyPI `_ + and it supports all active versions of Python: 2.7 and 3.4 to 3.7. @@ -40,7 +40,7 @@ * If you require more options and more flexibility, you could give a look to Flake8_ too. - + Design Principles ----------------- @@ -85,12 +85,17 @@ :alt: Build status .. _Pylint: http://www.pylint.org/ - .. _flake8: https://pypi.python.org/pypi/flake8 + .. _flake8: https://pypi.org/project/flake8/ .. _`PEP 8`: http://legacy.python.org/dev/peps/pep-0008/ .. _Pychecker: http://pychecker.sourceforge.net/ .. _`rebase your changes`: https://git-scm.com/book/en/v2/Git-Branching-Rebasing .. _`GitHub pull request`: https://github.com/PyCQA/pyflakes/pulls + Changelog + --------- + + Please see `NEWS.rst `_. + Platform: UNKNOWN Classifier: Development Status :: 6 - Mature Classifier: Environment :: Console @@ -98,6 +103,14 @@ Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development Classifier: Topic :: Utilities +Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* diff -Nru pyflakes-1.6.0/pyflakes/api.py pyflakes-2.1.1/pyflakes/api.py --- pyflakes-1.6.0/pyflakes/api.py 2017-06-21 14:37:06.000000000 +0000 +++ pyflakes-2.1.1/pyflakes/api.py 2019-02-28 19:10:37.000000000 +0000 @@ -3,17 +3,17 @@ """ from __future__ import with_statement -import sys +import ast import os +import platform import re -import _ast +import sys from pyflakes import checker, __version__ from pyflakes import reporter as modReporter __all__ = ['check', 'checkPath', 'checkRecursive', 'iterSourceCode', 'main'] - PYTHON_SHEBANG_REGEX = re.compile(br'^#!.*\bpython[23w]?\b\s*$') @@ -38,7 +38,7 @@ reporter = modReporter._makeDefaultReporter() # First, compile into an AST and handle syntax errors. try: - tree = compile(codeString, filename, "exec", _ast.PyCF_ONLY_AST) + tree = ast.parse(codeString, filename=filename) except SyntaxError: value = sys.exc_info()[1] msg = value.args[0] @@ -70,7 +70,8 @@ reporter.unexpectedError(filename, 'problem decoding source') return 1 # Okay, it's syntactically valid. Now check it. - w = checker.Checker(tree, filename) + file_tokens = checker.make_tokens(codeString) + w = checker.Checker(tree, file_tokens=file_tokens, filename=filename) w.messages.sort(key=lambda m: m.lineno) for warning in w.messages: reporter.flake(warning) @@ -89,22 +90,8 @@ if reporter is None: reporter = modReporter._makeDefaultReporter() try: - # in Python 2.6, compile() will choke on \r\n line endings. In later - # versions of python it's smarter, and we want binary mode to give - # compile() the best opportunity to do the right thing WRT text - # encodings. - if sys.version_info < (2, 7): - mode = 'rU' - else: - mode = 'rb' - - with open(filename, mode) as f: + with open(filename, 'rb') as f: codestr = f.read() - if sys.version_info < (2, 7): - codestr += '\n' # Work around for Python <= 2.6 - except UnicodeError: - reporter.unexpectedError(filename, 'problem decoding source') - return 1 except IOError: msg = sys.exc_info()[1] reporter.unexpectedError(filename, msg.args[1]) @@ -117,6 +104,10 @@ if filename.endswith('.py'): return True + # Avoid obvious Emacs backup files + if filename.endswith("~"): + return False + max_bytes = 128 try: @@ -193,6 +184,14 @@ pass +def _get_version(): + """ + Retrieve and format package version along with python version & OS used + """ + return ('%s Python %s on %s' % + (__version__, platform.python_version(), platform.system())) + + def main(prog=None, args=None): """Entry point for the script "pyflakes".""" import optparse @@ -201,7 +200,7 @@ _exitOnSignal('SIGINT', '... stopped') _exitOnSignal('SIGPIPE', 1) - parser = optparse.OptionParser(prog=prog, version=__version__) + parser = optparse.OptionParser(prog=prog, version=_get_version()) (__, args) = parser.parse_args(args=args) reporter = modReporter._makeDefaultReporter() if args: diff -Nru pyflakes-1.6.0/pyflakes/checker.py pyflakes-2.1.1/pyflakes/checker.py --- pyflakes-1.6.0/pyflakes/checker.py 2017-06-21 14:37:06.000000000 +0000 +++ pyflakes-2.1.1/pyflakes/checker.py 2019-02-28 19:10:37.000000000 +0000 @@ -5,14 +5,22 @@ Also, it models the Bindings and Scopes. """ import __future__ +import ast +import bisect +import collections import doctest +import functools import os +import re import sys +import tokenize + +from pyflakes import messages PY2 = sys.version_info < (3, 0) -PY32 = sys.version_info < (3, 3) # Python 2.5 to 3.2 -PY33 = sys.version_info < (3, 4) # Python 2.5 to 3.3 -PY34 = sys.version_info < (3, 5) # Python 2.5 to 3.4 +PY35_PLUS = sys.version_info >= (3, 5) # Python 3.5 and above +PY36_PLUS = sys.version_info >= (3, 6) # Python 3.6 and above +PY38_PLUS = sys.version_info >= (3, 8) try: sys.pypy_version_info PYPY = True @@ -21,29 +29,31 @@ builtin_vars = dir(__import__('__builtin__' if PY2 else 'builtins')) -try: - import ast -except ImportError: # Python 2.5 - import _ast as ast - - if 'decorator_list' not in ast.ClassDef._fields: - # Patch the missing attribute 'decorator_list' - ast.ClassDef.decorator_list = () - ast.FunctionDef.decorator_list = property(lambda s: s.decorators) - -from pyflakes import messages - +if PY2: + tokenize_tokenize = tokenize.generate_tokens +else: + tokenize_tokenize = tokenize.tokenize if PY2: def getNodeType(node_class): # workaround str.upper() which is locale-dependent return str(unicode(node_class.__name__).upper()) + + def get_raise_argument(node): + return node.type + else: def getNodeType(node_class): return node_class.__name__.upper() + def get_raise_argument(node): + return node.exc + + # Silence `pyflakes` from reporting `undefined name 'unicode'` in Python 3. + unicode = str + # Python >= 3.3 uses ast.Try instead of (ast.TryExcept + ast.TryFinally) -if PY32: +if PY2: def getAlternatives(n): if isinstance(n, (ast.If, ast.TryFinally)): return [n.body] @@ -56,10 +66,19 @@ if isinstance(n, ast.Try): return [n.body + n.orelse] + [[hdl] for hdl in n.handlers] -if PY34: - LOOP_TYPES = (ast.While, ast.For) -else: +if PY35_PLUS: + FOR_TYPES = (ast.For, ast.AsyncFor) LOOP_TYPES = (ast.While, ast.For, ast.AsyncFor) +else: + FOR_TYPES = (ast.For,) + LOOP_TYPES = (ast.While, ast.For) + +# https://github.com/python/typed_ast/blob/55420396/ast27/Parser/tokenizer.c#L102-L104 +TYPE_COMMENT_RE = re.compile(r'^#\s*type:\s*') +# https://github.com/python/typed_ast/blob/55420396/ast27/Parser/tokenizer.c#L1400 +TYPE_IGNORE_RE = re.compile(TYPE_COMMENT_RE.pattern + r'ignore\s*(#|$)') +# https://github.com/python/typed_ast/blob/55420396/ast27/Grammar/Grammar#L147 +TYPE_FUNC_RE = re.compile(r'^(\(.*?\))\s*->\s*(.*)$') class _FieldsOrder(dict): @@ -96,9 +115,15 @@ """ Yield all direct child nodes of *node*, that is, all fields that are nodes and all items of fields that are lists of nodes. + + :param node: AST node to be iterated upon + :param omit: String or tuple of strings denoting the + attributes of the node to be omitted from + further parsing + :param _fields_order: Order of AST node fields """ for name in _fields_order[node.__class__]: - if name == omit: + if omit and name in omit: continue field = getattr(node, name, None) if isinstance(field, ast.AST): @@ -128,13 +153,17 @@ result.name, result, ) - elif (not PY33) and isinstance(item, ast.NameConstant): + elif (not PY2) and isinstance(item, ast.NameConstant): # None, True, False are nameconstants in python3, but names in 2 return item.value else: return UnhandledKeyType() +def is_notimplemented_name_node(node): + return isinstance(node, ast.Name) and getNodeName(node) == 'NotImplemented' + + class Binding(object): """ Represents the binding of a value to a name. @@ -171,6 +200,18 @@ """ +class Builtin(Definition): + """A definition created for all Python builtins.""" + + def __init__(self, name): + super(Builtin, self).__init__(name, None) + + def __repr__(self): + return '<%s object %r at 0x%x>' % (self.__class__.__name__, + self.name, + id(self)) + + class UnhandledKeyType(object): """ A dictionary key of a type that we cannot or do not check for duplicates. @@ -188,8 +229,8 @@ def __eq__(self, compare): return ( - compare.__class__ == self.__class__ - and compare.name == self.name + compare.__class__ == self.__class__ and + compare.name == self.name ) def __hash__(self): @@ -367,10 +408,10 @@ can be determined statically, they will be treated as names for export and additional checking applied to them. - The only C{__all__} assignment that can be recognized is one which takes - the value of a literal list containing literal strings. For example:: + The only recognized C{__all__} assignment via list concatenation is in the + following format: - __all__ = ["foo", "bar"] + __all__ = ['a'] + ['b'] + ['c'] Names which are imported and not otherwise used but appear in the value of C{__all__} will not have an unused import warning reported for them. @@ -381,10 +422,32 @@ self.names = list(scope['__all__'].names) else: self.names = [] - if isinstance(source.value, (ast.List, ast.Tuple)): - for node in source.value.elts: + + def _add_to_names(container): + for node in container.elts: if isinstance(node, ast.Str): self.names.append(node.s) + + if isinstance(source.value, (ast.List, ast.Tuple)): + _add_to_names(source.value) + # If concatenating lists + elif isinstance(source.value, ast.BinOp): + currentValue = source.value + while isinstance(currentValue.right, ast.List): + left = currentValue.left + right = currentValue.right + _add_to_names(right) + # If more lists are being added + if isinstance(left, ast.BinOp): + currentValue = left + # If just two lists are being added + elif isinstance(left, ast.List): + _add_to_names(left) + # All lists accounted for - done + break + # If not list concatenation + else: + break super(ExportBinding, self).__init__(name, source) @@ -407,8 +470,8 @@ @ivar globals: Names declared 'global' in this function. """ usesLocals = False - alwaysUsed = set(['__tracebackhide__', - '__traceback_info__', '__traceback_supplement__']) + alwaysUsed = {'__tracebackhide__', '__traceback_info__', + '__traceback_supplement__'} def __init__(self): super(FunctionScope, self).__init__() @@ -422,9 +485,11 @@ Return a generator for the assignments which have not been used. """ for name, binding in self.items(): - if (not binding.used and name not in self.globals - and not self.usesLocals - and isinstance(binding, Assignment)): + if (not binding.used and + name != '_' and # see issue #202 + name not in self.globals and + not self.usesLocals and + isinstance(binding, Assignment)): yield name, binding @@ -435,15 +500,26 @@ class ModuleScope(Scope): """Scope for a module.""" _futures_allowed = True + _annotations_future_enabled = False class DoctestScope(ModuleScope): """Scope for a doctest.""" +class DummyNode(object): + """Used in place of an `ast.AST` to set error message positions""" + def __init__(self, lineno, col_offset): + self.lineno = lineno + self.col_offset = col_offset + + # Globally defined names which are not attributes of the builtins module, or # are only present on some platforms. _MAGIC_GLOBALS = ['__file__', '__builtins__', 'WindowsError'] +# module scope annotation will store in `__annotations__`, see also PEP 526. +if PY36_PLUS: + _MAGIC_GLOBALS.append('__annotations__') def getNodeName(node): @@ -454,6 +530,86 @@ return node.name +def is_typing_overload(value, scope): + def is_typing_overload_decorator(node): + return ( + ( + isinstance(node, ast.Name) and + node.id in scope and + isinstance(scope[node.id], ImportationFrom) and + scope[node.id].fullName == 'typing.overload' + ) or ( + isinstance(node, ast.Attribute) and + isinstance(node.value, ast.Name) and + node.value.id == 'typing' and + node.attr == 'overload' + ) + ) + + return ( + isinstance(value.source, ast.FunctionDef) and + len(value.source.decorator_list) == 1 and + is_typing_overload_decorator(value.source.decorator_list[0]) + ) + + +def make_tokens(code): + # PY3: tokenize.tokenize requires readline of bytes + if not isinstance(code, bytes): + code = code.encode('UTF-8') + lines = iter(code.splitlines(True)) + # next(lines, b'') is to prevent an error in pypy3 + return tuple(tokenize_tokenize(lambda: next(lines, b''))) + + +class _TypeableVisitor(ast.NodeVisitor): + """Collect the line number and nodes which are deemed typeable by + PEP 484 + + https://www.python.org/dev/peps/pep-0484/#type-comments + """ + def __init__(self): + self.typeable_lines = [] # type: List[int] + self.typeable_nodes = {} # type: Dict[int, ast.AST] + + def _typeable(self, node): + # if there is more than one typeable thing on a line last one wins + self.typeable_lines.append(node.lineno) + self.typeable_nodes[node.lineno] = node + + self.generic_visit(node) + + visit_Assign = visit_For = visit_FunctionDef = visit_With = _typeable + visit_AsyncFor = visit_AsyncFunctionDef = visit_AsyncWith = _typeable + + +def _collect_type_comments(tree, tokens): + visitor = _TypeableVisitor() + visitor.visit(tree) + + type_comments = collections.defaultdict(list) + for tp, text, start, _, _ in tokens: + if ( + tp != tokenize.COMMENT or # skip non comments + not TYPE_COMMENT_RE.match(text) or # skip non-type comments + TYPE_IGNORE_RE.match(text) # skip ignores + ): + continue + + # search for the typeable node at or before the line number of the + # type comment. + # if the bisection insertion point is before any nodes this is an + # invalid type comment which is ignored. + lineno, _ = start + idx = bisect.bisect_right(visitor.typeable_lines, lineno) + if idx == 0: + continue + node = visitor.typeable_nodes[visitor.typeable_lines[idx - 1]] + type_comments[node].append((start, text)) + + return type_comments + + class Checker(object): """ I check the cleanliness and sanity of Python code. @@ -467,6 +623,19 @@ callables which are deferred assignment checks. """ + _ast_node_scope = { + ast.Module: ModuleScope, + ast.ClassDef: ClassScope, + ast.FunctionDef: FunctionScope, + ast.Lambda: FunctionScope, + ast.ListComp: GeneratorScope, + ast.SetComp: GeneratorScope, + ast.GeneratorExp: GeneratorScope, + ast.DictComp: GeneratorScope, + } + if PY35_PLUS: + _ast_node_scope[ast.AsyncFunctionDef] = FunctionScope, + nodeDepth = 0 offset = None traceTree = False @@ -477,8 +646,11 @@ builtIns.update(_customBuiltIns.split(',')) del _customBuiltIns + # TODO: file_tokens= is required to perform checks on type comments, + # eventually make this a required positional argument. For now it + # is defaulted to `()` for api compatibility. def __init__(self, tree, filename='(none)', builtins=None, - withDoctest='PYFLAKES_DOCTEST' in os.environ): + withDoctest='PYFLAKES_DOCTEST' in os.environ, file_tokens=()): self._nodeHandlers = {} self._deferredFunctions = [] self._deferredAssignments = [] @@ -488,9 +660,15 @@ if builtins: self.builtIns = self.builtIns.union(builtins) self.withDoctest = withDoctest - self.scopeStack = [ModuleScope()] + try: + self.scopeStack = [Checker._ast_node_scope[type(tree)]()] + except KeyError: + raise RuntimeError('No scope implemented for the node %r' % tree) self.exceptHandlers = [()] self.root = tree + self._type_comments = _collect_type_comments(tree, file_tokens) + for builtin in self.builtIns: + self.addBinding(None, Builtin(builtin)) self.handleChildren(tree) self.runDeferred(self._deferredFunctions) # Set _deferredFunctions to None so that deferFunction will fail @@ -550,6 +728,19 @@ self.scope._futures_allowed = False @property + def annotationsFutureEnabled(self): + scope = self.scopeStack[0] + if not isinstance(scope, ModuleScope): + return False + return scope._annotations_future_enabled + + @annotationsFutureEnabled.setter + def annotationsFutureEnabled(self, value): + assert value is True + assert isinstance(self.scope, ModuleScope) + self.scope._annotations_future_enabled = True + + @property def scope(self): return self.scopeStack[-1] @@ -586,9 +777,16 @@ # mark all import '*' as used by the undefined in __all__ if scope.importStarred: + from_list = [] for binding in scope.values(): if isinstance(binding, StarImportation): binding.used = all_binding + from_list.append(binding.fullName) + # report * usage, with a list of possible sources + from_list = ', '.join(sorted(from_list)) + for name in undefined: + self.report(messages.ImportStarUsage, + scope['__all__'].source, name, from_list) # Look for imported names that aren't used. for value in scope.values(): @@ -598,7 +796,7 @@ messg = messages.UnusedImport self.report(messg, value.source, str(value)) for node in value.redefined: - if isinstance(self.getParent(node), ast.For): + if isinstance(self.getParent(node), FOR_TYPES): messg = messages.ImportShadowedByLoopVar elif used: continue @@ -638,6 +836,18 @@ return True return False + def _getAncestor(self, node, ancestor_type): + parent = node + while True: + if parent is self.root: + return None + parent = self.getParent(parent) + if isinstance(parent, ancestor_type): + return parent + + def getScopeNode(self, node): + return self._getAncestor(node, tuple(Checker._ast_node_scope.keys())) + def differentForks(self, lnode, rnode): """True, if lnode and rnode are located on different forks of IF/TRY""" ancestor = self.getCommonAncestor(lnode, rnode, self.root) @@ -662,22 +872,25 @@ break existing = scope.get(value.name) - if existing and not self.differentForks(node, existing.source): + if (existing and not isinstance(existing, Builtin) and + not self.differentForks(node, existing.source)): parent_stmt = self.getParent(value.source) - if isinstance(existing, Importation) and isinstance(parent_stmt, ast.For): + if isinstance(existing, Importation) and isinstance(parent_stmt, FOR_TYPES): self.report(messages.ImportShadowedByLoopVar, node, value.name, existing.source) elif scope is self.scope: if (isinstance(parent_stmt, ast.comprehension) and not isinstance(self.getParent(existing.source), - (ast.For, ast.comprehension))): + (FOR_TYPES, ast.comprehension))): self.report(messages.RedefinedInListComp, node, value.name, existing.source) elif not existing.used and value.redefines(existing): - self.report(messages.RedefinedWhileUnused, - node, value.name, existing.source) + if value.name != '_' or isinstance(existing, Importation): + if not is_typing_overload(existing, self.scope): + self.report(messages.RedefinedWhileUnused, + node, value.name, existing.source) elif isinstance(existing, Importation) and value.redefines(existing): existing.redefined.append(node) @@ -706,13 +919,34 @@ # try enclosing function scopes and global scope for scope in self.scopeStack[-1::-1]: - # only generators used in a class scope can access the names - # of the class. this is skipped during the first iteration - if in_generators is False and isinstance(scope, ClassScope): - continue + if isinstance(scope, ClassScope): + if not PY2 and name == '__class__': + return + elif in_generators is False: + # only generators used in a class scope can access the + # names of the class. this is skipped during the first + # iteration + continue + + if (name == 'print' and + isinstance(scope.get(name, None), Builtin)): + parent = self.getParent(node) + if (isinstance(parent, ast.BinOp) and + isinstance(parent.op, ast.RShift)): + self.report(messages.InvalidPrintSyntax, node) try: scope[name].used = (self.scope, node) + + # if the name of SubImportation is same as + # alias of other Importation and the alias + # is used, SubImportation also should be marked as used. + n = scope[name] + if isinstance(n, Importation) and n._has_alias(): + try: + scope[n.fullName].used = (self.scope, node) + except KeyError: + pass except KeyError: pass else: @@ -723,10 +957,6 @@ if in_generators is not False: in_generators = isinstance(scope, GeneratorScope) - # look in the built-ins - if name in self.builtIns: - return - if importStarred: from_list = [] @@ -746,6 +976,9 @@ # the special name __path__ is valid only in packages return + if name == '__module__' and isinstance(self.scope, ClassScope): + return + # protected with a NameError handler? if 'NameError' not in self.exceptHandlers[-1]: self.report(messages.UndefinedName, node, name) @@ -771,12 +1004,14 @@ break parent_stmt = self.getParent(node) - if isinstance(parent_stmt, (ast.For, ast.comprehension)) or ( + if isinstance(parent_stmt, (FOR_TYPES, ast.comprehension)) or ( parent_stmt != node.parent and not self.isLiteralTupleUnpacking(parent_stmt)): binding = Binding(name, node) elif name == '__all__' and isinstance(self.scope, ModuleScope): binding = ExportBinding(name, node.parent, self.scope) + elif isinstance(getattr(node, 'ctx', None), ast.Param): + binding = Argument(name, self.getScopeNode(node)) else: binding = Assignment(name, node) self.addBinding(node, binding) @@ -811,7 +1046,29 @@ except KeyError: self.report(messages.UndefinedName, node, name) + def _handle_type_comments(self, node): + for (lineno, col_offset), comment in self._type_comments.get(node, ()): + comment = comment.split(':', 1)[1].strip() + func_match = TYPE_FUNC_RE.match(comment) + if func_match: + parts = ( + func_match.group(1).replace('*', ''), + func_match.group(2).strip(), + ) + else: + parts = (comment,) + + for part in parts: + if PY2: + part = part.replace('...', 'Ellipsis') + self.deferFunction(functools.partial( + self.handleStringAnnotation, + part, DummyNode(lineno, col_offset), lineno, col_offset, + messages.CommentAnnotationSyntaxError, + )) + def handleChildren(self, tree, omit=None): + self._handle_type_comments(tree) for node in iter_child_nodes(tree, omit=omit): self.handleNode(node, tree) @@ -836,7 +1093,7 @@ if not isinstance(node, ast.Str): return (None, None) - if PYPY: + if PYPY or PY38_PLUS: doctest_lineno = node.lineno - 1 else: # Computed incorrectly if the docstring has backslash @@ -896,12 +1153,10 @@ self.scopeStack = [self.scopeStack[0]] node_offset = self.offset or (0, 0) self.pushScope(DoctestScope) - underscore_in_builtins = '_' in self.builtIns - if not underscore_in_builtins: - self.builtIns.add('_') + self.addBinding(None, Builtin('_')) for example in examples: try: - tree = compile(example.source, "", "exec", ast.PyCF_ONLY_AST) + tree = ast.parse(example.source, "") except SyntaxError: e = sys.exc_info()[1] if PYPY: @@ -914,27 +1169,64 @@ node_offset[1] + example.indent + 4) self.handleChildren(tree) self.offset = node_offset - if not underscore_in_builtins: - self.builtIns.remove('_') self.popScope() self.scopeStack = saved_stack + def handleStringAnnotation(self, s, node, ref_lineno, ref_col_offset, err): + try: + tree = ast.parse(s) + except SyntaxError: + self.report(err, node, s) + return + + body = tree.body + if len(body) != 1 or not isinstance(body[0], ast.Expr): + self.report(err, node, s) + return + + parsed_annotation = tree.body[0].value + for descendant in ast.walk(parsed_annotation): + if ( + 'lineno' in descendant._attributes and + 'col_offset' in descendant._attributes + ): + descendant.lineno = ref_lineno + descendant.col_offset = ref_col_offset + + self.handleNode(parsed_annotation, node) + + def handleAnnotation(self, annotation, node): + if isinstance(annotation, ast.Str): + # Defer handling forward annotation. + self.deferFunction(functools.partial( + self.handleStringAnnotation, + annotation.s, + node, + annotation.lineno, + annotation.col_offset, + messages.ForwardAnnotationSyntaxError, + )) + elif self.annotationsFutureEnabled: + self.deferFunction(lambda: self.handleNode(annotation, node)) + else: + self.handleNode(annotation, node) + def ignore(self, node): pass # "stmt" type nodes DELETE = PRINT = FOR = ASYNCFOR = WHILE = IF = WITH = WITHITEM = \ - ASYNCWITH = ASYNCWITHITEM = RAISE = TRYFINALLY = EXEC = \ + ASYNCWITH = ASYNCWITHITEM = TRYFINALLY = EXEC = \ EXPR = ASSIGN = handleChildren PASS = ignore # "expr" type nodes BOOLOP = BINOP = UNARYOP = IFEXP = SET = \ - COMPARE = CALL = REPR = ATTRIBUTE = SUBSCRIPT = \ + CALL = REPR = ATTRIBUTE = SUBSCRIPT = \ STARRED = NAMECONSTANT = handleChildren - NUM = STR = BYTES = ELLIPSIS = ignore + NUM = STR = BYTES = ELLIPSIS = CONSTANT = ignore # "slice" type nodes SLICE = EXTSLICE = INDEX = handleChildren @@ -948,6 +1240,19 @@ EQ = NOTEQ = LT = LTE = GT = GTE = IS = ISNOT = IN = NOTIN = \ MATMULT = ignore + def RAISE(self, node): + self.handleChildren(node) + + arg = get_raise_argument(node) + + if isinstance(arg, ast.Call): + if is_notimplemented_name_node(arg.func): + # Handle "raise NotImplemented(...)" + self.report(messages.RaiseNotImplemented, node) + elif is_notimplemented_name_node(arg): + # Handle "raise NotImplemented" + self.report(messages.RaiseNotImplemented, node) + # additional node types COMPREHENSION = KEYWORD = FORMATTEDVALUE = JOINEDSTR = handleChildren @@ -1040,17 +1345,16 @@ # Locate the name in locals / function / globals scopes. if isinstance(node.ctx, (ast.Load, ast.AugLoad)): self.handleNodeLoad(node) - if (node.id == 'locals' and isinstance(self.scope, FunctionScope) - and isinstance(node.parent, ast.Call)): + if (node.id == 'locals' and isinstance(self.scope, FunctionScope) and + isinstance(node.parent, ast.Call)): # we are doing locals() call in current scope self.scope.usesLocals = True - elif isinstance(node.ctx, (ast.Store, ast.AugStore)): + elif isinstance(node.ctx, (ast.Store, ast.AugStore, ast.Param)): self.handleNodeStore(node) elif isinstance(node.ctx, ast.Del): self.handleNodeDelete(node) else: - # must be a Param context -- this only happens for names in function - # arguments, but these aren't dispatched through here + # Unknown context raise RuntimeError("Got impossible expression context: %r" % (node.ctx,)) def CONTINUE(self, node): @@ -1141,9 +1445,9 @@ wildcard = getattr(node.args, arg_name) if not wildcard: continue - args.append(wildcard if PY33 else wildcard.arg) + args.append(wildcard if PY2 else wildcard.arg) if is_py3_func: - if PY33: # Python 2.5 to 3.3 + if PY2: # Python 2.7 argannotation = arg_name + 'annotation' annotations.append(getattr(node.args, argannotation)) else: # Python >= 3.4 @@ -1157,22 +1461,17 @@ if arg in args[:idx]: self.report(messages.DuplicateArgument, node, arg) - for child in annotations + defaults: - if child: - self.handleNode(child, node) + for annotation in annotations: + self.handleAnnotation(annotation, node) + + for default in defaults: + self.handleNode(default, node) def runFunction(): self.pushScope() - for name in args: - self.addBinding(node, Argument(name, node)) - if isinstance(node.body, list): - # case for FunctionDefs - for stmt in node.body: - self.handleNode(stmt, node) - else: - # case for Lambdas - self.handleNode(node.body, node) + + self.handleChildren(node, omit='decorator_list') def checkUnusedAssignments(): """ @@ -1182,7 +1481,7 @@ self.report(messages.UnusedVariable, binding.source, name) self.deferAssignment(checkUnusedAssignments) - if PY32: + if PY2: def checkReturnWithArgumentInsideGenerator(): """ Check to see if there is any return statement with @@ -1196,6 +1495,18 @@ self.deferFunction(runFunction) + def ARGUMENTS(self, node): + self.handleChildren(node, omit=('defaults', 'kw_defaults')) + if PY2: + scope_node = self.getScopeNode(node) + if node.vararg: + self.addBinding(node, Argument(node.vararg, scope_node)) + if node.kwarg: + self.addBinding(node, Argument(node.kwarg, scope_node)) + + def ARG(self, node): + self.addBinding(node, Argument(node.arg, self.getScopeNode(node))) + def CLASSDEF(self, node): """ Check names used in a class definition, including its decorators, base @@ -1277,6 +1588,8 @@ if alias.name not in __future__.all_feature_names: self.report(messages.FutureFeatureNotDefined, node, alias.name) + if alias.name == 'annotations': + self.annotationsFutureEnabled = True elif alias.name == '*': # Only Python 2, local import * is a SyntaxWarning if not PY2 and not isinstance(self.scope, ModuleScope): @@ -1319,34 +1632,45 @@ self.handleChildren(node) return - # 3.x: the name of the exception, which is not a Name node, but - # a simple string, creates a local that is only bound within the scope - # of the except: block. + # If the name already exists in the scope, modify state of existing + # binding. + if node.name in self.scope: + self.handleNodeStore(node) - for scope in self.scopeStack[::-1]: - if node.name in scope: - is_name_previously_defined = True - break - else: - is_name_previously_defined = False + # 3.x: the name of the exception, which is not a Name node, but a + # simple string, creates a local that is only bound within the scope of + # the except: block. As such, temporarily remove the existing binding + # to more accurately determine if the name is used in the except: + # block. + + try: + prev_definition = self.scope.pop(node.name) + except KeyError: + prev_definition = None self.handleNodeStore(node) self.handleChildren(node) - if not is_name_previously_defined: - # See discussion on https://github.com/PyCQA/pyflakes/pull/59 - # We're removing the local name since it's being unbound - # after leaving the except: block and it's always unbound - # if the except: block is never entered. This will cause an - # "undefined name" error raised if the checked code tries to - # use the name afterwards. - # - # Unless it's been removed already. Then do nothing. + # See discussion on https://github.com/PyCQA/pyflakes/pull/59 - try: - del self.scope[node.name] - except KeyError: - pass + # We're removing the local name since it's being unbound after leaving + # the except: block and it's always unbound if the except: block is + # never entered. This will cause an "undefined name" error raised if + # the checked code tries to use the name afterwards. + # + # Unless it's been removed already. Then do nothing. + + try: + binding = self.scope.pop(node.name) + except KeyError: + pass + else: + if not binding.used: + self.report(messages.UnusedVariable, node, node.name) + + # Restore. + if prev_definition: + self.scope[node.name] = prev_definition def ANNASSIGN(self, node): if node.value: @@ -1354,7 +1678,21 @@ # Otherwise it's not really ast.Store and shouldn't silence # UndefinedLocal warnings. self.handleNode(node.target, node) - self.handleNode(node.annotation, node) + self.handleAnnotation(node.annotation, node) if node.value: # If the assignment has value, handle the *value* now. self.handleNode(node.value, node) + + def COMPARE(self, node): + literals = (ast.Str, ast.Num) + if not PY2: + literals += (ast.Bytes,) + + left = node.left + for op, right in zip(node.ops, node.comparators): + if (isinstance(op, (ast.Is, ast.IsNot)) and + (isinstance(left, literals) or isinstance(right, literals))): + self.report(messages.IsLiteral, node) + left = right + + self.handleChildren(node) diff -Nru pyflakes-1.6.0/pyflakes/__init__.py pyflakes-2.1.1/pyflakes/__init__.py --- pyflakes-1.6.0/pyflakes/__init__.py 2017-08-03 14:35:03.000000000 +0000 +++ pyflakes-2.1.1/pyflakes/__init__.py 2019-02-28 19:15:13.000000000 +0000 @@ -1 +1 @@ -__version__ = '1.6.0' +__version__ = '2.1.1' diff -Nru pyflakes-1.6.0/pyflakes/__main__.py pyflakes-2.1.1/pyflakes/__main__.py --- pyflakes-1.6.0/pyflakes/__main__.py 2014-09-23 14:08:41.000000000 +0000 +++ pyflakes-2.1.1/pyflakes/__main__.py 2019-02-28 19:10:37.000000000 +0000 @@ -1,5 +1,5 @@ from pyflakes.api import main -# python -m pyflakes (with Python >= 2.7) +# python -m pyflakes if __name__ == '__main__': main(prog='pyflakes') diff -Nru pyflakes-1.6.0/pyflakes/messages.py pyflakes-2.1.1/pyflakes/messages.py --- pyflakes-1.6.0/pyflakes/messages.py 2016-12-30 15:09:43.000000000 +0000 +++ pyflakes-2.1.1/pyflakes/messages.py 2019-02-28 19:13:32.000000000 +0000 @@ -100,12 +100,19 @@ class UndefinedLocal(Message): - message = ('local variable %r (defined in enclosing scope on line %r) ' - 'referenced before assignment') + message = 'local variable %r {0} referenced before assignment' + + default = 'defined in enclosing scope on line %r' + builtin = 'defined as a builtin' def __init__(self, filename, loc, name, orig_loc): Message.__init__(self, filename, loc) - self.message_args = (name, orig_loc.lineno) + if orig_loc is None: + self.message = self.message.format(self.builtin) + self.message_args = name + else: + self.message = self.message.format(self.default) + self.message_args = (name, orig_loc.lineno) class DuplicateArgument(Message): @@ -231,3 +238,31 @@ Assertion test is a tuple, which are always True. """ message = 'assertion is always true, perhaps remove parentheses?' + + +class ForwardAnnotationSyntaxError(Message): + message = 'syntax error in forward annotation %r' + + def __init__(self, filename, loc, annotation): + Message.__init__(self, filename, loc) + self.message_args = (annotation,) + + +class CommentAnnotationSyntaxError(Message): + message = 'syntax error in type comment %r' + + def __init__(self, filename, loc, annotation): + Message.__init__(self, filename, loc) + self.message_args = (annotation,) + + +class RaiseNotImplemented(Message): + message = "'raise NotImplemented' should be 'raise NotImplementedError'" + + +class InvalidPrintSyntax(Message): + message = 'use of >> is invalid with print function' + + +class IsLiteral(Message): + message = 'use ==/!= to compare str, bytes, and int literals' diff -Nru pyflakes-1.6.0/pyflakes/reporter.py pyflakes-2.1.1/pyflakes/reporter.py --- pyflakes-1.6.0/pyflakes/reporter.py 2015-11-03 13:18:49.000000000 +0000 +++ pyflakes-2.1.1/pyflakes/reporter.py 2019-02-28 19:10:37.000000000 +0000 @@ -53,15 +53,16 @@ """ line = text.splitlines()[-1] if offset is not None: - offset = offset - (len(text) - len(line)) + if sys.version_info < (3, 8): + offset = offset - (len(text) - len(line)) + 1 self._stderr.write('%s:%d:%d: %s\n' % - (filename, lineno, offset + 1, msg)) + (filename, lineno, offset, msg)) else: self._stderr.write('%s:%d: %s\n' % (filename, lineno, msg)) self._stderr.write(line) self._stderr.write('\n') if offset is not None: - self._stderr.write(re.sub(r'\S', ' ', line[:offset]) + + self._stderr.write(re.sub(r'\S', ' ', line[:offset - 1]) + "^\n") def flake(self, message): diff -Nru pyflakes-1.6.0/pyflakes/test/harness.py pyflakes-2.1.1/pyflakes/test/harness.py --- pyflakes-1.6.0/pyflakes/test/harness.py 2015-11-17 13:16:01.000000000 +0000 +++ pyflakes-2.1.1/pyflakes/test/harness.py 2019-02-28 19:10:37.000000000 +0000 @@ -1,5 +1,4 @@ - -import sys +import ast import textwrap import unittest @@ -7,13 +6,8 @@ __all__ = ['TestCase', 'skip', 'skipIf'] -if sys.version_info < (2, 7): - skip = lambda why: (lambda func: 'skip') # not callable - skipIf = lambda cond, why: (skip(why) if cond else lambda func: func) -else: - skip = unittest.skip - skipIf = unittest.skipIf -PyCF_ONLY_AST = 1024 +skip = unittest.skip +skipIf = unittest.skipIf class TestCase(unittest.TestCase): @@ -21,8 +15,14 @@ withDoctest = False def flakes(self, input, *expectedOutputs, **kw): - tree = compile(textwrap.dedent(input), "", "exec", PyCF_ONLY_AST) - w = checker.Checker(tree, withDoctest=self.withDoctest, **kw) + tree = ast.parse(textwrap.dedent(input)) + file_tokens = checker.make_tokens(textwrap.dedent(input)) + if kw.get('is_segment'): + tree = tree.body[0] + kw.pop('is_segment') + w = checker.Checker( + tree, file_tokens=file_tokens, withDoctest=self.withDoctest, **kw + ) outputs = [type(o) for o in w.messages] expectedOutputs = list(expectedOutputs) outputs.sort(key=lambda t: t.__name__) diff -Nru pyflakes-1.6.0/pyflakes/test/test_api.py pyflakes-2.1.1/pyflakes/test/test_api.py --- pyflakes-1.6.0/pyflakes/test/test_api.py 2017-06-21 14:37:06.000000000 +0000 +++ pyflakes-2.1.1/pyflakes/test/test_api.py 2019-02-28 19:10:37.000000000 +0000 @@ -2,6 +2,7 @@ Tests for L{pyflakes.scripts.pyflakes}. """ +import contextlib import os import sys import shutil @@ -148,8 +149,7 @@ def makeEmptyFile(self, *parts): assert parts fpath = os.path.join(self.tempdir, *parts) - fd = open(fpath, 'a') - fd.close() + open(fpath, 'a').close() return fpath def test_emptyDirectory(self): @@ -180,6 +180,7 @@ """ os.mkdir(os.path.join(self.tempdir, 'foo')) apath = self.makeEmptyFile('foo', 'a.py') + self.makeEmptyFile('foo', 'a.py~') os.mkdir(os.path.join(self.tempdir, 'bar')) bpath = self.makeEmptyFile('bar', 'b.py') cpath = self.makeEmptyFile('c.py') @@ -256,7 +257,9 @@ """ err = StringIO() reporter = Reporter(None, err) - reporter.syntaxError('foo.py', 'a problem', 3, 7, 'bad line of source') + reporter.syntaxError('foo.py', 'a problem', 3, + 8 if sys.version_info >= (3, 8) else 7, + 'bad line of source') self.assertEqual( ("foo.py:3:8: a problem\n" "bad line of source\n" @@ -291,10 +294,11 @@ reporter = Reporter(None, err) reporter.syntaxError('foo.py', 'a problem', 3, len(lines[0]) + 7, '\n'.join(lines)) + column = 25 if sys.version_info >= (3, 8) else 7 self.assertEqual( - ("foo.py:3:7: a problem\n" + + ("foo.py:3:%d: a problem\n" % column + lines[-1] + "\n" + - " ^\n"), + " " * (column - 1) + "^\n"), err.getvalue()) def test_unexpectedError(self): @@ -323,17 +327,20 @@ Tests for L{check} and L{checkPath} which check a file for flakes. """ + @contextlib.contextmanager def makeTempFile(self, content): """ Make a temporary file containing C{content} and return a path to it. """ - _, fpath = tempfile.mkstemp() - if not hasattr(content, 'decode'): - content = content.encode('ascii') - fd = open(fpath, 'wb') - fd.write(content) - fd.close() - return fpath + fd, name = tempfile.mkstemp() + try: + with os.fdopen(fd, 'wb') as f: + if not hasattr(content, 'decode'): + content = content.encode('ascii') + f.write(content) + yield name + finally: + os.remove(name) def assertHasErrors(self, path, errorList): """ @@ -371,8 +378,8 @@ exception to be raised nor an error indicator to be returned by L{check}. """ - fName = self.makeTempFile("def foo():\n\tpass\n\t") - self.assertHasErrors(fName, []) + with self.makeTempFile("def foo():\n\tpass\n\t") as fName: + self.assertHasErrors(fName, []) def test_checkPathNonExisting(self): """ @@ -414,56 +421,56 @@ else: self.fail() - sourcePath = self.makeTempFile(source) - - if PYPY: - message = 'EOF while scanning triple-quoted string literal' - else: - message = 'invalid syntax' + with self.makeTempFile(source) as sourcePath: + if PYPY: + message = 'EOF while scanning triple-quoted string literal' + else: + message = 'invalid syntax' - self.assertHasErrors( - sourcePath, - ["""\ -%s:8:11: %s + column = 8 if sys.version_info >= (3, 8) else 11 + self.assertHasErrors( + sourcePath, + ["""\ +%s:8:%d: %s '''quux''' - ^ -""" % (sourcePath, message)]) +%s^ +""" % (sourcePath, column, message, ' ' * (column - 1))]) def test_eofSyntaxError(self): """ The error reported for source files which end prematurely causing a syntax error reflects the cause for the syntax error. """ - sourcePath = self.makeTempFile("def foo(") - if PYPY: - result = """\ + with self.makeTempFile("def foo(") as sourcePath: + if PYPY: + result = """\ %s:1:7: parenthesis is never closed def foo( ^ """ % (sourcePath,) - else: - result = """\ + else: + result = """\ %s:1:9: unexpected EOF while parsing def foo( ^ """ % (sourcePath,) - self.assertHasErrors( - sourcePath, - [result]) + self.assertHasErrors( + sourcePath, + [result]) def test_eofSyntaxErrorWithTab(self): """ The error reported for source files which end prematurely causing a syntax error reflects the cause for the syntax error. """ - sourcePath = self.makeTempFile("if True:\n\tfoo =") - column = 5 if PYPY else 7 - last_line = '\t ^' if PYPY else '\t ^' - - self.assertHasErrors( - sourcePath, - ["""\ + with self.makeTempFile("if True:\n\tfoo =") as sourcePath: + column = 5 if PYPY else 7 + last_line = '\t ^' if PYPY else '\t ^' + + self.assertHasErrors( + sourcePath, + ["""\ %s:2:%s: invalid syntax \tfoo = %s @@ -479,15 +486,19 @@ def foo(bar=baz, bax): pass """ - sourcePath = self.makeTempFile(source) - last_line = ' ^\n' if ERROR_HAS_LAST_LINE else '' - column = '8:' if ERROR_HAS_COL_NUM else '' - self.assertHasErrors( - sourcePath, - ["""\ + with self.makeTempFile(source) as sourcePath: + if ERROR_HAS_LAST_LINE: + column = 9 if sys.version_info >= (3, 8) else 8 + last_line = ' ' * (column - 1) + '^\n' + columnstr = '%d:' % column + else: + last_line = columnstr = '' + self.assertHasErrors( + sourcePath, + ["""\ %s:1:%s non-default argument follows default argument def foo(bar=baz, bax): -%s""" % (sourcePath, column, last_line)]) +%s""" % (sourcePath, columnstr, last_line)]) def test_nonKeywordAfterKeywordSyntaxError(self): """ @@ -498,21 +509,25 @@ source = """\ foo(bar=baz, bax) """ - sourcePath = self.makeTempFile(source) - last_line = ' ^\n' if ERROR_HAS_LAST_LINE else '' - column = '13:' if ERROR_HAS_COL_NUM or PYPY else '' + with self.makeTempFile(source) as sourcePath: + if ERROR_HAS_LAST_LINE: + column = 14 if sys.version_info >= (3, 8) else 13 + last_line = ' ' * (column - 1) + '^\n' + columnstr = '%d:' % column + else: + last_line = columnstr = '' - if sys.version_info >= (3, 5): - message = 'positional argument follows keyword argument' - else: - message = 'non-keyword arg after keyword arg' + if sys.version_info >= (3, 5): + message = 'positional argument follows keyword argument' + else: + message = 'non-keyword arg after keyword arg' - self.assertHasErrors( - sourcePath, - ["""\ + self.assertHasErrors( + sourcePath, + ["""\ %s:1:%s %s foo(bar=baz, bax) -%s""" % (sourcePath, column, message, last_line)]) +%s""" % (sourcePath, columnstr, message, last_line)]) def test_invalidEscape(self): """ @@ -520,27 +535,32 @@ """ ver = sys.version_info # ValueError: invalid \x escape - sourcePath = self.makeTempFile(r"foo = '\xyz'") - if ver < (3,): - decoding_error = "%s: problem decoding source\n" % (sourcePath,) - elif PYPY: - # pypy3 only - decoding_error = """\ -%s:1:6: %s: ('unicodeescape', b'\\\\xyz', 0, 2, 'truncated \\\\xXX escape') -foo = '\\xyz' - ^ -""" % (sourcePath, 'UnicodeDecodeError') - else: - last_line = ' ^\n' if ERROR_HAS_LAST_LINE else '' - # Column has been "fixed" since 3.2.4 and 3.3.1 - col = 1 if ver >= (3, 3, 1) or ((3, 2, 4) <= ver < (3, 3)) else 2 - decoding_error = """\ -%s:1:7: (unicode error) 'unicodeescape' codec can't decode bytes \ + with self.makeTempFile(r"foo = '\xyz'") as sourcePath: + if ver < (3,): + decoding_error = "%s: problem decoding source\n" % (sourcePath,) + else: + position_end = 1 + if PYPY: + column = 6 + else: + column = 7 + # Column has been "fixed" since 3.2.4 and 3.3.1 + if ver < (3, 2, 4) or ver[:3] == (3, 3, 0): + position_end = 2 + + if ERROR_HAS_LAST_LINE: + last_line = '%s^\n' % (' ' * (column - 1)) + else: + last_line = '' + + decoding_error = """\ +%s:1:%d: (unicode error) 'unicodeescape' codec can't decode bytes \ in position 0-%d: truncated \\xXX escape foo = '\\xyz' -%s""" % (sourcePath, col, last_line) - self.assertHasErrors( - sourcePath, [decoding_error]) +%s""" % (sourcePath, column, position_end, last_line) + + self.assertHasErrors( + sourcePath, [decoding_error]) @skipIf(sys.platform == 'win32', 'unsupported on Windows') def test_permissionDenied(self): @@ -551,24 +571,24 @@ if os.getuid() == 0: self.skipTest('root user can access all files regardless of ' 'permissions') - sourcePath = self.makeTempFile('') - os.chmod(sourcePath, 0) - count, errors = self.getErrors(sourcePath) - self.assertEqual(count, 1) - self.assertEqual( - errors, - [('unexpectedError', sourcePath, "Permission denied")]) + with self.makeTempFile('') as sourcePath: + os.chmod(sourcePath, 0) + count, errors = self.getErrors(sourcePath) + self.assertEqual(count, 1) + self.assertEqual( + errors, + [('unexpectedError', sourcePath, "Permission denied")]) def test_pyflakesWarning(self): """ If the source file has a pyflakes warning, this is reported as a 'flake'. """ - sourcePath = self.makeTempFile("import foo") - count, errors = self.getErrors(sourcePath) - self.assertEqual(count, 1) - self.assertEqual( - errors, [('flake', str(UnusedImport(sourcePath, Node(1), 'foo')))]) + with self.makeTempFile("import foo") as sourcePath: + count, errors = self.getErrors(sourcePath) + self.assertEqual(count, 1) + self.assertEqual( + errors, [('flake', str(UnusedImport(sourcePath, Node(1), 'foo')))]) def test_encodedFileUTF8(self): """ @@ -579,15 +599,15 @@ # coding: utf-8 x = "%s" """ % SNOWMAN).encode('utf-8') - sourcePath = self.makeTempFile(source) - self.assertHasErrors(sourcePath, []) + with self.makeTempFile(source) as sourcePath: + self.assertHasErrors(sourcePath, []) def test_CRLFLineEndings(self): """ Source files with Windows CR LF line endings are parsed successfully. """ - sourcePath = self.makeTempFile("x = 42\r\n") - self.assertHasErrors(sourcePath, []) + with self.makeTempFile("x = 42\r\n") as sourcePath: + self.assertHasErrors(sourcePath, []) def test_misencodedFileUTF8(self): """ @@ -599,22 +619,21 @@ # coding: ascii x = "%s" """ % SNOWMAN).encode('utf-8') - sourcePath = self.makeTempFile(source) - - if PYPY and sys.version_info < (3, ): - message = ('\'ascii\' codec can\'t decode byte 0xe2 ' - 'in position 21: ordinal not in range(128)') - result = """\ + with self.makeTempFile(source) as sourcePath: + if PYPY and sys.version_info < (3, ): + message = ('\'ascii\' codec can\'t decode byte 0xe2 ' + 'in position 21: ordinal not in range(128)') + result = """\ %s:0:0: %s x = "\xe2\x98\x83" ^\n""" % (sourcePath, message) - else: - message = 'problem decoding source' - result = "%s: problem decoding source\n" % (sourcePath,) + else: + message = 'problem decoding source' + result = "%s: problem decoding source\n" % (sourcePath,) - self.assertHasErrors( - sourcePath, [result]) + self.assertHasErrors( + sourcePath, [result]) def test_misencodedFileUTF16(self): """ @@ -626,9 +645,9 @@ # coding: ascii x = "%s" """ % SNOWMAN).encode('utf-16') - sourcePath = self.makeTempFile(source) - self.assertHasErrors( - sourcePath, ["%s: problem decoding source\n" % (sourcePath,)]) + with self.makeTempFile(source) as sourcePath: + self.assertHasErrors( + sourcePath, ["%s: problem decoding source\n" % (sourcePath,)]) def test_checkRecursive(self): """ @@ -636,24 +655,25 @@ and reporting problems. """ tempdir = tempfile.mkdtemp() - os.mkdir(os.path.join(tempdir, 'foo')) - file1 = os.path.join(tempdir, 'foo', 'bar.py') - fd = open(file1, 'wb') - fd.write("import baz\n".encode('ascii')) - fd.close() - file2 = os.path.join(tempdir, 'baz.py') - fd = open(file2, 'wb') - fd.write("import contraband".encode('ascii')) - fd.close() - log = [] - reporter = LoggingReporter(log) - warnings = checkRecursive([tempdir], reporter) - self.assertEqual(warnings, 2) - self.assertEqual( - sorted(log), - sorted([('flake', str(UnusedImport(file1, Node(1), 'baz'))), - ('flake', - str(UnusedImport(file2, Node(1), 'contraband')))])) + try: + os.mkdir(os.path.join(tempdir, 'foo')) + file1 = os.path.join(tempdir, 'foo', 'bar.py') + with open(file1, 'wb') as fd: + fd.write("import baz\n".encode('ascii')) + file2 = os.path.join(tempdir, 'baz.py') + with open(file2, 'wb') as fd: + fd.write("import contraband".encode('ascii')) + log = [] + reporter = LoggingReporter(log) + warnings = checkRecursive([tempdir], reporter) + self.assertEqual(warnings, 2) + self.assertEqual( + sorted(log), + sorted([('flake', str(UnusedImport(file1, Node(1), 'baz'))), + ('flake', + str(UnusedImport(file2, Node(1), 'contraband')))])) + finally: + shutil.rmtree(tempdir) class IntegrationTests(TestCase): @@ -711,8 +731,7 @@ When a Python source file is all good, the return code is zero and no messages are printed to either stdout or stderr. """ - fd = open(self.tempfilepath, 'a') - fd.close() + open(self.tempfilepath, 'a').close() d = self.runPyflakes([self.tempfilepath]) self.assertEqual(d, ('', '', 0)) @@ -721,9 +740,8 @@ When a Python source file has warnings, the return code is non-zero and the warnings are printed to stdout. """ - fd = open(self.tempfilepath, 'wb') - fd.write("import contraband\n".encode('ascii')) - fd.close() + with open(self.tempfilepath, 'wb') as fd: + fd.write("import contraband\n".encode('ascii')) d = self.runPyflakes([self.tempfilepath]) expected = UnusedImport(self.tempfilepath, Node(1), 'contraband') self.assertEqual(d, ("%s%s" % (expected, os.linesep), '', 1)) @@ -745,13 +763,12 @@ exist, say), then the return code is non-zero and the errors are printed to stderr. """ - fd = open(self.tempfilepath, 'wb') - fd.write("import".encode('ascii')) - fd.close() + with open(self.tempfilepath, 'wb') as fd: + fd.write("import".encode('ascii')) d = self.runPyflakes([self.tempfilepath]) error_msg = '{0}:1:{2}: invalid syntax{1}import{1} {3}^{1}'.format( self.tempfilepath, os.linesep, 5 if PYPY else 7, '' if PYPY else ' ') - self.assertEqual(d, ('', error_msg, True)) + self.assertEqual(d, ('', error_msg, 1)) def test_readFromStdin(self): """ @@ -772,6 +789,8 @@ with SysStreamCapturing(stdin) as capture: main(args=paths) except SystemExit as e: - return (capture.output, capture.error, e.code) + self.assertIsInstance(e.code, bool) + rv = int(e.code) + return (capture.output, capture.error, rv) else: raise RuntimeError('SystemExit not raised') diff -Nru pyflakes-1.6.0/pyflakes/test/test_builtin.py pyflakes-2.1.1/pyflakes/test/test_builtin.py --- pyflakes-1.6.0/pyflakes/test/test_builtin.py 1970-01-01 00:00:00.000000000 +0000 +++ pyflakes-2.1.1/pyflakes/test/test_builtin.py 2019-02-28 19:10:37.000000000 +0000 @@ -0,0 +1,41 @@ +""" +Tests for detecting redefinition of builtins. +""" +from sys import version_info + +from pyflakes import messages as m +from pyflakes.test.harness import TestCase, skipIf + + +class TestBuiltins(TestCase): + + def test_builtin_unbound_local(self): + self.flakes(''' + def foo(): + a = range(1, 10) + range = a + return range + + foo() + + print(range) + ''', m.UndefinedLocal) + + def test_global_shadowing_builtin(self): + self.flakes(''' + def f(): + global range + range = None + print(range) + + f() + ''') + + @skipIf(version_info >= (3,), 'not an UnboundLocalError in Python 3') + def test_builtin_in_comprehension(self): + self.flakes(''' + def f(): + [range for range in range(1, 10)] + + f() + ''', m.UndefinedLocal) diff -Nru pyflakes-1.6.0/pyflakes/test/test_checker.py pyflakes-2.1.1/pyflakes/test/test_checker.py --- pyflakes-1.6.0/pyflakes/test/test_checker.py 1970-01-01 00:00:00.000000000 +0000 +++ pyflakes-2.1.1/pyflakes/test/test_checker.py 2019-02-28 19:10:37.000000000 +0000 @@ -0,0 +1,186 @@ +import ast +import sys + +from pyflakes import checker +from pyflakes.test.harness import TestCase, skipIf + + +class TypeableVisitorTests(TestCase): + """ + Tests of L{_TypeableVisitor} + """ + + @staticmethod + def _run_visitor(s): + """ + Run L{_TypeableVisitor} on the parsed source and return the visitor. + """ + tree = ast.parse(s) + visitor = checker._TypeableVisitor() + visitor.visit(tree) + return visitor + + def test_node_types(self): + """ + Test that the typeable node types are collected + """ + visitor = self._run_visitor( + """\ +x = 1 # assignment +for x in range(1): pass # for loop +def f(): pass # function definition +with a as b: pass # with statement +""" + ) + self.assertEqual(visitor.typeable_lines, [1, 2, 3, 4]) + self.assertIsInstance(visitor.typeable_nodes[1], ast.Assign) + self.assertIsInstance(visitor.typeable_nodes[2], ast.For) + self.assertIsInstance(visitor.typeable_nodes[3], ast.FunctionDef) + self.assertIsInstance(visitor.typeable_nodes[4], ast.With) + + def test_visitor_recurses(self): + """ + Test the common pitfall of missing `generic_visit` in visitors by + ensuring that nested nodes are reported + """ + visitor = self._run_visitor( + """\ +def f(): + x = 1 +""" + ) + self.assertEqual(visitor.typeable_lines, [1, 2]) + self.assertIsInstance(visitor.typeable_nodes[1], ast.FunctionDef) + self.assertIsInstance(visitor.typeable_nodes[2], ast.Assign) + + @skipIf(sys.version_info < (3, 5), 'async syntax introduced in py35') + def test_py35_node_types(self): + """ + Test that the PEP 492 node types are collected + """ + visitor = self._run_visitor( + """\ +async def f(): # async def + async for x in y: pass # async for + async with a as b: pass # async with +""" + ) + self.assertEqual(visitor.typeable_lines, [1, 2, 3]) + self.assertIsInstance(visitor.typeable_nodes[1], ast.AsyncFunctionDef) + self.assertIsInstance(visitor.typeable_nodes[2], ast.AsyncFor) + self.assertIsInstance(visitor.typeable_nodes[3], ast.AsyncWith) + + def test_last_node_wins(self): + """ + Test that when two typeable nodes are present on a line, the last + typeable one wins. + """ + visitor = self._run_visitor('x = 1; y = 1') + # detected both assignable nodes + self.assertEqual(visitor.typeable_lines, [1, 1]) + # but the assignment to `y` wins + self.assertEqual(visitor.typeable_nodes[1].targets[0].id, 'y') + + +class CollectTypeCommentsTests(TestCase): + """ + Tests of L{_collect_type_comments} + """ + + @staticmethod + def _collect(s): + """ + Run L{_collect_type_comments} on the parsed source and return the + mapping from nodes to comments. The return value is converted to + a set: {(node_type, tuple of comments), ...} + """ + tree = ast.parse(s) + tokens = checker.make_tokens(s) + ret = checker._collect_type_comments(tree, tokens) + return {(type(k), tuple(s for _, s in v)) for k, v in ret.items()} + + def test_bytes(self): + """ + Test that the function works for binary source + """ + ret = self._collect(b'x = 1 # type: int') + self.assertSetEqual(ret, {(ast.Assign, ('# type: int',))}) + + def test_text(self): + """ + Test that the function works for text source + """ + ret = self._collect(u'x = 1 # type: int') + self.assertEqual(ret, {(ast.Assign, ('# type: int',))}) + + def test_non_type_comment_ignored(self): + """ + Test that a non-type comment is ignored + """ + ret = self._collect('x = 1 # noqa') + self.assertSetEqual(ret, set()) + + def test_type_comment_before_typeable(self): + """ + Test that a type comment before something typeable is ignored. + """ + ret = self._collect('# type: int\nx = 1') + self.assertSetEqual(ret, set()) + + def test_type_ignore_comment_ignored(self): + """ + Test that `# type: ignore` comments are not collected. + """ + ret = self._collect('x = 1 # type: ignore') + self.assertSetEqual(ret, set()) + + def test_type_ignore_with_other_things_ignored(self): + """ + Test that `# type: ignore` comments with more content are also not + collected. + """ + ret = self._collect('x = 1 # type: ignore # noqa') + self.assertSetEqual(ret, set()) + ret = self._collect('x = 1 #type:ignore#noqa') + self.assertSetEqual(ret, set()) + + def test_type_comment_with_extra_still_collected(self): + ret = self._collect('x = 1 # type: int # noqa') + self.assertSetEqual(ret, {(ast.Assign, ('# type: int # noqa',))}) + + def test_type_comment_without_whitespace(self): + ret = self._collect('x = 1 #type:int') + self.assertSetEqual(ret, {(ast.Assign, ('#type:int',))}) + + def test_type_comment_starts_with_word_ignore(self): + ret = self._collect('x = 1 # type: ignore[T]') + self.assertSetEqual(ret, {(ast.Assign, ('# type: ignore[T]',))}) + + def test_last_node_wins(self): + """ + Test that when two typeable nodes are present on a line, the last + typeable one wins. + """ + ret = self._collect('def f(): x = 1 # type: int') + self.assertSetEqual(ret, {(ast.Assign, ('# type: int',))}) + + def test_function_def_assigned_comments(self): + """ + Test that type comments for function arguments are all attributed to + the function definition. + """ + ret = self._collect( + """\ +def f( + a, # type: int + b, # type: str +): + # type: (...) -> None + pass +""" + ) + expected = {( + ast.FunctionDef, + ('# type: int', '# type: str', '# type: (...) -> None'), + )} + self.assertSetEqual(ret, expected) diff -Nru pyflakes-1.6.0/pyflakes/test/test_code_segment.py pyflakes-2.1.1/pyflakes/test/test_code_segment.py --- pyflakes-1.6.0/pyflakes/test/test_code_segment.py 1970-01-01 00:00:00.000000000 +0000 +++ pyflakes-2.1.1/pyflakes/test/test_code_segment.py 2019-02-28 19:10:37.000000000 +0000 @@ -0,0 +1,126 @@ +from pyflakes import messages as m +from pyflakes.checker import (FunctionScope, ClassScope, ModuleScope, + Argument, FunctionDefinition, Assignment) +from pyflakes.test.harness import TestCase + + +class TestCodeSegments(TestCase): + """ + Tests for segments of a module + """ + + def test_function_segment(self): + self.flakes(''' + def foo(): + def bar(): + pass + ''', is_segment=True) + + self.flakes(''' + def foo(): + def bar(): + x = 0 + ''', m.UnusedVariable, is_segment=True) + + def test_class_segment(self): + self.flakes(''' + class Foo: + class Bar: + pass + ''', is_segment=True) + + self.flakes(''' + class Foo: + def bar(): + x = 0 + ''', m.UnusedVariable, is_segment=True) + + def test_scope_class(self): + checker = self.flakes(''' + class Foo: + x = 0 + def bar(a, b=1, *d, **e): + pass + ''', is_segment=True) + + scopes = checker.deadScopes + module_scopes = [ + scope for scope in scopes if scope.__class__ is ModuleScope] + class_scopes = [ + scope for scope in scopes if scope.__class__ is ClassScope] + function_scopes = [ + scope for scope in scopes if scope.__class__ is FunctionScope] + + # Ensure module scope is not present because we are analysing + # the inner contents of Foo + self.assertEqual(len(module_scopes), 0) + self.assertEqual(len(class_scopes), 1) + self.assertEqual(len(function_scopes), 1) + + class_scope = class_scopes[0] + function_scope = function_scopes[0] + + self.assertIsInstance(class_scope, ClassScope) + self.assertIsInstance(function_scope, FunctionScope) + + self.assertIn('x', class_scope) + self.assertIn('bar', class_scope) + + self.assertIn('a', function_scope) + self.assertIn('b', function_scope) + self.assertIn('d', function_scope) + self.assertIn('e', function_scope) + + self.assertIsInstance(class_scope['bar'], FunctionDefinition) + self.assertIsInstance(class_scope['x'], Assignment) + + self.assertIsInstance(function_scope['a'], Argument) + self.assertIsInstance(function_scope['b'], Argument) + self.assertIsInstance(function_scope['d'], Argument) + self.assertIsInstance(function_scope['e'], Argument) + + def test_scope_function(self): + checker = self.flakes(''' + def foo(a, b=1, *d, **e): + def bar(f, g=1, *h, **i): + pass + ''', is_segment=True) + + scopes = checker.deadScopes + module_scopes = [ + scope for scope in scopes if scope.__class__ is ModuleScope] + function_scopes = [ + scope for scope in scopes if scope.__class__ is FunctionScope] + + # Ensure module scope is not present because we are analysing + # the inner contents of foo + self.assertEqual(len(module_scopes), 0) + self.assertEqual(len(function_scopes), 2) + + function_scope_foo = function_scopes[1] + function_scope_bar = function_scopes[0] + + self.assertIsInstance(function_scope_foo, FunctionScope) + self.assertIsInstance(function_scope_bar, FunctionScope) + + self.assertIn('a', function_scope_foo) + self.assertIn('b', function_scope_foo) + self.assertIn('d', function_scope_foo) + self.assertIn('e', function_scope_foo) + self.assertIn('bar', function_scope_foo) + + self.assertIn('f', function_scope_bar) + self.assertIn('g', function_scope_bar) + self.assertIn('h', function_scope_bar) + self.assertIn('i', function_scope_bar) + + self.assertIsInstance(function_scope_foo['bar'], FunctionDefinition) + self.assertIsInstance(function_scope_foo['a'], Argument) + self.assertIsInstance(function_scope_foo['b'], Argument) + self.assertIsInstance(function_scope_foo['d'], Argument) + self.assertIsInstance(function_scope_foo['e'], Argument) + + self.assertIsInstance(function_scope_bar['f'], Argument) + self.assertIsInstance(function_scope_bar['g'], Argument) + self.assertIsInstance(function_scope_bar['h'], Argument) + self.assertIsInstance(function_scope_bar['i'], Argument) diff -Nru pyflakes-1.6.0/pyflakes/test/test_dict.py pyflakes-2.1.1/pyflakes/test/test_dict.py --- pyflakes-1.6.0/pyflakes/test/test_dict.py 2016-09-01 19:49:06.000000000 +0000 +++ pyflakes-2.1.1/pyflakes/test/test_dict.py 2019-02-28 19:10:37.000000000 +0000 @@ -19,15 +19,11 @@ @skipIf(version_info < (3,), "bytes and strings with same 'value' are not equal in python3") - @skipIf(version_info[0:2] == (3, 2), - "python3.2 does not allow u"" literal string definition") def test_duplicate_keys_bytes_vs_unicode_py3(self): self.flakes("{b'a': 1, u'a': 2}") @skipIf(version_info < (3,), "bytes and strings with same 'value' are not equal in python3") - @skipIf(version_info[0:2] == (3, 2), - "python3.2 does not allow u"" literal string definition") def test_duplicate_values_bytes_vs_unicode_py3(self): self.flakes( "{1: b'a', 1: u'a'}", diff -Nru pyflakes-1.6.0/pyflakes/test/test_doctests.py pyflakes-2.1.1/pyflakes/test/test_doctests.py --- pyflakes-1.6.0/pyflakes/test/test_doctests.py 2016-03-01 14:04:37.000000000 +0000 +++ pyflakes-2.1.1/pyflakes/test/test_doctests.py 2019-02-28 19:10:37.000000000 +0000 @@ -33,7 +33,8 @@ line.startswith('except ') or line.startswith('finally:') or line.startswith('else:') or - line.startswith('elif ')): + line.startswith('elif ') or + (lines and lines[-1].startswith(('>>> @', '... @')))): line = "... %s" % line else: line = ">>> %s" % line @@ -327,7 +328,10 @@ m.DoctestSyntaxError).messages exc = exceptions[0] self.assertEqual(exc.lineno, 4) - self.assertEqual(exc.col, 26) + if sys.version_info >= (3, 8): + self.assertEqual(exc.col, 18) + else: + self.assertEqual(exc.col, 26) # PyPy error column offset is 0, # for the second and third line of the doctest @@ -340,7 +344,7 @@ self.assertEqual(exc.col, 16) exc = exceptions[2] self.assertEqual(exc.lineno, 6) - if PYPY: + if PYPY or sys.version_info >= (3, 8): self.assertEqual(exc.col, 13) else: self.assertEqual(exc.col, 18) @@ -354,7 +358,7 @@ """ ''', m.DoctestSyntaxError).messages[0] self.assertEqual(exc.lineno, 5) - if PYPY: + if PYPY or sys.version_info >= (3, 8): self.assertEqual(exc.col, 13) else: self.assertEqual(exc.col, 16) diff -Nru pyflakes-1.6.0/pyflakes/test/test_imports.py pyflakes-2.1.1/pyflakes/test/test_imports.py --- pyflakes-1.6.0/pyflakes/test/test_imports.py 2016-09-01 19:49:06.000000000 +0000 +++ pyflakes-2.1.1/pyflakes/test/test_imports.py 2019-02-28 19:10:37.000000000 +0000 @@ -1,4 +1,3 @@ - from sys import version_info from pyflakes import messages as m @@ -95,6 +94,13 @@ assert binding.source_statement == 'from __future__ import print_function' assert str(binding) == '__future__.print_function' + def test_unusedImport_underscore(self): + """ + The magic underscore var should be reported as unused when used as an + import alias. + """ + self.flakes('import fu as _', m.UnusedImport) + class Test(TestCase): @@ -570,12 +576,15 @@ ''') def test_redefinedByExcept(self): - as_exc = ', ' if version_info < (2, 6) else ' as ' + expected = [m.RedefinedWhileUnused] + if version_info >= (3,): + # The exc variable is unused inside the exception handler. + expected.append(m.UnusedVariable) self.flakes(''' import fu try: pass - except Exception%sfu: pass - ''' % as_exc, m.RedefinedWhileUnused) + except Exception as fu: pass + ''', *expected) def test_usedInRaise(self): self.flakes(''' @@ -878,6 +887,22 @@ fu.x ''') + def test_used_package_with_submodule_import_of_alias(self): + """ + Usage of package by alias marks submodule imports as used. + """ + self.flakes(''' + import foo as f + import foo.bar + f.bar.do_something() + ''') + + self.flakes(''' + import foo as f + import foo.bar.blah + f.bar.blah.do_something() + ''') + def test_unused_package_with_submodule_import(self): """ When a package and its submodule are imported, only report once. @@ -1059,19 +1084,41 @@ __all__ += ['c', 'd'] ''', m.UndefinedExport, m.UndefinedExport) - def test_unrecognizable(self): + def test_concatenationAssignment(self): """ - If C{__all__} is defined in a way that can't be recognized statically, - it is ignored. + The C{__all__} variable is defined through list concatenation. """ self.flakes(''' - import foo - __all__ = ["f" + "oo"] - ''', m.UnusedImport) + import sys + __all__ = ['a'] + ['b'] + ['c'] + ''', m.UndefinedExport, m.UndefinedExport, m.UndefinedExport, m.UnusedImport) + + def test_all_with_attributes(self): self.flakes(''' - import foo - __all__ = [] + ["foo"] - ''', m.UnusedImport) + from foo import bar + __all__ = [bar.__name__] + ''') + + def test_all_with_names(self): + # not actually valid, but shouldn't produce a crash + self.flakes(''' + from foo import bar + __all__ = [bar] + ''') + + def test_all_with_attributes_added(self): + self.flakes(''' + from foo import bar + from bar import baz + __all__ = [bar.__name__] + [baz.__name__] + ''') + + def test_all_mixed_attributes_and_strings(self): + self.flakes(''' + from foo import bar + from foo import baz + __all__ = ['bar', baz.__name__] + ''') def test_unboundExported(self): """ @@ -1090,12 +1137,13 @@ def test_importStarExported(self): """ - Do not report undefined if import * is used + Report undefined if import * is used """ self.flakes(''' - from foolib import * - __all__ = ["foo"] - ''', m.ImportStarUsed) + from math import * + __all__ = ['sin', 'cos'] + csc(1) + ''', m.ImportStarUsed, m.ImportStarUsage, m.ImportStarUsage, m.ImportStarUsage) def test_importStarNotExported(self): """Report unused import when not needed to satisfy __all__.""" @@ -1146,13 +1194,6 @@ return "hello" ''', m.UndefinedName) - -class Python26Tests(TestCase): - """ - Tests for checking of syntax which is valid in Python 2.6 and newer. - """ - - @skipIf(version_info < (2, 6), "Python >= 2.6 only") def test_usedAsClassDecorator(self): """ Using an imported name as a class decorator results in no warnings, diff -Nru pyflakes-1.6.0/pyflakes/test/test_is_literal.py pyflakes-2.1.1/pyflakes/test/test_is_literal.py --- pyflakes-1.6.0/pyflakes/test/test_is_literal.py 1970-01-01 00:00:00.000000000 +0000 +++ pyflakes-2.1.1/pyflakes/test/test_is_literal.py 2019-02-28 19:10:37.000000000 +0000 @@ -0,0 +1,200 @@ +from pyflakes.messages import IsLiteral +from pyflakes.test.harness import TestCase + + +class Test(TestCase): + def test_is_str(self): + self.flakes(""" + x = 'foo' + if x is 'foo': + pass + """, IsLiteral) + + def test_is_bytes(self): + self.flakes(""" + x = b'foo' + if x is b'foo': + pass + """, IsLiteral) + + def test_is_unicode(self): + self.flakes(""" + x = u'foo' + if x is u'foo': + pass + """, IsLiteral) + + def test_is_int(self): + self.flakes(""" + x = 10 + if x is 10: + pass + """, IsLiteral) + + def test_is_true(self): + self.flakes(""" + x = True + if x is True: + pass + """) + + def test_is_false(self): + self.flakes(""" + x = False + if x is False: + pass + """) + + def test_is_not_str(self): + self.flakes(""" + x = 'foo' + if x is not 'foo': + pass + """, IsLiteral) + + def test_is_not_bytes(self): + self.flakes(""" + x = b'foo' + if x is not b'foo': + pass + """, IsLiteral) + + def test_is_not_unicode(self): + self.flakes(""" + x = u'foo' + if x is not u'foo': + pass + """, IsLiteral) + + def test_is_not_int(self): + self.flakes(""" + x = 10 + if x is not 10: + pass + """, IsLiteral) + + def test_is_not_true(self): + self.flakes(""" + x = True + if x is not True: + pass + """) + + def test_is_not_false(self): + self.flakes(""" + x = False + if x is not False: + pass + """) + + def test_left_is_str(self): + self.flakes(""" + x = 'foo' + if 'foo' is x: + pass + """, IsLiteral) + + def test_left_is_bytes(self): + self.flakes(""" + x = b'foo' + if b'foo' is x: + pass + """, IsLiteral) + + def test_left_is_unicode(self): + self.flakes(""" + x = u'foo' + if u'foo' is x: + pass + """, IsLiteral) + + def test_left_is_int(self): + self.flakes(""" + x = 10 + if 10 is x: + pass + """, IsLiteral) + + def test_left_is_true(self): + self.flakes(""" + x = True + if True is x: + pass + """) + + def test_left_is_false(self): + self.flakes(""" + x = False + if False is x: + pass + """) + + def test_left_is_not_str(self): + self.flakes(""" + x = 'foo' + if 'foo' is not x: + pass + """, IsLiteral) + + def test_left_is_not_bytes(self): + self.flakes(""" + x = b'foo' + if b'foo' is not x: + pass + """, IsLiteral) + + def test_left_is_not_unicode(self): + self.flakes(""" + x = u'foo' + if u'foo' is not x: + pass + """, IsLiteral) + + def test_left_is_not_int(self): + self.flakes(""" + x = 10 + if 10 is not x: + pass + """, IsLiteral) + + def test_left_is_not_true(self): + self.flakes(""" + x = True + if True is not x: + pass + """) + + def test_left_is_not_false(self): + self.flakes(""" + x = False + if False is not x: + pass + """) + + def test_chained_operators_is_true(self): + self.flakes(""" + x = 5 + if x is True < 4: + pass + """) + + def test_chained_operators_is_str(self): + self.flakes(""" + x = 5 + if x is 'foo' < 4: + pass + """, IsLiteral) + + def test_chained_operators_is_true_end(self): + self.flakes(""" + x = 5 + if 4 < x is True: + pass + """) + + def test_chained_operators_is_str_end(self): + self.flakes(""" + x = 5 + if 4 < x is 'foo': + pass + """, IsLiteral) diff -Nru pyflakes-1.6.0/pyflakes/test/test_other.py pyflakes-2.1.1/pyflakes/test/test_other.py --- pyflakes-1.6.0/pyflakes/test/test_other.py 2017-06-21 14:37:02.000000000 +0000 +++ pyflakes-2.1.1/pyflakes/test/test_other.py 2019-02-28 19:10:37.000000000 +0000 @@ -81,7 +81,6 @@ (1 for a, b in [(1, 2)]) ''') - @skipIf(version_info < (2, 7), "Python >= 2.7 only") def test_redefinedInSetComprehension(self): """ Test that reusing a variable in a set comprehension does not raise @@ -111,7 +110,6 @@ {1 for a, b in [(1, 2)]} ''') - @skipIf(version_info < (2, 7), "Python >= 2.7 only") def test_redefinedInDictComprehension(self): """ Test that reusing a variable in a dict comprehension does not raise @@ -151,6 +149,25 @@ def a(): pass ''', m.RedefinedWhileUnused) + def test_redefinedUnderscoreFunction(self): + """ + Test that shadowing a function definition named with underscore doesn't + raise anything. + """ + self.flakes(''' + def _(): pass + def _(): pass + ''') + + def test_redefinedUnderscoreImportation(self): + """ + Test that shadowing an underscore importation raises a warning. + """ + self.flakes(''' + from .i18n import _ + def _(): pass + ''', m.RedefinedWhileUnused) + def test_redefinedClassFunction(self): """ Test that shadowing a function definition in a class suite with another @@ -260,7 +277,6 @@ a = classmethod(a) ''') - @skipIf(version_info < (2, 6), "Python >= 2.6 only") def test_modernProperty(self): self.flakes(""" class A: @@ -1143,6 +1159,37 @@ def g(): foo = 'anything'; foo.is_used() ''') + def test_function_arguments(self): + """ + Test to traverse ARG and ARGUMENT handler + """ + self.flakes(''' + def foo(a, b): + pass + ''') + + self.flakes(''' + def foo(a, b, c=0): + pass + ''') + + self.flakes(''' + def foo(a, b, c=0, *args): + pass + ''') + + self.flakes(''' + def foo(a, b, c=0, *args, **kwargs): + pass + ''') + + @skipIf(version_info < (3, 3), "Python >= 3.3 only") + def test_function_arguments_python3(self): + self.flakes(''' + def foo(a, b, c=0, *args, d=0, **kwargs): + pass + ''') + class TestUnusedAssignment(TestCase): """ @@ -1159,6 +1206,16 @@ b = 1 ''', m.UnusedVariable) + def test_unusedUnderscoreVariable(self): + """ + Don't warn when the magic "_" (underscore) variable is unused. + See issue #202. + """ + self.flakes(''' + def a(unused_param): + _ = unused_param + ''') + def test_unusedVariableAsLocals(self): """ Using locals() it is perfectly valid to have unused variables @@ -1565,7 +1622,6 @@ pass ''', m.UndefinedName) - @skipIf(version_info < (2, 7), "Python >= 2.7 only") def test_dictComprehension(self): """ Dict comprehensions are properly handled. @@ -1574,7 +1630,6 @@ a = {1: x for x in range(10)} ''') - @skipIf(version_info < (2, 7), "Python >= 2.7 only") def test_setComprehensionAndLiteral(self): """ Set comprehensions are properly handled. @@ -1585,17 +1640,31 @@ ''') def test_exceptionUsedInExcept(self): - as_exc = ', ' if version_info < (2, 6) else ' as ' self.flakes(''' try: pass - except Exception%se: e - ''' % as_exc) + except Exception as e: e + ''') + + self.flakes(''' + def download_review(): + try: pass + except Exception as e: e + ''') + + @skipIf(version_info < (3,), + "In Python 2 exception names stay bound after the exception handler") + def test_exceptionUnusedInExcept(self): + self.flakes(''' + try: pass + except Exception as e: pass + ''', m.UnusedVariable) + def test_exceptionUnusedInExceptInFunction(self): self.flakes(''' def download_review(): try: pass - except Exception%se: e - ''' % as_exc) + except Exception as e: pass + ''', m.UnusedVariable) def test_exceptWithoutNameInFunction(self): """ @@ -1720,6 +1789,14 @@ ''') @skipIf(version_info < (3, 5), 'new in Python 3.5') + def test_asyncForUnderscoreLoopVar(self): + self.flakes(''' + async def coro(it): + async for _ in it: + pass + ''') + + @skipIf(version_info < (3, 5), 'new in Python 3.5') def test_loopControlInAsyncFor(self): self.flakes(''' async def read_data(db): @@ -1809,68 +1886,69 @@ f'{hi} {mom}' ''') - @skipIf(version_info < (3, 6), 'new in Python 3.6') - def test_variable_annotations(self): + def test_raise_notimplemented(self): self.flakes(''' - name: str - age: int + raise NotImplementedError("This is fine") ''') + self.flakes(''' - name: str = 'Bob' - age: int = 18 + raise NotImplementedError ''') + self.flakes(''' - class C: - name: str - age: int - ''') + raise NotImplemented("This isn't gonna work") + ''', m.RaiseNotImplemented) + self.flakes(''' - class C: - name: str = 'Bob' - age: int = 18 - ''') + raise NotImplemented + ''', m.RaiseNotImplemented) + + +class TestIncompatiblePrintOperator(TestCase): + """ + Tests for warning about invalid use of print function. + """ + + def test_valid_print(self): self.flakes(''' - def f(): - name: str - age: int + print("Hello") ''') + + def test_invalid_print_when_imported_from_future(self): + exc = self.flakes(''' + from __future__ import print_function + import sys + print >>sys.stderr, "Hello" + ''', m.InvalidPrintSyntax).messages[0] + + self.assertEqual(exc.lineno, 4) + self.assertEqual(exc.col, 0) + + def test_print_function_assignment(self): + """ + A valid assignment, tested for catching false positives. + """ self.flakes(''' - def f(): - name: str = 'Bob' - age: int = 18 - foo: not_a_real_type = None - ''', m.UnusedVariable, m.UnusedVariable, m.UnusedVariable, m.UndefinedName) - self.flakes(''' - def f(): - name: str - print(name) - ''', m.UndefinedName) - self.flakes(''' - from typing import Any - def f(): - a: Any + from __future__ import print_function + log = print + log("Hello") ''') + + def test_print_in_lambda(self): self.flakes(''' - foo: not_a_real_type - ''', m.UndefinedName) - self.flakes(''' - foo: not_a_real_type = None - ''', m.UndefinedName) - self.flakes(''' - class C: - foo: not_a_real_type - ''', m.UndefinedName) - self.flakes(''' - class C: - foo: not_a_real_type = None - ''', m.UndefinedName) + from __future__ import print_function + a = lambda: print + ''') + + def test_print_returned_in_function(self): self.flakes(''' - def f(): - class C: - foo: not_a_real_type - ''', m.UndefinedName) + from __future__ import print_function + def a(): + return print + ''') + + def test_print_as_condition_test(self): self.flakes(''' - def f(): - class C: - foo: not_a_real_type = None - ''', m.UndefinedName) + from __future__ import print_function + if print: pass + ''') diff -Nru pyflakes-1.6.0/pyflakes/test/test_type_annotations.py pyflakes-2.1.1/pyflakes/test/test_type_annotations.py --- pyflakes-1.6.0/pyflakes/test/test_type_annotations.py 1970-01-01 00:00:00.000000000 +0000 +++ pyflakes-2.1.1/pyflakes/test/test_type_annotations.py 2019-02-28 19:10:37.000000000 +0000 @@ -0,0 +1,317 @@ +""" +Tests for behaviour related to type annotations. +""" + +from sys import version_info + +from pyflakes import messages as m +from pyflakes.test.harness import TestCase, skipIf + + +class TestTypeAnnotations(TestCase): + + def test_typingOverload(self): + """Allow intentional redefinitions via @typing.overload""" + self.flakes(""" + import typing + from typing import overload + + @overload + def f(s): # type: (None) -> None + pass + + @overload + def f(s): # type: (int) -> int + pass + + def f(s): + return s + + @typing.overload + def g(s): # type: (None) -> None + pass + + @typing.overload + def g(s): # type: (int) -> int + pass + + def g(s): + return s + """) + + def test_not_a_typing_overload(self): + """regression test for @typing.overload detection bug in 2.1.0""" + self.flakes(""" + x = lambda f: f + + @x + def t(): + pass + + y = lambda f: f + + @x + @y + def t(): + pass + + @x + @y + def t(): + pass + """, m.RedefinedWhileUnused, m.RedefinedWhileUnused) + + @skipIf(version_info < (3, 6), 'new in Python 3.6') + def test_variable_annotations(self): + self.flakes(''' + name: str + age: int + ''') + self.flakes(''' + name: str = 'Bob' + age: int = 18 + ''') + self.flakes(''' + class C: + name: str + age: int + ''') + self.flakes(''' + class C: + name: str = 'Bob' + age: int = 18 + ''') + self.flakes(''' + def f(): + name: str + age: int + ''') + self.flakes(''' + def f(): + name: str = 'Bob' + age: int = 18 + foo: not_a_real_type = None + ''', m.UnusedVariable, m.UnusedVariable, m.UnusedVariable, m.UndefinedName) + self.flakes(''' + def f(): + name: str + print(name) + ''', m.UndefinedName) + self.flakes(''' + from typing import Any + def f(): + a: Any + ''') + self.flakes(''' + foo: not_a_real_type + ''', m.UndefinedName) + self.flakes(''' + foo: not_a_real_type = None + ''', m.UndefinedName) + self.flakes(''' + class C: + foo: not_a_real_type + ''', m.UndefinedName) + self.flakes(''' + class C: + foo: not_a_real_type = None + ''', m.UndefinedName) + self.flakes(''' + def f(): + class C: + foo: not_a_real_type + ''', m.UndefinedName) + self.flakes(''' + def f(): + class C: + foo: not_a_real_type = None + ''', m.UndefinedName) + self.flakes(''' + from foo import Bar + bar: Bar + ''') + self.flakes(''' + from foo import Bar + bar: 'Bar' + ''') + self.flakes(''' + import foo + bar: foo.Bar + ''') + self.flakes(''' + import foo + bar: 'foo.Bar' + ''') + self.flakes(''' + from foo import Bar + def f(bar: Bar): pass + ''') + self.flakes(''' + from foo import Bar + def f(bar: 'Bar'): pass + ''') + self.flakes(''' + from foo import Bar + def f(bar) -> Bar: return bar + ''') + self.flakes(''' + from foo import Bar + def f(bar) -> 'Bar': return bar + ''') + self.flakes(''' + bar: 'Bar' + ''', m.UndefinedName) + self.flakes(''' + bar: 'foo.Bar' + ''', m.UndefinedName) + self.flakes(''' + from foo import Bar + bar: str + ''', m.UnusedImport) + self.flakes(''' + from foo import Bar + def f(bar: str): pass + ''', m.UnusedImport) + self.flakes(''' + def f(a: A) -> A: pass + class A: pass + ''', m.UndefinedName, m.UndefinedName) + self.flakes(''' + def f(a: 'A') -> 'A': return a + class A: pass + ''') + self.flakes(''' + a: A + class A: pass + ''', m.UndefinedName) + self.flakes(''' + a: 'A' + class A: pass + ''') + self.flakes(''' + a: 'A B' + ''', m.ForwardAnnotationSyntaxError) + self.flakes(''' + a: 'A; B' + ''', m.ForwardAnnotationSyntaxError) + self.flakes(''' + a: '1 + 2' + ''') + self.flakes(''' + a: 'a: "A"' + ''', m.ForwardAnnotationSyntaxError) + + @skipIf(version_info < (3, 5), 'new in Python 3.5') + def test_annotated_async_def(self): + self.flakes(''' + class c: pass + async def func(c: c) -> None: pass + ''') + + @skipIf(version_info < (3, 7), 'new in Python 3.7') + def test_postponed_annotations(self): + self.flakes(''' + from __future__ import annotations + def f(a: A) -> A: pass + class A: + b: B + class B: pass + ''') + + self.flakes(''' + from __future__ import annotations + def f(a: A) -> A: pass + class A: + b: Undefined + class B: pass + ''', m.UndefinedName) + + def test_typeCommentsMarkImportsAsUsed(self): + self.flakes(""" + from mod import A, B, C, D, E, F, G + + + def f( + a, # type: A + ): + # type: (...) -> B + for b in a: # type: C + with b as c: # type: D + d = c.x # type: E + return d + + + def g(x): # type: (F) -> G + return x.y + """) + + def test_typeCommentsFullSignature(self): + self.flakes(""" + from mod import A, B, C, D + def f(a, b): + # type: (A, B[C]) -> D + return a + b + """) + + def test_typeCommentsStarArgs(self): + self.flakes(""" + from mod import A, B, C, D + def f(a, *b, **c): + # type: (A, *B, **C) -> D + return a + b + """) + + def test_typeCommentsFullSignatureWithDocstring(self): + self.flakes(''' + from mod import A, B, C, D + def f(a, b): + # type: (A, B[C]) -> D + """do the thing!""" + return a + b + ''') + + def test_typeCommentsAdditionalComemnt(self): + self.flakes(""" + from mod import F + + x = 1 # type: F # noqa + """) + + def test_typeCommentsNoWhitespaceAnnotation(self): + self.flakes(""" + from mod import F + + x = 1 #type:F + """) + + def test_typeCommentsInvalidDoesNotMarkAsUsed(self): + self.flakes(""" + from mod import F + + # type: F + """, m.UnusedImport) + + def test_typeCommentsSyntaxError(self): + self.flakes(""" + def f(x): # type: (F[) -> None + pass + """, m.CommentAnnotationSyntaxError) + + def test_typeCommentsSyntaxErrorCorrectLine(self): + checker = self.flakes("""\ + x = 1 + # type: definitely not a PEP 484 comment + """, m.CommentAnnotationSyntaxError) + self.assertEqual(checker.messages[0].lineno, 2) + + def test_typeCommentsAssignedToPreviousNode(self): + # This test demonstrates an issue in the implementation which + # associates the type comment with a node above it, however the type + # comment isn't valid according to mypy. If an improved approach + # which can detect these "invalid" type comments is implemented, this + # test should be removed / improved to assert that new check. + self.flakes(""" + from mod import F + x = 1 + # type: F + """) diff -Nru pyflakes-1.6.0/pyflakes/test/test_undefined_names.py pyflakes-2.1.1/pyflakes/test/test_undefined_names.py --- pyflakes-1.6.0/pyflakes/test/test_undefined_names.py 2016-05-06 16:40:47.000000000 +0000 +++ pyflakes-2.1.1/pyflakes/test/test_undefined_names.py 2019-02-28 19:10:37.000000000 +0000 @@ -1,5 +1,4 @@ - -from _ast import PyCF_ONLY_AST +import ast from sys import version_info from pyflakes import messages as m, checker @@ -25,15 +24,16 @@ @skipIf(version_info < (3,), 'in Python 2 exception names stay bound after the except: block') def test_undefinedExceptionName(self): - """Exception names can't be used after the except: block.""" + """Exception names can't be used after the except: block. + + The exc variable is unused inside the exception handler.""" self.flakes(''' try: raise ValueError('ve') except ValueError as exc: pass exc - ''', - m.UndefinedName) + ''', m.UndefinedName, m.UnusedVariable) def test_namesDeclaredInExceptBlocks(self): """Locals declared in except: blocks can be used after the block. @@ -71,7 +71,8 @@ """Exception names are unbound after the `except:` block. Last line will raise UnboundLocalError on Python 3 but would print out - 've' on Python 2.""" + 've' on Python 2. The exc variable is unused inside the exception + handler.""" self.flakes(''' try: raise ValueError('ve') @@ -79,14 +80,15 @@ pass print(exc) exc = 'Original value' - ''', - m.UndefinedName) + ''', m.UndefinedName, m.UnusedVariable) def test_undefinedExceptionNameObscuringLocalVariableFalsePositive1(self): """Exception names obscure locals, can't be used after. Unless. Last line will never raise UnboundLocalError because it's only entered if no exception was raised.""" + # The exc variable is unused inside the exception handler. + expected = [] if version_info < (3,) else [m.UnusedVariable] self.flakes(''' exc = 'Original value' try: @@ -95,7 +97,7 @@ print('exception logged') raise exc - ''') + ''', *expected) def test_delExceptionInExcept(self): """The exception name can be deleted in the except: block.""" @@ -111,6 +113,8 @@ Last line will never raise UnboundLocalError because `error` is only falsy if the `except:` block has not been entered.""" + # The exc variable is unused inside the exception handler. + expected = [] if version_info < (3,) else [m.UnusedVariable] self.flakes(''' exc = 'Original value' error = None @@ -122,7 +126,7 @@ print(error) else: exc - ''') + ''', *expected) @skip('error reporting disabled due to false positives below') def test_undefinedExceptionNameObscuringGlobalVariable(self): @@ -168,6 +172,8 @@ Last line will never raise NameError because it's only entered if no exception was raised.""" + # The exc variable is unused inside the exception handler. + expected = [] if version_info < (3,) else [m.UnusedVariable] self.flakes(''' exc = 'Original value' def func(): @@ -178,13 +184,15 @@ print('exception logged') raise exc - ''') + ''', *expected) def test_undefinedExceptionNameObscuringGlobalVariableFalsePositive2(self): """Exception names obscure globals, can't be used after. Unless. Last line will never raise NameError because `error` is only falsy if the `except:` block has not been entered.""" + # The exc variable is unused inside the exception handler. + expected = [] if version_info < (3,) else [m.UnusedVariable] self.flakes(''' exc = 'Original value' def func(): @@ -198,7 +206,7 @@ print(error) else: exc - ''') + ''', *expected) def test_functionsNeedGlobalScope(self): self.flakes(''' @@ -218,6 +226,14 @@ """ self.flakes('WindowsError') + @skipIf(version_info < (3, 6), 'new feature in 3.6') + def test_moduleAnnotations(self): + """ + Use of the C{__annotations__} in module scope should not emit + an undefined name warning when version is greater than or equal to 3.6. + """ + self.flakes('__annotations__') + def test_magicGlobalsFile(self): """ Use of the C{__file__} magic global should not emit an undefined name @@ -247,6 +263,22 @@ self.flakes('__path__', m.UndefinedName) self.flakes('__path__', filename='package/__init__.py') + def test_magicModuleInClassScope(self): + """ + Use of the C{__module__} magic builtin should not emit an undefined + name warning if used in class scope. + """ + self.flakes('__module__', m.UndefinedName) + self.flakes(''' + class Foo: + __module__ + ''') + self.flakes(''' + class Foo: + def bar(self): + __module__ + ''', m.UndefinedName) + def test_globalImportStar(self): """Can't find undefined names with import *.""" self.flakes('from fu import *; bar', @@ -719,14 +751,13 @@ B = dict((i, str(i)) for i in T) ''') - if version_info >= (2, 7): - self.flakes(''' - class A: - T = range(10) - - X = {x for x in T} - Y = {x:x for x in T} - ''') + self.flakes(''' + class A: + T = range(10) + + X = {x for x in T} + Y = {x:x for x in T} + ''') def test_definedInClassNested(self): """Defined name for nested generator expressions in a class.""" @@ -752,7 +783,6 @@ (42 for i in range(i)) ''', m.UndefinedName) - @skipIf(version_info < (2, 7), 'Dictionary comprehensions do not exist') def test_definedFromLambdaInDictionaryComprehension(self): """ Defined name referenced from a lambda function within a dict/set @@ -771,7 +801,6 @@ any(lambda: id(x) for x in range(10)) ''') - @skipIf(version_info < (2, 7), 'Dictionary comprehensions do not exist') def test_undefinedFromLambdaInDictionaryComprehension(self): """ Undefined name referenced from a lambda function within a dict/set @@ -790,6 +819,24 @@ any(lambda: id(y) for x in range(10)) ''', m.UndefinedName) + def test_dunderClass(self): + """ + `__class__` is defined in class scope under Python 3, but is not + in Python 2. + """ + code = ''' + class Test(object): + def __init__(self): + print(__class__.__name__) + self.x = 1 + + t = Test() + ''' + if version_info < (3,): + self.flakes(code, m.UndefinedName) + else: + self.flakes(code) + class NameTests(TestCase): """ @@ -800,7 +847,8 @@ A Name node with an unrecognized context results in a RuntimeError being raised. """ - tree = compile("x = 10", "", "exec", PyCF_ONLY_AST) + tree = ast.parse("x = 10") + file_tokens = checker.make_tokens("x = 10") # Make it into something unrecognizable. tree.body[0].targets[0].ctx = object() - self.assertRaises(RuntimeError, checker.Checker, tree) + self.assertRaises(RuntimeError, checker.Checker, tree, file_tokens=file_tokens) diff -Nru pyflakes-1.6.0/pyflakes.egg-info/PKG-INFO pyflakes-2.1.1/pyflakes.egg-info/PKG-INFO --- pyflakes-1.6.0/pyflakes.egg-info/PKG-INFO 2017-08-03 14:54:11.000000000 +0000 +++ pyflakes-2.1.1/pyflakes.egg-info/PKG-INFO 2019-02-28 19:25:22.000000000 +0000 @@ -1,6 +1,6 @@ -Metadata-Version: 1.1 +Metadata-Version: 1.2 Name: pyflakes -Version: 1.6.0 +Version: 2.1.1 Summary: passive checker of Python programs Home-page: https://github.com/PyCQA/pyflakes Author: A lot of people @@ -16,8 +16,8 @@ parsing the source file, not importing it, so it is safe to use on modules with side effects. It's also much faster. - It is `available on PyPI `_ - and it supports all active versions of Python from 2.5 to 3.6. + It is `available on PyPI `_ + and it supports all active versions of Python: 2.7 and 3.4 to 3.7. @@ -40,7 +40,7 @@ * If you require more options and more flexibility, you could give a look to Flake8_ too. - + Design Principles ----------------- @@ -85,12 +85,17 @@ :alt: Build status .. _Pylint: http://www.pylint.org/ - .. _flake8: https://pypi.python.org/pypi/flake8 + .. _flake8: https://pypi.org/project/flake8/ .. _`PEP 8`: http://legacy.python.org/dev/peps/pep-0008/ .. _Pychecker: http://pychecker.sourceforge.net/ .. _`rebase your changes`: https://git-scm.com/book/en/v2/Git-Branching-Rebasing .. _`GitHub pull request`: https://github.com/PyCQA/pyflakes/pulls + Changelog + --------- + + Please see `NEWS.rst `_. + Platform: UNKNOWN Classifier: Development Status :: 6 - Mature Classifier: Environment :: Console @@ -98,6 +103,14 @@ Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development Classifier: Topic :: Utilities +Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* diff -Nru pyflakes-1.6.0/pyflakes.egg-info/SOURCES.txt pyflakes-2.1.1/pyflakes.egg-info/SOURCES.txt --- pyflakes-1.6.0/pyflakes.egg-info/SOURCES.txt 2017-08-03 14:54:11.000000000 +0000 +++ pyflakes-2.1.1/pyflakes.egg-info/SOURCES.txt 2019-02-28 19:25:22.000000000 +0000 @@ -1,7 +1,7 @@ AUTHORS LICENSE MANIFEST.in -NEWS.txt +NEWS.rst README.rst setup.cfg setup.py @@ -22,9 +22,14 @@ pyflakes/test/__init__.py pyflakes/test/harness.py pyflakes/test/test_api.py +pyflakes/test/test_builtin.py +pyflakes/test/test_checker.py +pyflakes/test/test_code_segment.py pyflakes/test/test_dict.py pyflakes/test/test_doctests.py pyflakes/test/test_imports.py +pyflakes/test/test_is_literal.py pyflakes/test/test_other.py pyflakes/test/test_return_with_arguments_inside_generator.py +pyflakes/test/test_type_annotations.py pyflakes/test/test_undefined_names.py \ No newline at end of file diff -Nru pyflakes-1.6.0/README.rst pyflakes-2.1.1/README.rst --- pyflakes-1.6.0/README.rst 2017-08-03 14:28:59.000000000 +0000 +++ pyflakes-2.1.1/README.rst 2019-02-28 19:10:37.000000000 +0000 @@ -8,8 +8,8 @@ parsing the source file, not importing it, so it is safe to use on modules with side effects. It's also much faster. -It is `available on PyPI `_ -and it supports all active versions of Python from 2.5 to 3.6. +It is `available on PyPI `_ +and it supports all active versions of Python: 2.7 and 3.4 to 3.7. @@ -32,7 +32,7 @@ * If you require more options and more flexibility, you could give a look to Flake8_ too. - + Design Principles ----------------- @@ -77,8 +77,13 @@ :alt: Build status .. _Pylint: http://www.pylint.org/ -.. _flake8: https://pypi.python.org/pypi/flake8 +.. _flake8: https://pypi.org/project/flake8/ .. _`PEP 8`: http://legacy.python.org/dev/peps/pep-0008/ .. _Pychecker: http://pychecker.sourceforge.net/ .. _`rebase your changes`: https://git-scm.com/book/en/v2/Git-Branching-Rebasing .. _`GitHub pull request`: https://github.com/PyCQA/pyflakes/pulls + +Changelog +--------- + +Please see `NEWS.rst `_. diff -Nru pyflakes-1.6.0/setup.cfg pyflakes-2.1.1/setup.cfg --- pyflakes-1.6.0/setup.cfg 2017-08-03 14:54:11.000000000 +0000 +++ pyflakes-2.1.1/setup.cfg 2019-02-28 19:25:22.000000000 +0000 @@ -1,6 +1,9 @@ [bdist_wheel] universal = 1 +[metadata] +license_file = LICENSE + [egg_info] tag_build = tag_date = 0 diff -Nru pyflakes-1.6.0/setup.py pyflakes-2.1.1/setup.py --- pyflakes-1.6.0/setup.py 2016-12-30 15:09:43.000000000 +0000 +++ pyflakes-2.1.1/setup.py 2019-02-28 19:10:37.000000000 +0000 @@ -44,6 +44,7 @@ author_email="code-quality@python.org", url="https://github.com/PyCQA/pyflakes", packages=["pyflakes", "pyflakes.scripts", "pyflakes.test"], + python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', classifiers=[ "Development Status :: 6 - Mature", "Environment :: Console", @@ -51,7 +52,14 @@ "License :: OSI Approved :: MIT License", "Programming Language :: Python", "Programming Language :: Python :: 2", + "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", "Topic :: Software Development", "Topic :: Utilities", ],