diff -Nru python-flake8-3.7.9/debian/changelog python-flake8-3.8.3/debian/changelog --- python-flake8-3.7.9/debian/changelog 2020-02-15 10:08:53.000000000 -0800 +++ python-flake8-3.8.3/debian/changelog 2020-07-02 08:48:13.000000000 -0700 @@ -1,3 +1,22 @@ +python-flake8 (3.8.3-1~20.04.2) focal; urgency=medium + + * Backport to focal. (LP: #1883175) + * Drop debhelper compat level to 12 + + -- Ted Kern Thu, 02 Jul 2020 08:48:13 -0700 + +python-flake8 (3.8.3-1) unstable; urgency=medium + + * New upstream release. + * Rebase patches. + * Bump required version of pyflakes and pycodestyle. + * Use pytest to run tests during build. + * Bump debhelper compat level to 13. + * Bump standards version to 4.5.0. + * Set Rules-Requires-Root: no. + + -- Ondřej Nový Fri, 12 Jun 2020 12:26:38 +0200 + python-flake8 (3.7.9-2) unstable; urgency=medium * Drop python2 support; Closes: #937757 diff -Nru python-flake8-3.7.9/debian/control python-flake8-3.8.3/debian/control --- python-flake8-3.7.9/debian/control 2020-02-15 10:08:53.000000000 -0800 +++ python-flake8-3.8.3/debian/control 2020-07-02 08:48:13.000000000 -0700 @@ -13,22 +13,22 @@ python3-mccabe (>= 0.6.0), python3-mock (>= 2.0.0), python3-nose, - python3-pycodestyle (>= 2.5.0), - python3-pyflakes (>= 2.1.0), + python3-pycodestyle (>= 2.6.0), + python3-pyflakes (>= 2.2.0), python3-pytest, python3-pytest-runner, python3-setuptools (>= 30), -Standards-Version: 4.4.1 +Standards-Version: 4.5.0 Homepage: https://gitlab.com/pycqa/flake8 Testsuite: autopkgtest-pkg-python Vcs-Git: https://salsa.debian.org/python-team/modules/python-flake8.git Vcs-Browser: https://salsa.debian.org/python-team/modules/python-flake8 +Rules-Requires-Root: no Package: flake8 Architecture: all Depends: python3-flake8 (=${binary:Version}), python3-pkg-resources, - python3-pycodestyle (>= 2.5.0), ${misc:Depends}, ${python3:Depends}, Replaces: python-flake8 (<< 2.5.4-1), @@ -44,7 +44,8 @@ Package: python3-flake8 Architecture: all Depends: python3-mccabe (>= 0.6.0), - python3-pyflakes (>= 2.1.0), + python3-pycodestyle (>= 2.6.0), + python3-pyflakes (>= 2.2.0), python3-setuptools (>= 30), ${misc:Depends}, ${python3:Depends}, diff -Nru python-flake8-3.7.9/debian/patches/0001-Remove-upper-constrains-from-upstream-requirements.patch python-flake8-3.8.3/debian/patches/0001-Remove-upper-constrains-from-upstream-requirements.patch --- python-flake8-3.7.9/debian/patches/0001-Remove-upper-constrains-from-upstream-requirements.patch 2020-02-15 10:08:53.000000000 -0800 +++ python-flake8-3.8.3/debian/patches/0001-Remove-upper-constrains-from-upstream-requirements.patch 2020-07-02 08:48:13.000000000 -0700 @@ -6,35 +6,16 @@ === modified file 'setup.cfg' --- a/setup.cfg +++ b/setup.cfg -@@ -8,10 +8,10 @@ - typing; python_version<"3.5" - configparser; python_version<"3.2" - functools32; python_version<"3.2" -- entrypoints >= 0.3.0, < 0.4.0 -- pyflakes >= 2.1.0, < 2.2.0 -- pycodestyle >= 2.5.0, < 2.6.0 +@@ -36,9 +36,9 @@ + =src + packages = find: + install_requires = +- pyflakes >= 2.2.0, < 2.3.0 +- pycodestyle >= 2.6.0a1, < 2.7.0 - mccabe >= 0.6.0, < 0.7.0 -+ entrypoints >= 0.3.0 -+ pyflakes >= 2.1.0 -+ pycodestyle >= 2.5.0 ++ pyflakes >= 2.2.0 ++ pycodestyle >= 2.6.0a1 + mccabe >= 0.6.0 - - [mypy] - check_untyped_defs = true ---- a/setup.py -+++ b/setup.py -@@ -19,10 +19,10 @@ - # http://flake8.pycqa.org/en/latest/faq.html#why-does-flake8-use-ranges-for-its-dependencies - # And in which releases we will update those ranges here: - # http://flake8.pycqa.org/en/latest/internal/releases.html#releasing-flake8 -- "entrypoints >= 0.3.0, < 0.4.0", -- "pyflakes >= 2.1.0, < 2.2.0", -- "pycodestyle >= 2.5.0, < 2.6.0", -- "mccabe >= 0.6.0, < 0.7.0", -+ "entrypoints >= 0.3.0", -+ "pyflakes >= 2.1.0", -+ "pycodestyle >= 2.5.0", -+ "mccabe >= 0.6.0", - ] - - extras_require = { + enum34; python_version<"3.4" + typing; python_version<"3.5" + configparser; python_version<"3.2" diff -Nru python-flake8-3.7.9/debian/rules python-flake8-3.8.3/debian/rules --- python-flake8-3.7.9/debian/rules 2020-02-15 10:08:53.000000000 -0800 +++ python-flake8-3.8.3/debian/rules 2020-07-02 08:48:13.000000000 -0700 @@ -6,15 +6,15 @@ dh $@ --with python3 --buildsystem=pybuild override_dh_auto_test: - touch tox.ini - PYBUILD_SYSTEM=custom PYBUILD_TEST_ARGS="{interpreter} setup.py test" dh_auto_test override_dh_auto_install: -ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS))) dh_auto_install mkdir -p debian/flake8/usr mv $(CURDIR)/debian/python3-flake8/usr/bin $(CURDIR)/debian/flake8/usr/ + # run tests after install: plugin needs to get registered by setup.py/entry_points +ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS))) + PYBUILD_SYSTEM=custom PYBUILD_TEST_ARGS="{interpreter} -m pytest" dh_auto_test endif override_dh_installchangelogs: diff -Nru python-flake8-3.7.9/docs/source/glossary.rst python-flake8-3.8.3/docs/source/glossary.rst --- python-flake8-3.7.9/docs/source/glossary.rst 2019-10-28 08:59:37.000000000 -0700 +++ python-flake8-3.8.3/docs/source/glossary.rst 2020-06-08 12:15:48.000000000 -0700 @@ -54,4 +54,4 @@ mccabe The project |Flake8| depends on to calculate the McCabe complexity of a unit of code (e.g., a function). This uses the ``C`` - :term:`class` of :term`error code`\ s. + :term:`class` of :term:`error code`\ s. diff -Nru python-flake8-3.7.9/docs/source/internal/option_handling.rst python-flake8-3.8.3/docs/source/internal/option_handling.rst --- python-flake8-3.7.9/docs/source/internal/option_handling.rst 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/docs/source/internal/option_handling.rst 2020-06-08 12:15:48.000000000 -0700 @@ -28,7 +28,7 @@ to ``add_option``, specifically ``parse_from_config`` which is a boolean value. -|Flake8| does this by creating its own abstractions on top of :mod:`optparse`. +|Flake8| does this by creating its own abstractions on top of :mod:`argparse`. The first abstraction is the :class:`flake8.options.manager.Option` class. The second is the :class:`flake8.options.manager.OptionManager`. In fact, we add three new parameters: @@ -67,7 +67,7 @@ E251 It makes sense here to allow users to specify the value this way, but, the -standard libary's :class:`configparser.RawConfigParser` class does returns a +standard library's :class:`configparser.RawConfigParser` class does returns a string that looks like .. code-block:: python @@ -219,7 +219,7 @@ .. autofunction:: flake8.options.aggregator.aggregate_options .. autoclass:: flake8.options.manager.Option - :members: __init__, normalize, to_optparse + :members: __init__, normalize, to_argparse .. autoclass:: flake8.options.manager.OptionManager :members: diff -Nru python-flake8-3.7.9/docs/source/internal/utils.rst python-flake8-3.8.3/docs/source/internal/utils.rst --- python-flake8-3.7.9/docs/source/internal/utils.rst 2019-10-28 10:30:33.000000000 -0700 +++ python-flake8-3.8.3/docs/source/internal/utils.rst 2020-06-08 12:15:48.000000000 -0700 @@ -25,13 +25,8 @@ "E121,W123,F904" "E121,\nW123,\nF804" - "E121,\n\tW123,\n\tF804" - -Or it will take a list of strings (potentially with whitespace) such as - -.. code-block:: python - - [" E121\n", "\t\nW123 ", "\n\tF904\n "] + " E121,\n\tW123,\n\tF804 " + " E121\n\tW123 \n\tF804" And converts it to a list that looks as follows @@ -50,9 +45,9 @@ .. autofunction:: flake8.utils.normalize_paths -This function utilizes :func:`~flake8.utils.parse_comma_separated_list` and -:func:`~flake8.utils.normalize_path` to normalize its input to a list of -strings that should be paths. +This function utilizes :func:`~flake8.utils.normalize_path` to normalize a +sequence of paths. See :func:`~flake8.utils.normalize_path` for what defines a +normalized path. .. autofunction:: flake8.utils.stdin_get_value diff -Nru python-flake8-3.7.9/docs/source/plugin-development/plugin-parameters.rst python-flake8-3.8.3/docs/source/plugin-development/plugin-parameters.rst --- python-flake8-3.7.9/docs/source/plugin-development/plugin-parameters.rst 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/docs/source/plugin-development/plugin-parameters.rst 2020-06-08 12:15:48.000000000 -0700 @@ -72,7 +72,7 @@ Your ``add_options`` function should expect to receive an instance of |OptionManager|. An |OptionManager| instance behaves very similarly to :class:`optparse.OptionParser`. It, however, uses the layer that |Flake8| has -developed on top of :mod:`optparse` to also handle configuration file parsing. +developed on top of :mod:`argparse` to also handle configuration file parsing. :meth:`~flake8.options.manager.OptionManager.add_option` creates an |Option| which accepts the same parameters as :mod:`optparse` as well as three extra boolean parameters: @@ -115,13 +115,13 @@ '--max-line-length', type='int', metavar='n', default=defaults.MAX_LINE_LENGTH, parse_from_config=True, help='Maximum allowed line length for the entirety of this run. ' - '(Default: %default)', + '(Default: %(default)s)', ) Here we are adding the ``--max-line-length`` command-line option which is always an integer and will be parsed from the configuration file. Since we -provide a default, we take advantage of :mod:`optparse`\ 's willingness to -display that in the help text with ``%default``. +provide a default, we take advantage of :mod:`argparse`\ 's willingness to +display that in the help text with ``%(default)s``. .. code-block:: python @@ -129,7 +129,7 @@ '--select', metavar='errors', default='', parse_from_config=True, comma_separated_list=True, help='Comma-separated list of errors and warnings to enable.' - ' For example, ``--select=E4,E51,W234``. (Default: %default)', + ' For example, ``--select=E4,E51,W234``. (Default: %(default)s)', ) In adding the ``--select`` command-line option, we're also indicating to the @@ -143,7 +143,7 @@ comma_separated_list=True, parse_from_config=True, normalize_paths=True, help='Comma-separated list of files or directories to exclude.' - '(Default: %default)', + '(Default: %(default)s)', ) Finally, we show an option that uses all three extra flags. Values from @@ -152,7 +152,7 @@ For information about other parameters to :meth:`~flake8.options.manager.OptionManager.add_option` refer to the -documentation of :mod:`optparse`. +documentation of :mod:`argparse`. Accessing Parsed Options @@ -160,10 +160,10 @@ When a plugin has a callable ``parse_options`` attribute, |Flake8| will call it and attempt to provide the |OptionManager| instance, the parsed options -which will be an instance of :class:`optparse.Values`, and the extra arguments -that were not parsed by the |OptionManager|. If that fails, we will just pass -the :class:`optparse.Values`. In other words, your ``parse_options`` -callable will have one of the following signatures: +which will be an instance of :class:`argparse.Namespace`, and the extra +arguments that were not parsed by the |OptionManager|. If that fails, we will +just pass the :class:`argparse.Namespace`. In other words, your +``parse_options`` callable will have one of the following signatures: .. code-block:: python diff -Nru python-flake8-3.7.9/docs/source/release-notes/3.7.0.rst python-flake8-3.8.3/docs/source/release-notes/3.7.0.rst --- python-flake8-3.7.9/docs/source/release-notes/3.7.0.rst 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/docs/source/release-notes/3.7.0.rst 2020-06-08 12:15:48.000000000 -0700 @@ -83,4 +83,3 @@ https://gitlab.com/pycqa/flake8/merge_requests/287 .. _GitLab!288: https://gitlab.com/pycqa/flake8/merge_requests/288 - diff -Nru python-flake8-3.7.9/docs/source/release-notes/3.8.0.rst python-flake8-3.8.3/docs/source/release-notes/3.8.0.rst --- python-flake8-3.7.9/docs/source/release-notes/3.8.0.rst 1969-12-31 16:00:00.000000000 -0800 +++ python-flake8-3.8.3/docs/source/release-notes/3.8.0.rst 2020-06-08 12:15:48.000000000 -0700 @@ -0,0 +1,225 @@ +3.8.0 -- 2020-05-11 +------------------- + +You can view the `3.8.0 milestone`_ on GitLab for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Fix logical checks which report positions out of bounds (See also + `GitLab!422`_, `GitLab#635`_) + +- Fix ``--exclude=.*`` accidentally matching ``.`` and ``..`` (See also + `GitLab!424`_, `GitLab#632`_) + +Deprecations +~~~~~~~~~~~~ + +- Add deprecation message for vcs hooks (See also `GitLab!420`_, + `GitLab#568`_) + + +3.8.0a2 -- 2020-04-24 +--------------------- + +You can view the `3.8.0 milestone`_ on GitLab for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Fix ``type="str"`` optparse options (See also `GitLab!419`_) + + +3.8.0a1 -- 2020-04-24 +--------------------- + +You can view the `3.8.0 milestone`_ on GitLab for more details. + +New Dependency Information +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- Remove dependency on ``entrypoints`` and add dependency on + ``importlib-metadata`` (only for ``python<3.8``) (See also `GitLab!388`_, + `GitLab#569`_) + +- Pyflakes has been updated to >= 2.2.0, < 2.3.0 (See also `GitLab!417`_) + +- pycodestyle has been updated to >= 2.6.0a1, < 2.7.0 (See also `GitLab!418`_) + +Features +~~~~~~~~ + +- Add ``--extend-exclude`` option to add to ``--exclude`` without overwriting + (See also `GitLab!315`_, `GitLab#535`_) + +- Move argument parsing from ``optparse`` to ``argparse`` (See also + `GitLab!341`_ + +- Group plugin options in ``--help`` (See also `GitLab!342`_, `GitLab#565`_) + +- Remove parsing of ``verbose`` from configuration files as it was not + consistently applied (See also `GitLab!360`_, `GitLab#439`_) + +- Remove parsing of ``output_file`` from configuration files as it was not + consistently applied (See also `GitLab!361`_) + +- Resolve configuration files relative to ``cwd`` instead of common prefix of + passed filenames. You may need to change ``flake8 subproject`` to + ``cd subproject && flake8 .`` (See also `GitLab!363`_) + +- Officially support python3.8 (See also `GitLab!377`_) + +- ``--disable-noqa`` now also disables ``# flake8: noqa`` (See also + `GitLab!380`_, `GitLab#590`_) + +- Ensure that a missing file produces a ``E902`` error (See also `GitLab!404`_, + `GitLab#600`_) + +- ``# noqa`` comments now apply to all of the lines in an explicit ``\`` + continuation or in a line continued by a multi-line string (See also + `GitLab!413`_, `GitLab#375`_) + +Bugs Fixed +~~~~~~~~~~ + +- Fix ``--exclude=./t.py`` to only match ``t.py`` at the top level (See also + `GitLab!311`_, `GitLab#382`_) + +- Fix ``--show-source`` when a file is indented with tabs (See also + `GitLab!339`_, `GitLab#563`_) + +- Fix crash when ``--max-line-length`` is given a non-integer (See also + `GitLab!341`_, `GitLab#541`_) + +- Prevent flip-flopping of ``indent_char`` causing extra ``E101`` errors (See + also `GitLab!357`_, `pycodestyle#886`_) + +- Only enable multiprocessing when the method is ``fork`` fixing issues + on macos with python3.8+ (See also `GitLab!366`_, `GitLab#587`_) (note: this + fix also landed in 3.7.9) + +- ``noqa`` is now only handled by flake8 fixing specific-noqa. Plugins + requesting this parameter will always receive ``False`` (See also + `GitLab!331`_, `GitLab#552`_) + +- Fix duplicate loading of plugins when invoked via ``python -m flake8`` (See + also `GitLab!388`_) + +- Fix early exit when ``--exit-zero`` and ``--diff`` are provided and the diff + is empty (See also `GitLab!391`_) + +- Consistently split lines when ``\f`` is present when reading from stdin (See + also `GitLab!406`_, `GitLab#270`_) + +Deprecations +~~~~~~~~~~~~ + +- ``python setup.py flake8`` (setuptools integration) is now deprecated and + will be removed in a future version (See also `GitLab!330`_, `GitLab#544`_) + +- ``type='string'`` (optparse) types are deprecated, use + ``type=callable`` (argparse) instead. Support for ``type='string'`` will + be removed in a future version (See also `GitLab!341`_) + +- ``%default`` in plugin option help text is deprecated, use ``%(default)s`` + instead. Support for ``%default`` will be removed in a future version (See + also `GitLab!341`_) + +- optparse-style ``action='callback'`` setting for options is deprecated, use + argparse action classes instead. This will be removed in a future version + (See also `GitLab!341`_) + + +.. all links +.. _3.8.0 milestone: + https://gitlab.com/pycqa/flake8/-/milestones/32 + +.. merge request links +.. _GitLab#270: + https://gitlab.com/pycqa/flake8/-/issues/270 +.. _GitLab#375: + https://gitlab.com/pycqa/flake8/-/issues/375 +.. _GitLab#382: + https://gitlab.com/pycqa/flake8/-/issues/382 +.. _GitLab#439: + https://gitlab.com/pycqa/flake8/-/issues/439 +.. _GitLab#535: + https://gitlab.com/pycqa/flake8/-/issues/535 +.. _GitLab#541: + https://gitlab.com/pycqa/flake8/-/issues/541 +.. _GitLab#544: + https://gitlab.com/pycqa/flake8/-/issues/544 +.. _GitLab#552: + https://gitlab.com/pycqa/flake8/-/issues/552 +.. _GitLab#563: + https://gitlab.com/pycqa/flake8/-/issues/563 +.. _GitLab#565: + https://gitlab.com/pycqa/flake8/-/issues/565 +.. _GitLab#568: + https://gitlab.com/pycqa/flake8/-/issues/568 +.. _GitLab#569: + https://gitlab.com/pycqa/flake8/-/issues/569 +.. _GitLab#587: + https://gitlab.com/pycqa/flake8/-/issues/587 +.. _GitLab#590: + https://gitlab.com/pycqa/flake8/-/issues/590 +.. _GitLab#600: + https://gitlab.com/pycqa/flake8/-/issues/600 +.. _GitLab#632: + https://gitlab.com/pycqa/flake8/-/issues/632 +.. _GitLab#635: + https://gitlab.com/pycqa/flake8/-/issues/635 +.. _pycodestyle#886: + https://github.com/PyCQA/pycodestyle/issues/886 + +.. issue links +.. _GitLab!311: + https://gitlab.com/pycqa/flake8/-/merge_requests/311 +.. _GitLab!315: + https://gitlab.com/pycqa/flake8/-/merge_requests/315 +.. _GitLab!330: + https://gitlab.com/pycqa/flake8/-/merge_requests/330 +.. _GitLab!331: + https://gitlab.com/pycqa/flake8/-/merge_requests/331 +.. _GitLab!339: + https://gitlab.com/pycqa/flake8/-/merge_requests/339 +.. _GitLab!341: + https://gitlab.com/pycqa/flake8/-/merge_requests/341 +.. _GitLab!342: + https://gitlab.com/pycqa/flake8/-/merge_requests/342 +.. _GitLab!357: + https://gitlab.com/pycqa/flake8/-/merge_requests/357 +.. _GitLab!360: + https://gitlab.com/pycqa/flake8/-/merge_requests/360 +.. _GitLab!361: + https://gitlab.com/pycqa/flake8/-/merge_requests/361 +.. _GitLab!363: + https://gitlab.com/pycqa/flake8/-/merge_requests/363 +.. _GitLab!366: + https://gitlab.com/pycqa/flake8/-/merge_requests/366 +.. _GitLab!377: + https://gitlab.com/pycqa/flake8/-/merge_requests/377 +.. _GitLab!380: + https://gitlab.com/pycqa/flake8/-/merge_requests/380 +.. _GitLab!388: + https://gitlab.com/pycqa/flake8/-/merge_requests/388 +.. _GitLab!391: + https://gitlab.com/pycqa/flake8/-/merge_requests/391 +.. _GitLab!404: + https://gitlab.com/pycqa/flake8/-/merge_requests/404 +.. _GitLab!406: + https://gitlab.com/pycqa/flake8/-/merge_requests/406 +.. _GitLab!413: + https://gitlab.com/pycqa/flake8/-/merge_requests/413 +.. _GitLab!417: + https://gitlab.com/pycqa/flake8/-/merge_requests/417 +.. _GitLab!418: + https://gitlab.com/pycqa/flake8/-/merge_requests/418 +.. _GitLab!419: + https://gitlab.com/pycqa/flake8/-/merge_requests/419 +.. _GitLab!420: + https://gitlab.com/pycqa/flake8/-/merge_requests/420 +.. _GitLab!422: + https://gitlab.com/pycqa/flake8/-/merge_requests/422 +.. _GitLab!424: + https://gitlab.com/pycqa/flake8/-/merge_requests/424 diff -Nru python-flake8-3.7.9/docs/source/release-notes/3.8.1.rst python-flake8-3.8.3/docs/source/release-notes/3.8.1.rst --- python-flake8-3.7.9/docs/source/release-notes/3.8.1.rst 1969-12-31 16:00:00.000000000 -0800 +++ python-flake8-3.8.3/docs/source/release-notes/3.8.1.rst 2020-06-08 12:15:48.000000000 -0700 @@ -0,0 +1,23 @@ +3.8.1 -- 2020-05-11 +------------------- + +You can view the `3.8.1 milestone`_ on GitLab for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Fix ``--output-file`` (regression in 3.8.0) (See also `GitLab!427`_, + `GitLab#637`_) + + +.. all links +.. _3.8.1 milestone: + https://gitlab.com/pycqa/flake8/-/milestones/34 + +.. issue links +.. _GitLab#637: + https://gitlab.com/pycqa/flake8/issues/637 + +.. merge request links +.. _GitLab!427: + https://gitlab.com/pycqa/flake8/merge_requests/427 diff -Nru python-flake8-3.7.9/docs/source/release-notes/3.8.2.rst python-flake8-3.8.3/docs/source/release-notes/3.8.2.rst --- python-flake8-3.7.9/docs/source/release-notes/3.8.2.rst 1969-12-31 16:00:00.000000000 -0800 +++ python-flake8-3.8.3/docs/source/release-notes/3.8.2.rst 2020-06-08 12:15:48.000000000 -0700 @@ -0,0 +1,40 @@ +3.8.2 -- 2020-05-22 +------------------- + +You can view the `3.8.2 milestone`_ on GitLab for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Improve performance by eliminating unncessary sort (See also `GitLab!429`_) + +- Improve messaging of ``--jobs`` argument by utilizing ``argparse`` (See also + `GitLab!428`_, `GitLab#567`_) + +- Fix file configuration options to be relative to the config passed on the + command line (See also `GitLab!431`_, `GitLab#651`_) + +- Fix incorrect handling of ``--extend-exclude`` by treating its values as + files (See also `GitLab!432`_, `GitLab#653`_) + +.. all links +.. _3.8.2 milestone: + https://gitlab.com/pycqa/flake8/-/milestones/35 + +.. issue links +.. _GitLab#567: + https://gitlab.com/pycqa/flake8/issues/567 +.. _GitLab#651: + https://gitlab.com/pycqa/flake8/issues/651 +.. _GitLab#653: + https://gitlab.com/pycqa/flake8/issues/653 + +.. merge request links +.. _GitLab!428: + https://gitlab.com/pycqa/flake8/merge_requests/428 +.. _GitLab!429: + https://gitlab.com/pycqa/flake8/merge_requests/429 +.. _GitLab!431: + https://gitlab.com/pycqa/flake8/merge_requests/431 +.. _GitLab!432: + https://gitlab.com/pycqa/flake8/merge_requests/432 diff -Nru python-flake8-3.7.9/docs/source/release-notes/3.8.3.rst python-flake8-3.8.3/docs/source/release-notes/3.8.3.rst --- python-flake8-3.7.9/docs/source/release-notes/3.8.3.rst 1969-12-31 16:00:00.000000000 -0800 +++ python-flake8-3.8.3/docs/source/release-notes/3.8.3.rst 2020-06-08 12:26:49.000000000 -0700 @@ -0,0 +1,29 @@ +3.8.3 -- 2020-06-08 +------------------- + +You can view the `3.8.3 milestone`_ on GitLab for more details. + +Bugs Fixed +~~~~~~~~~~ + +- Also catch ``SyntaxError`` when tokenizing (See also `GitLab!433`_, + `GitLab#662`_) + +- Fix ``--jobs`` default display in ``flake8 --help`` (See also `GitLab!434`_, + `GitLab#665`_) + +.. all links +.. _3.8.3 milestone: + https://gitlab.com/pycqa/flake8/-/milestones/36 + +.. issue links +.. _GitLab#662: + https://gitlab.com/pycqa/flake8/issues/662 +.. _GitLab#665: + https://gitlab.com/pycqa/flake8/issues/665 + +.. merge request links +.. _GitLab!433: + https://gitlab.com/pycqa/flake8/merge_requests/433 +.. _GitLab!434: + https://gitlab.com/pycqa/flake8/merge_requests/434 diff -Nru python-flake8-3.7.9/docs/source/release-notes/index.rst python-flake8-3.8.3/docs/source/release-notes/index.rst --- python-flake8-3.7.9/docs/source/release-notes/index.rst 2019-10-28 10:33:58.000000000 -0700 +++ python-flake8-3.8.3/docs/source/release-notes/index.rst 2020-06-08 12:19:16.000000000 -0700 @@ -9,6 +9,10 @@ ================== .. toctree:: + 3.8.3 + 3.8.2 + 3.8.1 + 3.8.0 3.7.9 3.7.8 3.7.7 diff -Nru python-flake8-3.7.9/docs/source/user/configuration.rst python-flake8-3.8.3/docs/source/user/configuration.rst --- python-flake8-3.7.9/docs/source/user/configuration.rst 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/docs/source/user/configuration.rst 2020-06-08 12:15:48.000000000 -0700 @@ -30,6 +30,11 @@ - In your project in one of ``setup.cfg``, ``tox.ini``, or ``.flake8``. +Values set at the command line have highest priority, then those in the +project configuration file, then those in your user directory, and finally +there are the defaults. However, there are additional command line options +which can alter this. + "User" Configuration -------------------- diff -Nru python-flake8-3.7.9/docs/source/user/error-codes.rst python-flake8-3.8.3/docs/source/user/error-codes.rst --- python-flake8-3.7.9/docs/source/user/error-codes.rst 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/docs/source/user/error-codes.rst 2020-06-08 12:15:48.000000000 -0700 @@ -4,7 +4,7 @@ Error / Violation Codes ========================= -Flake8 and its plugins assign a code to each message that we refer to as a +Flake8 and its plugins assign a code to each message that we refer to as an :term:`error code` (or :term:`violation`). Most plugins will list their error codes in their documentation or README. @@ -29,6 +29,37 @@ | F407 | an undefined ``__future__`` feature name was imported | +------+---------------------------------------------------------------------+ +------+---------------------------------------------------------------------+ +| F501 | invalid ``%`` format literal | ++------+---------------------------------------------------------------------+ +| F502 | ``%`` format expected mapping but got sequence | ++------+---------------------------------------------------------------------+ +| F503 | ``%`` format expected sequence but got mapping | ++------+---------------------------------------------------------------------+ +| F504 | ``%`` format unused named arguments | ++------+---------------------------------------------------------------------+ +| F505 | ``%`` format missing named arguments | ++------+---------------------------------------------------------------------+ +| F506 | ``%`` format mixed positional and named arguments | ++------+---------------------------------------------------------------------+ +| F507 | ``%`` format mismatch of placeholder and argument count | ++------+---------------------------------------------------------------------+ +| F508 | ``%`` format with ``*`` specifier requires a sequence | ++------+---------------------------------------------------------------------+ +| F509 | ``%`` format with unsupported format character | ++------+---------------------------------------------------------------------+ +| F521 | ``.format(...)`` invalid format string | ++------+---------------------------------------------------------------------+ +| F522 | ``.format(...)`` unused named arguments | ++------+---------------------------------------------------------------------+ +| F523 | ``.format(...)`` unused positional arguments | ++------+---------------------------------------------------------------------+ +| F524 | ``.format(...)`` missing argument | ++------+---------------------------------------------------------------------+ +| F525 | ``.format(...)`` mixing automatic and manual numbering | ++------+---------------------------------------------------------------------+ +| F541 | f-string without any placeholders | ++------+---------------------------------------------------------------------+ ++------+---------------------------------------------------------------------+ | F601 | dictionary key ``name`` repeated with different values | +------+---------------------------------------------------------------------+ | F602 | dictionary key variable ``name`` repeated with different values | @@ -37,11 +68,13 @@ +------+---------------------------------------------------------------------+ | F622 | two or more starred expressions in an assignment ``(a, *b, *c = d)``| +------+---------------------------------------------------------------------+ -| F631 | assertion test is a tuple, which are always ``True`` | +| F631 | assertion test is a tuple, which is always ``True`` | +------+---------------------------------------------------------------------+ | F632 | use ``==/!=`` to compare ``str``, ``bytes``, and ``int`` literals | +------+---------------------------------------------------------------------+ -| F633 | assertion test is a tuple, which are always ``True`` | +| F633 | use of ``>>`` is invalid with ``print`` function | ++------+---------------------------------------------------------------------+ +| F634 | if test is a tuple, which is always ``True`` | +------+---------------------------------------------------------------------+ +------+---------------------------------------------------------------------+ | F701 | a ``break`` statement outside of a ``while`` or ``for`` loop | diff -Nru python-flake8-3.7.9/docs/source/user/options.rst python-flake8-3.8.3/docs/source/user/options.rst --- python-flake8-3.7.9/docs/source/user/options.rst 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/docs/source/user/options.rst 2020-06-08 12:15:48.000000000 -0700 @@ -155,13 +155,7 @@ flake8 -vv - This **can** be specified in config files. - - Example config file usage: - - .. code-block:: ini - - verbose = 2 + This **can not** be specified in config files. .. option:: -q, --quiet @@ -257,6 +251,38 @@ __pycache__ +.. option:: --extend-exclude= + + :ref:`Go back to index ` + + .. versionadded:: 3.8.0 + + Provide a comma-separated list of glob patterns to add to the list of excluded ones. + Similar considerations as in :option:`--exclude` apply here with regard to the + value. + + The difference to the :option:`--exclude` option is, that this option can be + used to selectively add individual patterns without overriding the default + list entirely. + + Command-line example: + + .. prompt:: bash + + flake8 --extend-exclude=legacy/,vendor/ dir/ + + This **can** be specified in config files. + + Example config file usage: + + .. code-block:: ini + + extend-exclude = + legacy/, + vendor/ + extend-exclude = legacy/,vendor/ + + .. option:: --filename= :ref:`Go back to index ` diff -Nru python-flake8-3.7.9/docs/source/user/using-hooks.rst python-flake8-3.8.3/docs/source/user/using-hooks.rst --- python-flake8-3.7.9/docs/source/user/using-hooks.rst 2019-10-28 08:59:37.000000000 -0700 +++ python-flake8-3.8.3/docs/source/user/using-hooks.rst 2020-06-08 12:15:48.000000000 -0700 @@ -21,7 +21,7 @@ will always lint explicitly passed arguments (:option:`flake8 --exclude` has no effect). Instead use ``pre-commit``'s ``exclude: ...`` regex to exclude files. ``pre-commit`` won't ever pass untracked files to ``flake8`` so -excluding ``.git`` / ``.tox`` / etc. is unnecesary. +excluding ``.git`` / ``.tox`` / etc. is unnecessary. .. code-block:: yaml @@ -45,7 +45,7 @@ It is strongly suggested to use |Flake8| via `pre-commit`_ over the built-in hook mechanisms. ``pre-commit`` smooths out many of the rough edges of ``git`` and is much more battle-tested than the |Flake8| - hook impementation. + hook implementation. |Flake8| can be integrated into your development workflow in many ways. A default installation of |Flake8| can install pre-commit hooks for both @@ -91,7 +91,7 @@ |Flake8| aims to make smart choices that keep things fast for users where possible. As a result, the |Flake8| Git pre-commit will default to only checking files that have been staged (i.e., added to the index). If, however, -you are keen to be lazy and not independenty add files to your git index, you +you are keen to be lazy and not independently add files to your git index, you can set ``flake8.lazy`` to ``true`` (similar to how you would set ``flake8.strict`` above) and this will check all tracked files. diff -Nru python-flake8-3.7.9/docs/source/user/violations.rst python-flake8-3.8.3/docs/source/user/violations.rst --- python-flake8-3.7.9/docs/source/user/violations.rst 2019-10-28 08:59:37.000000000 -0700 +++ python-flake8-3.8.3/docs/source/user/violations.rst 2020-06-08 12:15:48.000000000 -0700 @@ -103,6 +103,17 @@ Finally, if we have a particularly bad line of code, we can ignore every error using simply ``# noqa`` with nothing after it. +Contents before and after the ``# noqa: ...`` portion are ignored so multiple +comments may appear on one line. Here are several examples: + +.. code-block:: python + + # mypy requires `# type: ignore` to appear first + x = 5 # type: ignore # noqa: ABC123 + + # can use to add useful user information to a noqa comment + y = 6 # noqa: ABC456 # TODO: will fix this later + Ignoring Entire Files --------------------- diff -Nru python-flake8-3.7.9/PKG-INFO python-flake8-3.8.3/PKG-INFO --- python-flake8-3.7.9/PKG-INFO 2019-10-28 10:36:48.000000000 -0700 +++ python-flake8-3.8.3/PKG-INFO 2020-06-08 12:30:20.849549300 -0700 @@ -1,7 +1,7 @@ Metadata-Version: 1.2 Name: flake8 -Version: 3.7.9 -Summary: the modular source code checker: pep8, pyflakes and co +Version: 3.8.3 +Summary: the modular source code checker: pep8 pyflakes and co Home-page: https://gitlab.com/pycqa/flake8 Author: Tarek Ziade Author-email: tarek@ziade.org @@ -100,6 +100,7 @@ Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Software Development :: Quality Assurance -Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* +Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7 diff -Nru python-flake8-3.7.9/setup.cfg python-flake8-3.8.3/setup.cfg --- python-flake8-3.7.9/setup.cfg 2019-10-28 10:36:48.000000000 -0700 +++ python-flake8-3.8.3/setup.cfg 2020-06-08 12:30:20.849549300 -0700 @@ -2,16 +2,101 @@ universal = 1 [metadata] +name = flake8 +version = attr: flake8.__version__ +license = MIT license_file = LICENSE -requires-dist = +description = the modular source code checker: pep8 pyflakes and co +long_description = file: README.rst +author = Tarek Ziade +author_email = tarek@ziade.org +maintainer = Ian Stapleton Cordasco +maintainer_email = graffatcolmingov@gmail.com +url = https://gitlab.com/pycqa/flake8 +classifiers = + Development Status :: 5 - Production/Stable + Environment :: Console + Framework :: Flake8 + Intended Audience :: Developers + 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 :: 3.8 + Topic :: Software Development :: Libraries :: Python Modules + Topic :: Software Development :: Quality Assurance + +[options] +package_dir = + =src +packages = find: +install_requires = + pyflakes >= 2.2.0, < 2.3.0 + pycodestyle >= 2.6.0a1, < 2.7.0 + mccabe >= 0.6.0, < 0.7.0 enum34; python_version<"3.4" typing; python_version<"3.5" configparser; python_version<"3.2" functools32; python_version<"3.2" - entrypoints >= 0.3.0, < 0.4.0 - pyflakes >= 2.1.0, < 2.2.0 - pycodestyle >= 2.5.0, < 2.6.0 - mccabe >= 0.6.0, < 0.7.0 + importlib-metadata; python_version<"3.8" +python_requires = >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* + +[options.packages.find] +where = src + +[options.entry_points] +distutils.commands = + flake8 = flake8.main.setuptools_command:Flake8 +console_scripts = + flake8 = flake8.main.cli:main +flake8.extension = + F = flake8.plugins.pyflakes:FlakesChecker + pycodestyle.ambiguous_identifier = pycodestyle:ambiguous_identifier + pycodestyle.bare_except = pycodestyle:bare_except + pycodestyle.blank_lines = pycodestyle:blank_lines + pycodestyle.break_after_binary_operator = pycodestyle:break_after_binary_operator + pycodestyle.break_before_binary_operator = pycodestyle:break_before_binary_operator + pycodestyle.comparison_negative = pycodestyle:comparison_negative + pycodestyle.comparison_to_singleton = pycodestyle:comparison_to_singleton + pycodestyle.comparison_type = pycodestyle:comparison_type + pycodestyle.compound_statements = pycodestyle:compound_statements + pycodestyle.continued_indentation = pycodestyle:continued_indentation + pycodestyle.explicit_line_join = pycodestyle:explicit_line_join + pycodestyle.extraneous_whitespace = pycodestyle:extraneous_whitespace + pycodestyle.imports_on_separate_lines = pycodestyle:imports_on_separate_lines + pycodestyle.indentation = pycodestyle:indentation + pycodestyle.maximum_doc_length = pycodestyle:maximum_doc_length + pycodestyle.maximum_line_length = pycodestyle:maximum_line_length + pycodestyle.missing_whitespace = pycodestyle:missing_whitespace + pycodestyle.missing_whitespace_after_import_keyword = pycodestyle:missing_whitespace_after_import_keyword + pycodestyle.missing_whitespace_around_operator = pycodestyle:missing_whitespace_around_operator + pycodestyle.module_imports_on_top_of_file = pycodestyle:module_imports_on_top_of_file + pycodestyle.python_3000_async_await_keywords = pycodestyle:python_3000_async_await_keywords + pycodestyle.python_3000_backticks = pycodestyle:python_3000_backticks + pycodestyle.python_3000_has_key = pycodestyle:python_3000_has_key + pycodestyle.python_3000_invalid_escape_sequence = pycodestyle:python_3000_invalid_escape_sequence + pycodestyle.python_3000_not_equal = pycodestyle:python_3000_not_equal + pycodestyle.python_3000_raise_comma = pycodestyle:python_3000_raise_comma + pycodestyle.tabs_obsolete = pycodestyle:tabs_obsolete + pycodestyle.tabs_or_spaces = pycodestyle:tabs_or_spaces + pycodestyle.trailing_blank_lines = pycodestyle:trailing_blank_lines + pycodestyle.trailing_whitespace = pycodestyle:trailing_whitespace + pycodestyle.whitespace_around_comma = pycodestyle:whitespace_around_comma + pycodestyle.whitespace_around_keywords = pycodestyle:whitespace_around_keywords + pycodestyle.whitespace_around_named_parameter_equals = pycodestyle:whitespace_around_named_parameter_equals + pycodestyle.whitespace_around_operator = pycodestyle:whitespace_around_operator + pycodestyle.whitespace_before_comment = pycodestyle:whitespace_before_comment + pycodestyle.whitespace_before_parameters = pycodestyle:whitespace_before_parameters +flake8.report = + default = flake8.formatting.default:Default + pylint = flake8.formatting.default:Pylint + quiet-filename = flake8.formatting.default:FilenameOnly + quiet-nothing = flake8.formatting.default:Nothing [mypy] check_untyped_defs = true @@ -29,12 +114,21 @@ [mypy-flake8.formatting.*] disallow_untyped_defs = true +[mypy-flake8.options.manager] +disallow_untyped_defs = true + [mypy-flake8.main.cli] disallow_untyped_defs = true +[mypy-flake8.processor] +disallow_untyped_defs = true + [mypy-flake8.statistics] disallow_untyped_defs = true +[mypy-flake8.style_guide] +disallow_untyped_defs = true + [mypy-flake8.utils] disallow_untyped_defs = true diff -Nru python-flake8-3.7.9/setup.py python-flake8-3.8.3/setup.py --- python-flake8-3.7.9/setup.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/setup.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- """Packaging logic for Flake8.""" -import functools -import io import os import sys @@ -9,142 +7,4 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'src')) # noqa -import flake8 - - -# NOTE(sigmavirus24): When updating these requirements, update them in -# setup.cfg as well. -requires = [ - # We document the reasoning for using ranges here: - # http://flake8.pycqa.org/en/latest/faq.html#why-does-flake8-use-ranges-for-its-dependencies - # And in which releases we will update those ranges here: - # http://flake8.pycqa.org/en/latest/internal/releases.html#releasing-flake8 - "entrypoints >= 0.3.0, < 0.4.0", - "pyflakes >= 2.1.0, < 2.2.0", - "pycodestyle >= 2.5.0, < 2.6.0", - "mccabe >= 0.6.0, < 0.7.0", -] - -extras_require = { - ":python_version<'3.4'": ['enum34'], - ":python_version<'3.5'": ['typing'], - ":python_version<'3.2'": ['configparser', 'functools32'], -} - -if int(setuptools.__version__.split('.')[0]) < 18: - extras_require = {} - if sys.version_info < (3, 4): - requires.append('enum34') - if sys.version_info < (3, 2): - requires.append('configparser') - - -def get_long_description(): - """Generate a long description from the README file.""" - descr = [] - for fname in ('README.rst',): - with io.open(fname, encoding='utf-8') as f: - descr.append(f.read()) - return '\n\n'.join(descr) - - -PEP8 = 'pycodestyle' -_FORMAT = '{0}.{1} = {0}:{1}' -PEP8_PLUGIN = functools.partial(_FORMAT.format, PEP8) - - -setuptools.setup( - name="flake8", - license="MIT", - version=flake8.__version__, - description="the modular source code checker: pep8, pyflakes and co", - long_description=get_long_description(), - author="Tarek Ziade", - author_email="tarek@ziade.org", - maintainer="Ian Stapleton Cordasco", - maintainer_email="graffatcolmingov@gmail.com", - url="https://gitlab.com/pycqa/flake8", - package_dir={"": "src"}, - packages=[ - "flake8", - "flake8.api", - "flake8.formatting", - "flake8.main", - "flake8.options", - "flake8.plugins", - ], - python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*", - install_requires=requires, - extras_require=extras_require, - entry_points={ - 'distutils.commands': [ - 'flake8 = flake8.main.setuptools_command:Flake8' - ], - 'console_scripts': [ - 'flake8 = flake8.main.cli:main' - ], - 'flake8.extension': [ - 'F = flake8.plugins.pyflakes:FlakesChecker', - # PEP-0008 checks provided by PyCQA/pycodestyle - PEP8_PLUGIN('tabs_or_spaces'), - PEP8_PLUGIN('tabs_obsolete'), - PEP8_PLUGIN('trailing_whitespace'), - PEP8_PLUGIN('trailing_blank_lines'), - PEP8_PLUGIN('maximum_line_length'), - PEP8_PLUGIN('blank_lines'), - PEP8_PLUGIN('extraneous_whitespace'), - PEP8_PLUGIN('whitespace_around_keywords'), - PEP8_PLUGIN('missing_whitespace_after_import_keyword'), - PEP8_PLUGIN('missing_whitespace'), - PEP8_PLUGIN('indentation'), - PEP8_PLUGIN('continued_indentation'), - PEP8_PLUGIN('whitespace_before_parameters'), - PEP8_PLUGIN('whitespace_around_operator'), - PEP8_PLUGIN('missing_whitespace_around_operator'), - PEP8_PLUGIN('whitespace_around_comma'), - PEP8_PLUGIN('whitespace_around_named_parameter_equals'), - PEP8_PLUGIN('whitespace_before_comment'), - PEP8_PLUGIN('imports_on_separate_lines'), - PEP8_PLUGIN('module_imports_on_top_of_file'), - PEP8_PLUGIN('compound_statements'), - PEP8_PLUGIN('explicit_line_join'), - PEP8_PLUGIN('break_after_binary_operator'), - PEP8_PLUGIN('break_before_binary_operator'), - PEP8_PLUGIN('comparison_to_singleton'), - PEP8_PLUGIN('comparison_negative'), - PEP8_PLUGIN('comparison_type'), - PEP8_PLUGIN('ambiguous_identifier'), - PEP8_PLUGIN('bare_except'), - PEP8_PLUGIN('maximum_doc_length'), - PEP8_PLUGIN('python_3000_has_key'), - PEP8_PLUGIN('python_3000_raise_comma'), - PEP8_PLUGIN('python_3000_not_equal'), - PEP8_PLUGIN('python_3000_backticks'), - PEP8_PLUGIN('python_3000_invalid_escape_sequence'), - PEP8_PLUGIN('python_3000_async_await_keywords'), - ], - 'flake8.report': [ - 'default = flake8.formatting.default:Default', - 'pylint = flake8.formatting.default:Pylint', - 'quiet-filename = flake8.formatting.default:FilenameOnly', - 'quiet-nothing = flake8.formatting.default:Nothing', - ], - }, - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Environment :: Console", - "Framework :: Flake8", - "Intended Audience :: Developers", - "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", - "Topic :: Software Development :: Libraries :: Python Modules", - "Topic :: Software Development :: Quality Assurance", - ], -) +setuptools.setup() diff -Nru python-flake8-3.7.9/src/flake8/api/legacy.py python-flake8-3.8.3/src/flake8/api/legacy.py --- python-flake8-3.7.9/src/flake8/api/legacy.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/src/flake8/api/legacy.py 2020-06-08 12:15:48.000000000 -0700 @@ -3,12 +3,14 @@ Previously, users would import :func:`get_style_guide` from ``flake8.engine``. In 3.0 we no longer have an "engine" module but we maintain the API from it. """ +import argparse import logging import os.path import flake8 from flake8.formatting import base as formatter from flake8.main import application as app +from flake8.options import config LOG = logging.getLogger(__name__) @@ -27,14 +29,20 @@ :class:`StyleGuide` """ application = app.Application() - application.parse_preliminary_options_and_args([]) - flake8.configure_logging( - application.prelim_opts.verbose, application.prelim_opts.output_file + prelim_opts, remaining_args = application.parse_preliminary_options([]) + flake8.configure_logging(prelim_opts.verbose, prelim_opts.output_file) + config_finder = config.ConfigFileFinder( + application.program, + prelim_opts.append_config, + config_file=prelim_opts.config, + ignore_config_files=prelim_opts.isolated, ) - application.make_config_finder() - application.find_plugins() + + application.find_plugins(config_finder) application.register_plugin_options() - application.parse_configuration_and_cli([]) + application.parse_configuration_and_cli( + config_finder, remaining_args, + ) # We basically want application.initialize to be called but with these # options set instead before we make our formatter, notifier, internal # style guide and file checker manager. @@ -72,10 +80,10 @@ self._file_checker_manager = application.file_checker_manager @property - def options(self): + def options(self): # type: () -> argparse.Namespace """Return application's options. - An instance of :class:`optparse.Values` containing parsed options. + An instance of :class:`argparse.Namespace` containing parsed options. """ return self._application.options diff -Nru python-flake8-3.7.9/src/flake8/checker.py python-flake8-3.8.3/src/flake8/checker.py --- python-flake8-3.7.9/src/flake8/checker.py 2019-10-28 10:30:33.000000000 -0700 +++ python-flake8-3.8.3/src/flake8/checker.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,6 +1,7 @@ """Checker Manager and Checker classes.""" import collections import errno +import itertools import logging import signal import sys @@ -85,6 +86,7 @@ self.options = style_guide.options self.checks = checker_plugins self.jobs = self._job_count() + self._all_checkers = [] # type: List[FileChecker] self.checkers = [] # type: List[FileChecker] self.statistics = { "files": 0, @@ -92,6 +94,9 @@ "physical lines": 0, "tokens": 0, } + self.exclude = tuple( + itertools.chain(self.options.exclude, self.options.extend_exclude) + ) def _process_statistics(self): for checker in self.checkers: @@ -132,19 +137,12 @@ return 0 jobs = self.options.jobs - if jobs != "auto" and not jobs.isdigit(): - LOG.warning( - '"%s" is not a valid parameter to --jobs. Must be one ' - 'of "auto" or a numerical value, e.g., 4.', - jobs, - ) - return 0 # If the value is "auto", we want to let the multiprocessing library # decide the number based on the number of CPUs. However, if that # function is not implemented for this particular value of Python we # default to 1 - if jobs == "auto": + if jobs.is_auto: try: return multiprocessing.cpu_count() except NotImplementedError: @@ -152,7 +150,7 @@ # Otherwise, we know jobs should be an integer and we can just convert # it to an integer - return int(jobs) + return jobs.n_jobs def _handle_results(self, filename, results): style_guide = self.style_guide @@ -187,7 +185,7 @@ return utils.matches_filename( path, - patterns=self.options.exclude, + patterns=self.exclude, log_message='"%(path)s" has %(whether)sbeen excluded', logger=LOG, ) @@ -230,17 +228,15 @@ ) or is_stdin checks = self.checks.to_dictionary() - checkers = ( + self._all_checkers = [ FileChecker(filename, checks, self.options) for argument in paths for filename in utils.filenames_from( argument, self.is_path_excluded ) if should_create_file_checker(filename, argument) - ) - self.checkers = [ - checker for checker in checkers if checker.should_process ] + self.checkers = [c for c in self._all_checkers if c.should_process] LOG.info("Checking %d files", len(self.checkers)) def report(self): @@ -256,7 +252,7 @@ tuple(int, int) """ results_reported = results_found = 0 - for checker in self.checkers: + for checker in self._all_checkers: results = sorted( checker.results, key=lambda tup: (tup[1], tup[2]) ) @@ -304,9 +300,7 @@ for checker in self.checkers: filename = checker.display_name - checker.results = sorted( - final_results[filename], key=lambda tup: (tup[2], tup[2]) - ) + checker.results = final_results[filename] checker.statistics = final_statistics[filename] def run_serial(self): @@ -363,7 +357,7 @@ :param options: Parsed option values from config and command-line. :type options: - optparse.Values + argparse.Namespace """ self.options = options self.filename = filename @@ -384,7 +378,7 @@ self.should_process = not self.processor.should_ignore_file() self.statistics["physical lines"] = len(self.processor.lines) - def __repr__(self): + def __repr__(self): # type: () -> str """Provide helpful debugging representation.""" return "FileChecker for {}".format(self.filename) @@ -403,20 +397,20 @@ self.report("E902", 0, 0, message) return None - def report(self, error_code, line_number, column, text, line=None): - # type: (str, int, int, str, Optional[str]) -> str + def report(self, error_code, line_number, column, text): + # type: (Optional[str], int, int, str) -> str """Report an error by storing it in the results list.""" if error_code is None: error_code, text = text.split(" ", 1) - physical_line = line # If we're recovering from a problem in _make_processor, we will not # have this attribute. - if not physical_line and getattr(self, "processor", None): - physical_line = self.processor.line_for(line_number) + if hasattr(self, "processor"): + line = self.processor.noqa_line_for(line_number) + else: + line = None - error = (error_code, line_number, column, text, physical_line) - self.results.append(error) + self.results.append((error_code, line_number, column, text, line)) return error_code def run_check(self, plugin, **arguments): @@ -480,7 +474,7 @@ column -= column_offset return row, column - def run_ast_checks(self): + def run_ast_checks(self): # type: () -> None """Run all checks expecting an abstract syntax tree.""" try: ast = self.processor.build_ast() @@ -499,7 +493,7 @@ runner = checker.run() except AttributeError: runner = checker - for (line_number, offset, text, check) in runner: + for (line_number, offset, text, _) in runner: self.report( error_code=None, line_number=line_number, @@ -520,8 +514,9 @@ self.processor.update_checker_state_for(plugin) results = self.run_check(plugin, logical_line=logical_line) or () for offset, text in results: - offset = find_offset(offset, mapping) - line_number, column_offset = offset + line_number, column_offset = find_offset(offset, mapping) + if line_number == column_offset == 0: + LOG.warning("position of error out of bounds: %s", plugin) self.report( error_code=None, line_number=line_number, @@ -531,7 +526,7 @@ self.processor.next_logical_line() - def run_physical_checks(self, physical_line, override_error_line=None): + def run_physical_checks(self, physical_line): """Run all checks for a given physical line. A single physical check may return multiple errors. @@ -554,15 +549,11 @@ for result_single in result: column_offset, text = result_single - error_code = self.report( + self.report( error_code=None, line_number=self.processor.line_number, column=column_offset, text=text, - line=(override_error_line or physical_line), - ) - self.processor.check_physical_error( - error_code, physical_line ) def process_tokens(self): @@ -640,9 +631,7 @@ line_no = token[2][0] with self.processor.inside_multiline(line_number=line_no): for line in self.processor.split_line(token): - self.run_physical_checks( - line + "\n", override_error_line=token[4] - ) + self.run_physical_checks(line + "\n") def _pool_init(): @@ -667,11 +656,17 @@ def find_offset(offset, mapping): + # type: (int, processor._LogicalMapping) -> Tuple[int, int] """Find the offset tuple for a single offset.""" if isinstance(offset, tuple): return offset - for token_offset, position in mapping: + for token in mapping: + token_offset = token[0] if offset <= token_offset: + position = token[1] break + else: + position = (0, 0) + offset = token_offset = 0 return (position[0], position[1] + offset - token_offset) diff -Nru python-flake8-3.7.9/src/flake8/_compat.py python-flake8-3.8.3/src/flake8/_compat.py --- python-flake8-3.7.9/src/flake8/_compat.py 1969-12-31 16:00:00.000000000 -0800 +++ python-flake8-3.8.3/src/flake8/_compat.py 2020-06-08 12:15:48.000000000 -0700 @@ -0,0 +1,14 @@ +"""Expose backports in a single place.""" +import sys + +if sys.version_info >= (3,): # pragma: no cover (PY3+) + from functools import lru_cache +else: # pragma: no cover (= (3, 8): # pragma: no cover (PY38+) + import importlib.metadata as importlib_metadata +else: # pragma: no cover ( None + def __init__(self, plugin_name, exception): + # type: (str, Exception) -> None """Initialize our FailedToLoadPlugin exception.""" - self.plugin = plugin - self.ep_name = self.plugin.name + self.plugin_name = plugin_name self.original_exception = exception - super(FailedToLoadPlugin, self).__init__(plugin, exception) + super(FailedToLoadPlugin, self).__init__(plugin_name, exception) def __str__(self): # type: () -> str """Format our exception message.""" return self.FORMAT % { - "name": self.ep_name, + "name": self.plugin_name, "exc": self.original_exception, } diff -Nru python-flake8-3.7.9/src/flake8/formatting/base.py python-flake8-3.8.3/src/flake8/formatting/base.py --- python-flake8-3.7.9/src/flake8/formatting/base.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/src/flake8/formatting/base.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,7 +1,7 @@ """The base class and interface for all formatting plugins.""" from __future__ import print_function -import optparse +import argparse from typing import IO, List, Optional, Tuple if False: # `typing.TYPE_CHECKING` was introduced in 3.5.2 @@ -32,15 +32,17 @@ """ def __init__(self, options): - # type: (optparse.Values) -> None + # type: (argparse.Namespace) -> None """Initialize with the options parsed from config and cli. This also calls a hook, :meth:`after_init`, so subclasses do not need to call super to call this method. - :param optparse.Values options: + :param options: User specified configuration parsed from both configuration files and the command-line interface. + :type options: + :class:`argparse.Namespace` """ self.options = options self.filename = options.output_file @@ -171,10 +173,13 @@ # Because column numbers are 1-indexed, we need to remove one to get # the proper number of space characters. - pointer = (" " * (error.column_number - 1)) + "^" + indent = "".join( + c if c.isspace() else " " + for c in error.physical_line[: error.column_number - 1] + ) # Physical lines have a newline at the end, no need to add an extra # one - return error.physical_line + pointer + return "{}{}^".format(error.physical_line, indent) def _write(self, output): # type: (str) -> None """Handle logic of whether to use an output file or print().""" diff -Nru python-flake8-3.7.9/src/flake8/__init__.py python-flake8-3.8.3/src/flake8/__init__.py --- python-flake8-3.7.9/src/flake8/__init__.py 2019-10-28 10:34:29.000000000 -0700 +++ python-flake8-3.8.3/src/flake8/__init__.py 2020-06-08 12:19:20.000000000 -0700 @@ -18,7 +18,7 @@ LOG = logging.getLogger(__name__) LOG.addHandler(logging.NullHandler()) -__version__ = "3.7.9" +__version__ = "3.8.3" __version_info__ = tuple( int(i) for i in __version__.split(".") if i.isdigit() ) diff -Nru python-flake8-3.7.9/src/flake8/main/application.py python-flake8-3.8.3/src/flake8/main/application.py --- python-flake8-3.7.9/src/flake8/main/application.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/src/flake8/main/application.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,11 +1,11 @@ """Module containing the application logic for Flake8.""" from __future__ import print_function +import argparse import logging -import optparse import sys import time -from typing import Dict, List, Optional, Set +from typing import Dict, List, Optional, Set, Tuple import flake8 from flake8 import checker @@ -45,22 +45,19 @@ self.program = program #: The version of the program being run self.version = version + #: The prelimary argument parser for handling options required for + #: obtaining and parsing the configuration file. + self.prelim_arg_parser = argparse.ArgumentParser(add_help=False) + options.register_preliminary_options(self.prelim_arg_parser) #: The instance of :class:`flake8.options.manager.OptionManager` used #: to parse and handle the options and arguments passed by the user self.option_manager = manager.OptionManager( - prog="flake8", version=flake8.__version__ + prog="flake8", + version=flake8.__version__, + parents=[self.prelim_arg_parser], ) options.register_default_options(self.option_manager) - #: The preliminary options parsed from CLI before plugins are loaded, - #: into a :class:`optparse.Values` instance - self.prelim_opts = None # type: optparse.Values - #: The preliminary arguments parsed from CLI before plugins are loaded - self.prelim_args = None # type: List[str] - #: The instance of :class:`flake8.options.config.ConfigFileFinder` - self.config_finder = None - #: The :class:`flake8.options.config.LocalPlugins` found in config - self.local_plugins = None # type: config.LocalPlugins #: The instance of :class:`flake8.plugins.manager.Checkers` self.check_plugins = None # type: plugin_manager.Checkers # fmt: off @@ -77,8 +74,8 @@ self.file_checker_manager = None # type: checker.Manager #: The user-supplied options parsed into an instance of - #: :class:`optparse.Values` - self.options = None # type: optparse.Values + #: :class:`argparse.Namespace` + self.options = None # type: argparse.Namespace #: The left over arguments that were not parsed by #: :attr:`option_manager` self.args = None # type: List[str] @@ -97,48 +94,30 @@ #: The parsed diff information self.parsed_diff = {} # type: Dict[str, Set[int]] - def parse_preliminary_options_and_args(self, argv=None): - # type: (Optional[List[str]]) -> None - """Get preliminary options and args from CLI, pre-plugin-loading. + def parse_preliminary_options(self, argv): + # type: (List[str]) -> Tuple[argparse.Namespace, List[str]] + """Get preliminary options from the CLI, pre-plugin-loading. - We need to know the values of a few standard options and args now, so - that we can find config files and configure logging. + We need to know the values of a few standard options so that we can + locate configuration files and configure logging. Since plugins aren't loaded yet, there may be some as-yet-unknown options; we ignore those for now, they'll be parsed later when we do real option parsing. - Sets self.prelim_opts and self.prelim_args. - :param list argv: Command-line arguments passed in directly. + :returns: + Populated namespace and list of remaining argument strings. + :rtype: + (argparse.Namespace, list) """ - # We haven't found or registered our plugins yet, so let's defer - # printing the version until we aggregate options from config files - # and the command-line. First, let's clone our arguments on the CLI, - # then we'll attempt to remove ``--version`` so that we can avoid - # triggering the "version" action in optparse. If it's not there, we - # do not need to worry and we can continue. If it is, we successfully - # defer printing the version until just a little bit later. - # Similarly we have to defer printing the help text until later. - args = (argv if argv is not None else sys.argv)[:] - try: - args.remove("--version") - except ValueError: - pass - try: - args.remove("--help") - except ValueError: - pass - try: - args.remove("-h") - except ValueError: - pass - - opts, args = self.option_manager.parse_known_args(args) - # parse_known_args includes program name and unknown options as args - args = [a for a in args[1:] if not a.startswith("-")] - self.prelim_opts, self.prelim_args = opts, args + args, rest = self.prelim_arg_parser.parse_known_args(argv) + # XXX (ericvw): Special case "forwarding" the output file option so + # that it can be reparsed again for the BaseFormatter.filename. + if args.output_file: + rest.extend(("--output-file", args.output_file)) + return args, rest def exit(self): # type: () -> None @@ -150,51 +129,32 @@ if self.options.count: print(self.result_count) - if not self.options.exit_zero: + if self.options.exit_zero: + raise SystemExit(self.catastrophic_failure) + else: raise SystemExit( (self.result_count > 0) or self.catastrophic_failure ) - def make_config_finder(self): - """Make our ConfigFileFinder based on preliminary opts and args.""" - if self.config_finder is None: - extra_config_files = utils.normalize_paths( - self.prelim_opts.append_config - ) - self.config_finder = config.ConfigFileFinder( - self.option_manager.program_name, - self.prelim_args, - extra_config_files, - ) - - def find_plugins(self): - # type: () -> None + def find_plugins(self, config_finder): + # type: (config.ConfigFileFinder) -> None """Find and load the plugins for this application. - If :attr:`check_plugins`, or :attr:`formatting_plugins` are ``None`` - then this method will update them with the appropriate plugin manager - instance. Given the expense of finding plugins (via :mod:`entrypoints`) - we want this to be idempotent and so only update those attributes if - they are ``None``. + Set the :attr:`check_plugins` and :attr:`formatting_plugins` attributes + based on the discovered plugins found. + + :param config.ConfigFileFinder config_finder: + The finder for finding and reading configuration files. """ - if self.local_plugins is None: - self.local_plugins = config.get_local_plugins( - self.config_finder, - self.prelim_opts.config, - self.prelim_opts.isolated, - ) + local_plugins = config.get_local_plugins(config_finder) - sys.path.extend(self.local_plugins.paths) + sys.path.extend(local_plugins.paths) - if self.check_plugins is None: - self.check_plugins = plugin_manager.Checkers( - self.local_plugins.extension - ) + self.check_plugins = plugin_manager.Checkers(local_plugins.extension) - if self.formatting_plugins is None: - self.formatting_plugins = plugin_manager.ReportFormatters( - self.local_plugins.report - ) + self.formatting_plugins = plugin_manager.ReportFormatters( + local_plugins.report + ) self.check_plugins.load_plugins() self.formatting_plugins.load_plugins() @@ -206,17 +166,22 @@ self.check_plugins.register_plugin_versions(self.option_manager) self.formatting_plugins.register_options(self.option_manager) - def parse_configuration_and_cli(self, argv=None): - # type: (Optional[List[str]]) -> None + def parse_configuration_and_cli( + self, + config_finder, # type: config.ConfigFileFinder + argv, # type: List[str] + ): + # type: (...) -> None """Parse configuration files and the CLI options. + :param config.ConfigFileFinder config_finder: + The finder for finding and reading configuration files. :param list argv: Command-line arguments passed in directly. """ - if self.options is None and self.args is None: - self.options, self.args = aggregator.aggregate_options( - self.option_manager, self.config_finder, argv - ) + self.options, self.args = aggregator.aggregate_options( + self.option_manager, config_finder, argv, + ) self.running_against_diff = self.options.diff if self.running_against_diff: @@ -249,25 +214,23 @@ def make_formatter(self, formatter_class=None): # type: (Optional[Type[BaseFormatter]]) -> None """Initialize a formatter based on the parsed options.""" - if self.formatter is None: - format_plugin = self.options.format - if 1 <= self.options.quiet < 2: - format_plugin = "quiet-filename" - elif 2 <= self.options.quiet: - format_plugin = "quiet-nothing" + format_plugin = self.options.format + if 1 <= self.options.quiet < 2: + format_plugin = "quiet-filename" + elif 2 <= self.options.quiet: + format_plugin = "quiet-nothing" - if formatter_class is None: - formatter_class = self.formatter_for(format_plugin) + if formatter_class is None: + formatter_class = self.formatter_for(format_plugin) - self.formatter = formatter_class(self.options) + self.formatter = formatter_class(self.options) def make_guide(self): # type: () -> None """Initialize our StyleGuide.""" - if self.guide is None: - self.guide = style_guide.StyleGuideManager( - self.options, self.formatter - ) + self.guide = style_guide.StyleGuideManager( + self.options, self.formatter + ) if self.running_against_diff: self.guide.add_diff_ranges(self.parsed_diff) @@ -275,12 +238,11 @@ def make_file_checker_manager(self): # type: () -> None """Initialize our FileChecker Manager.""" - if self.file_checker_manager is None: - self.file_checker_manager = checker.Manager( - style_guide=self.guide, - arguments=self.args, - checker_plugins=self.check_plugins, - ) + self.file_checker_manager = checker.Manager( + style_guide=self.guide, + arguments=self.args, + checker_plugins=self.check_plugins, + ) def run_checks(self, files=None): # type: (Optional[List[str]]) -> None @@ -347,7 +309,7 @@ self.formatter.show_statistics(self.guide.stats) def initialize(self, argv): - # type: (Optional[List[str]]) -> None + # type: (List[str]) -> None """Initialize the application to be run. This finds the plugins, registers their options, and parses the @@ -355,14 +317,19 @@ """ # NOTE(sigmavirus24): When updating this, make sure you also update # our legacy API calls to these same methods. - self.parse_preliminary_options_and_args(argv) - flake8.configure_logging( - self.prelim_opts.verbose, self.prelim_opts.output_file + prelim_opts, remaining_args = self.parse_preliminary_options(argv) + flake8.configure_logging(prelim_opts.verbose, prelim_opts.output_file) + config_finder = config.ConfigFileFinder( + self.program, + prelim_opts.append_config, + config_file=prelim_opts.config, + ignore_config_files=prelim_opts.isolated, ) - self.make_config_finder() - self.find_plugins() + self.find_plugins(config_finder) self.register_plugin_options() - self.parse_configuration_and_cli(argv) + self.parse_configuration_and_cli( + config_finder, remaining_args, + ) self.make_formatter() self.make_guide() self.make_file_checker_manager() @@ -376,13 +343,13 @@ self.formatter.stop() def _run(self, argv): - # type: (Optional[List[str]]) -> None + # type: (List[str]) -> None self.initialize(argv) self.run_checks() self.report() - def run(self, argv=None): - # type: (Optional[List[str]]) -> None + def run(self, argv): + # type: (List[str]) -> None """Run our application. This method will also handle KeyboardInterrupt exceptions for the diff -Nru python-flake8-3.7.9/src/flake8/main/cli.py python-flake8-3.8.3/src/flake8/main/cli.py --- python-flake8-3.7.9/src/flake8/main/cli.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/src/flake8/main/cli.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,4 +1,5 @@ """Command-line implementation of flake8.""" +import sys from typing import List, Optional from flake8.main import application @@ -14,6 +15,9 @@ :param list argv: The arguments to be passed to the application for parsing. """ + if argv is None: + argv = sys.argv[1:] + app = application.Application() app.run(argv) app.exit() diff -Nru python-flake8-3.7.9/src/flake8/main/debug.py python-flake8-3.8.3/src/flake8/main/debug.py --- python-flake8-3.7.9/src/flake8/main/debug.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/src/flake8/main/debug.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,41 +1,37 @@ """Module containing the logic for our debugging logic.""" from __future__ import print_function +import argparse import json import platform +from typing import Dict, List -import entrypoints +class DebugAction(argparse.Action): + """argparse action to print debug information.""" -def print_information( - option, option_string, value, parser, option_manager=None -): - """Print debugging information used in bug reports. - - :param option: - The optparse Option instance. - :type option: - optparse.Option - :param str option_string: - The option name - :param value: - The value passed to the callback parsed from the command-line - :param parser: - The optparse OptionParser instance - :type parser: - optparse.OptionParser - :param option_manager: - The Flake8 OptionManager instance. - :type option_manager: - flake8.options.manager.OptionManager - """ - if not option_manager.registered_plugins: + def __init__(self, *args, **kwargs): + """Initialize the action. + + This takes an extra `option_manager` keyword argument which will be + used to delay response. + """ + self._option_manager = kwargs.pop("option_manager") + super(DebugAction, self).__init__(*args, **kwargs) + + def __call__(self, parser, namespace, values, option_string=None): + """Perform the argparse action for printing debug information.""" # NOTE(sigmavirus24): Flake8 parses options twice. The first time, we # will not have any registered plugins. We can skip this one and only # take action on the second time we're called. - return - print(json.dumps(information(option_manager), indent=2, sort_keys=True)) - raise SystemExit(False) + if not self._option_manager.registered_plugins: + return + print( + json.dumps( + information(self._option_manager), indent=2, sort_keys=True + ) + ) + raise SystemExit(0) def information(option_manager): @@ -64,6 +60,6 @@ ] -def dependencies(): +def dependencies(): # type: () -> List[Dict[str, str]] """Generate the list of dependencies we care about.""" - return [{"dependency": "entrypoints", "version": entrypoints.__version__}] + return [] diff -Nru python-flake8-3.7.9/src/flake8/main/options.py python-flake8-3.8.3/src/flake8/main/options.py --- python-flake8-3.7.9/src/flake8/main/options.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/src/flake8/main/options.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,19 +1,102 @@ """Contains the logic for all of the default options for Flake8.""" +import argparse +import functools + from flake8 import defaults from flake8.main import debug from flake8.main import vcs +def register_preliminary_options(parser): + # type: (argparse.ArgumentParser) -> None + """Register the preliminary options on our OptionManager. + + The preliminary options include: + + - ``-v``/``--verbose`` + - ``--output-file`` + - ``--append-config`` + - ``--config`` + - ``--isolated`` + """ + add_argument = parser.add_argument + + add_argument( + "-v", + "--verbose", + default=0, + action="count", + help="Print more information about what is happening in flake8." + " This option is repeatable and will increase verbosity each " + "time it is repeated.", + ) + + add_argument( + "--output-file", default=None, help="Redirect report to a file." + ) + + # Config file options + + add_argument( + "--append-config", + action="append", + help="Provide extra config files to parse in addition to the files " + "found by Flake8 by default. These files are the last ones read " + "and so they take the highest precedence when multiple files " + "provide the same option.", + ) + + add_argument( + "--config", + default=None, + help="Path to the config file that will be the authoritative config " + "source. This will cause Flake8 to ignore all other " + "configuration files.", + ) + + add_argument( + "--isolated", + default=False, + action="store_true", + help="Ignore all configuration files.", + ) + + +class JobsArgument: + """Type callback for the --jobs argument.""" + + def __init__(self, arg): # type: (str) -> None + """Parse and validate the --jobs argument. + + :param str arg: + The argument passed by argparse for validation + """ + self.is_auto = False + self.n_jobs = -1 + if arg == "auto": + self.is_auto = True + elif arg.isdigit(): + self.n_jobs = int(arg) + else: + raise argparse.ArgumentTypeError( + "{!r} must be 'auto' or an integer.".format(arg), + ) + + def __str__(self): + """Format our JobsArgument class.""" + return "auto" if self.is_auto else str(self.n_jobs) + + def register_default_options(option_manager): """Register the default options on our OptionManager. The default options include: - - ``-v``/``--verbose`` - ``-q``/``--quiet`` - ``--count`` - ``--diff`` - ``--exclude`` + - ``--extend-exclude`` - ``--filename`` - ``--format`` - ``--hang-closing`` @@ -29,11 +112,7 @@ - ``--enable-extensions`` - ``--exit-zero`` - ``-j``/``--jobs`` - - ``--output-file`` - ``--tee`` - - ``--append-config`` - - ``--config`` - - ``--isolated`` - ``--benchmark`` - ``--bug-report`` """ @@ -41,16 +120,6 @@ # pep8 options add_option( - "-v", - "--verbose", - default=0, - action="count", - parse_from_config=True, - help="Print more information about what is happening in flake8." - " This option is repeatable and will increase verbosity each " - "time it is repeated.", - ) - add_option( "-q", "--quiet", default=0, @@ -82,7 +151,18 @@ parse_from_config=True, normalize_paths=True, help="Comma-separated list of files or directories to exclude." - " (Default: %default)", + " (Default: %(default)s)", + ) + + add_option( + "--extend-exclude", + metavar="patterns", + default="", + parse_from_config=True, + comma_separated_list=True, + normalize_paths=True, + help="Comma-separated list of files or directories to add to the list" + " of excluded ones.", ) add_option( @@ -92,7 +172,7 @@ parse_from_config=True, comma_separated_list=True, help="Only check for filenames matching the patterns in this comma-" - "separated list. (Default: %default)", + "separated list. (Default: %(default)s)", ) add_option( @@ -100,7 +180,7 @@ default="stdin", help="The name used when reporting errors from code passed via stdin." " This is useful for editors piping the file contents to flake8." - " (Default: %default)", + " (Default: %(default)s)", ) # TODO(sigmavirus24): Figure out --first/--repeat @@ -131,7 +211,7 @@ parse_from_config=True, comma_separated_list=True, help="Comma-separated list of errors and warnings to ignore (or skip)." - " For example, ``--ignore=E4,E51,W234``. (Default: %default)", + " For example, ``--ignore=E4,E51,W234``. (Default: %(default)s)", ) add_option( @@ -157,22 +237,22 @@ add_option( "--max-line-length", - type="int", + type=int, metavar="n", default=defaults.MAX_LINE_LENGTH, parse_from_config=True, help="Maximum allowed line length for the entirety of this run. " - "(Default: %default)", + "(Default: %(default)s)", ) add_option( "--max-doc-length", - type="int", + type=int, metavar="n", default=None, parse_from_config=True, help="Maximum allowed doc line length for the entirety of this run. " - "(Default: %default)", + "(Default: %(default)s)", ) add_option( @@ -182,7 +262,7 @@ parse_from_config=True, comma_separated_list=True, help="Comma-separated list of errors and warnings to enable." - " For example, ``--select=E4,E51,W234``. (Default: %default)", + " For example, ``--select=E4,E51,W234``. (Default: %(default)s)", ) add_option( @@ -216,7 +296,6 @@ default="", parse_from_config=True, comma_separated_list=True, - type="string", help="Enable plugins and extensions that are otherwise disabled " "by default", ) @@ -229,10 +308,8 @@ add_option( "--install-hook", - action="callback", - type="choice", + action=vcs.InstallAction, choices=vcs.choices(), - callback=vcs.install, help="Install a hook that is run prior to a commit for the supported " "version control system.", ) @@ -240,22 +317,13 @@ add_option( "-j", "--jobs", - type="string", default="auto", parse_from_config=True, + type=JobsArgument, help="Number of subprocesses to use to run checks in parallel. " 'This is ignored on Windows. The default, "auto", will ' "auto-detect the number of processors available to use." - " (Default: %default)", - ) - - add_option( - "--output-file", - default=None, - type="string", - parse_from_config=True, - # callback=callbacks.redirect_stdout, - help="Redirect report to a file.", + " (Default: %(default)s)", ) add_option( @@ -266,32 +334,6 @@ help="Write to stdout and output-file.", ) - # Config file options - - add_option( - "--append-config", - action="append", - help="Provide extra config files to parse in addition to the files " - "found by Flake8 by default. These files are the last ones read " - "and so they take the highest precedence when multiple files " - "provide the same option.", - ) - - add_option( - "--config", - default=None, - help="Path to the config file that will be the authoritative config " - "source. This will cause Flake8 to ignore all other " - "configuration files.", - ) - - add_option( - "--isolated", - default=False, - action="store_true", - help="Ignore all configuration files.", - ) - # Benchmarking add_option( @@ -305,8 +347,9 @@ add_option( "--bug-report", - action="callback", - callback=debug.print_information, - callback_kwargs={"option_manager": option_manager}, + action=functools.partial( + debug.DebugAction, option_manager=option_manager + ), + nargs=0, help="Print information necessary when preparing a bug report", ) diff -Nru python-flake8-3.7.9/src/flake8/main/setuptools_command.py python-flake8-3.8.3/src/flake8/main/setuptools_command.py --- python-flake8-3.7.9/src/flake8/main/setuptools_command.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/src/flake8/main/setuptools_command.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,4 +1,5 @@ """The logic for Flake8's integration with setuptools.""" +from distutils import log import os from typing import List, Tuple @@ -105,3 +106,10 @@ # other possibly remaining/pending setuptools commands). if e.code: raise + finally: + self.announce( + "WARNING: flake8 setuptools integration is deprecated and " + "scheduled for removal in 4.x. For more information, see " + "https://gitlab.com/pycqa/flake8/issues/544", + log.WARN, + ) diff -Nru python-flake8-3.7.9/src/flake8/main/vcs.py python-flake8-3.8.3/src/flake8/main/vcs.py --- python-flake8-3.7.9/src/flake8/main/vcs.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/src/flake8/main/vcs.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,4 +1,9 @@ """Module containing some of the logic for our VCS installation logic.""" +from __future__ import print_function + +import argparse +import sys + from flake8 import exceptions as exc from flake8.main import git from flake8.main import mercurial @@ -11,24 +16,31 @@ _INSTALLERS = {"git": git.install, "mercurial": mercurial.install} -def install(option, option_string, value, parser): - """Determine which version control hook to install. +class InstallAction(argparse.Action): + """argparse action to run the hook installation.""" + + def __call__(self, parser, namespace, value, option_string=None): + """Perform the argparse action for installing vcs hooks.""" + installer = _INSTALLERS[value] + errored = False + successful = False + try: + successful = installer() + except exc.HookInstallationError as hook_error: + print(str(hook_error)) + errored = True + + if not successful: + print("Could not find the {0} directory".format(value)) + + print( + "\nWARNING: flake8 vcs hooks integration is deprecated and " + "scheduled for removal in 4.x. For more information, see " + "https://gitlab.com/pycqa/flake8/issues/568", + file=sys.stderr, + ) - For more information about the callback signature, see: - https://docs.python.org/3/library/optparse.html#optparse-option-callbacks - """ - installer = _INSTALLERS[value] - errored = False - successful = False - try: - successful = installer() - except exc.HookInstallationError as hook_error: - print(str(hook_error)) - errored = True - - if not successful: - print("Could not find the {0} directory".format(value)) - raise SystemExit(not successful and errored) + raise SystemExit(not successful and errored) def choices(): diff -Nru python-flake8-3.7.9/src/flake8/options/aggregator.py python-flake8-3.8.3/src/flake8/options/aggregator.py --- python-flake8-3.7.9/src/flake8/options/aggregator.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/src/flake8/options/aggregator.py 2020-06-08 12:15:48.000000000 -0700 @@ -3,37 +3,38 @@ This holds the logic that uses the collected and merged config files and applies the user-specified command-line configuration on top of it. """ +import argparse import logging +from typing import List, Tuple from flake8.options import config +from flake8.options.manager import OptionManager LOG = logging.getLogger(__name__) -def aggregate_options(manager, config_finder, arglist=None, values=None): +def aggregate_options( + manager, # type: OptionManager + config_finder, # type: config.ConfigFileFinder + argv, # type: List[str] +): # type: (...) -> Tuple[argparse.Namespace, List[str]] """Aggregate and merge CLI and config file options. :param flake8.options.manager.OptionManager manager: The instance of the OptionManager that we're presently using. :param flake8.options.config.ConfigFileFinder config_finder: The config file finder to use. - :param list arglist: - The list of arguments to pass to ``manager.parse_args``. In most cases - this will be None so ``parse_args`` uses ``sys.argv``. This is mostly - available to make testing easier. - :param optparse.Values values: - Previously parsed set of parsed options. + :param list argv: + The list of remaining command-line argumentsthat were unknown during + preliminary option parsing to pass to ``manager.parse_args``. :returns: Tuple of the parsed options and extra arguments returned by ``manager.parse_args``. :rtype: - tuple(optparse.Values, list) + tuple(argparse.Namespace, list) """ # Get defaults from the option parser - default_values, _ = manager.parse_args([], values=values) - # Get original CLI values so we can find additional config file paths and - # see if --config was specified. - original_values, _ = manager.parse_args(arglist) + default_values, _ = manager.parse_args([]) # Make our new configuration file mergerator config_parser = config.MergedConfigParser( @@ -41,9 +42,7 @@ ) # Get the parsed config - parsed_config = config_parser.parse( - original_values.config, original_values.isolated - ) + parsed_config = config_parser.parse() # Extend the default ignore value with the extended default ignore list, # registered by plugins. @@ -79,4 +78,4 @@ setattr(default_values, dest_name, value) # Finally parse the command-line options - return manager.parse_args(arglist, default_values) + return manager.parse_args(argv, default_values) diff -Nru python-flake8-3.7.9/src/flake8/options/config.py python-flake8-3.8.3/src/flake8/options/config.py --- python-flake8-3.7.9/src/flake8/options/config.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/src/flake8/options/config.py 2020-06-08 12:15:48.000000000 -0700 @@ -3,8 +3,7 @@ import configparser import logging import os.path -import sys -from typing import Dict, List, Sequence, Tuple, Union +from typing import List, Optional, Tuple from flake8 import utils @@ -16,57 +15,63 @@ class ConfigFileFinder(object): """Encapsulate the logic for finding and reading config files.""" - def __init__(self, program_name, args, extra_config_files): + def __init__( + self, + program_name, + extra_config_files=None, + config_file=None, + ignore_config_files=False, + ): + # type: (str, Optional[List[str]], Optional[str], bool) -> None """Initialize object to find config files. :param str program_name: Name of the current program (e.g., flake8). - :param list args: - The extra arguments passed on the command-line. :param list extra_config_files: Extra configuration files specified by the user to read. + :param str config_file: + Configuration file override to only read configuraiton from. + :param bool ignore_config_files: + Determine whether to ignore configuration files or not. """ # The values of --append-config from the CLI - extra_config_files = extra_config_files or [] - self.extra_config_files = [ - # Ensure the paths are absolute paths for local_config_files - os.path.abspath(f) - for f in extra_config_files - ] - - # Platform specific settings - self.is_windows = sys.platform == "win32" - self.xdg_home = os.environ.get( - "XDG_CONFIG_HOME", os.path.expanduser("~/.config") - ) + if extra_config_files is None: + extra_config_files = [] + self.extra_config_files = utils.normalize_paths(extra_config_files) + + # The value of --config from the CLI. + self.config_file = config_file - # Look for '.' files - self.program_config = "." + program_name + # The value of --isolated from the CLI. + self.ignore_config_files = ignore_config_files + + # User configuration file. self.program_name = program_name + self.user_config_file = self._user_config_file(program_name) # List of filenames to find in the local/project directory - self.project_filenames = ("setup.cfg", "tox.ini", self.program_config) + self.project_filenames = ("setup.cfg", "tox.ini", "." + program_name) self.local_directory = os.path.abspath(os.curdir) - if not args: - args = ["."] - self.parent = self.tail = os.path.abspath(os.path.commonprefix(args)) - - # caches to avoid double-reading config files - self._local_configs = None - self._local_found_files = [] # type: List[str] - self._user_config = None - # fmt: off - self._cli_configs = {} # type: Dict[str, configparser.RawConfigParser] - # fmt: on + @staticmethod + def _user_config_file(program_name): + # type: (str) -> str + if utils.is_windows(): + home_dir = os.path.expanduser("~") + config_file_basename = "." + program_name + else: + home_dir = os.environ.get( + "XDG_CONFIG_HOME", os.path.expanduser("~/.config") + ) + config_file_basename = program_name + + return os.path.join(home_dir, config_file_basename) @staticmethod - def _read_config(files): - # type: (Union[Sequence[str], str]) -> Tuple[configparser.RawConfigParser, List[str]] # noqa: E501 + def _read_config(*files): + # type: (*str) -> Tuple[configparser.RawConfigParser, List[str]] config = configparser.RawConfigParser() - if isinstance(files, (str, type(u""))): - files = [files] found_files = [] for filename in files: @@ -89,17 +94,14 @@ def cli_config(self, files): # type: (str) -> configparser.RawConfigParser """Read and parse the config file specified on the command-line.""" - if files not in self._cli_configs: - config, found_files = self._read_config(files) - if found_files: - LOG.debug("Found cli configuration files: %s", found_files) - self._cli_configs[files] = config - return self._cli_configs[files] + config, found_files = self._read_config(files) + if found_files: + LOG.debug("Found cli configuration files: %s", found_files) + return config def generate_possible_local_files(self): """Find and generate all local config files.""" - tail = self.tail - parent = self.parent + parent = tail = os.getcwd() found_config_files = False while tail and not found_config_files: for project_filename in self.project_filenames: @@ -135,32 +137,21 @@ Return (config, found_config_files) tuple. """ - if self._local_configs is None: - config, found_files = self._read_config(self.local_config_files()) - if found_files: - LOG.debug("Found local configuration files: %s", found_files) - self._local_configs = config - self._local_found_files = found_files - return (self._local_configs, self._local_found_files) + config, found_files = self._read_config(*self.local_config_files()) + if found_files: + LOG.debug("Found local configuration files: %s", found_files) + return (config, found_files) def local_configs(self): """Parse all local config files into one config object.""" return self.local_configs_with_files()[0] - def user_config_file(self): - """Find the user-level config file.""" - if self.is_windows: - return os.path.expanduser("~\\" + self.program_config) - return os.path.join(self.xdg_home, self.program_name) - def user_config(self): """Parse the user config file into a config object.""" - if self._user_config is None: - config, found_files = self._read_config(self.user_config_file()) - if found_files: - LOG.debug("Found user configuration files: %s", found_files) - self._user_config = config - return self._user_config + config, found_files = self._read_config(self.user_config_file) + if found_files: + LOG.debug("Found user configuration files: %s", found_files) + return config class MergedConfigParser(object): @@ -171,9 +162,6 @@ dictionaries with the parsed values. """ - #: Set of types that should use the - #: :meth:`~configparser.RawConfigParser.getint` method. - GETINT_TYPES = {"int", "count"} #: Set of actions that should use the #: :meth:`~configparser.RawConfigParser.getbool` method. GETBOOL_ACTIONS = {"store_true", "store_false"} @@ -196,10 +184,11 @@ #: Our instance of our :class:`~ConfigFileFinder` self.config_finder = config_finder - def _normalize_value(self, option, value): - final_value = option.normalize( - value, self.config_finder.local_directory - ) + def _normalize_value(self, option, value, parent=None): + if parent is None: + parent = self.config_finder.local_directory + + final_value = option.normalize(value, parent) LOG.debug( '%r has been normalized to %r for option "%s"', value, @@ -208,7 +197,7 @@ ) return final_value - def _parse_config(self, config_parser): + def _parse_config(self, config_parser, parent=None): config_dict = {} for option_name in config_parser.options(self.program_name): if option_name not in self.config_options: @@ -220,10 +209,7 @@ # Use the appropriate method to parse the config value method = config_parser.get - if ( - option.type in self.GETINT_TYPES - or option.action in self.GETINT_TYPES - ): + if option.type is int or option.action == "count": method = config_parser.getint elif option.action in self.GETBOOL_ACTIONS: method = config_parser.getboolean @@ -231,7 +217,7 @@ value = method(self.program_name, option_name) LOG.debug('Option "%s" returned value: %r', option_name, value) - final_value = self._normalize_value(option, value) + final_value = self._normalize_value(option, value, parent) config_dict[option.config_name] = final_value return config_dict @@ -277,7 +263,7 @@ return {} LOG.debug("Parsing CLI configuration files.") - return self._parse_config(config) + return self._parse_config(config, os.path.dirname(config_path)) def merge_user_and_local_config(self): """Merge the parsed user and local configuration files. @@ -295,54 +281,42 @@ return config - def parse(self, cli_config=None, isolated=False): + def parse(self): """Parse and return the local and user config files. First this copies over the parsed local configuration and then iterates over the options in the user configuration and sets them if they were not set by the local configuration file. - :param str cli_config: - Value of --config when specified at the command-line. Overrides - all other config files. - :param bool isolated: - Determines if we should parse configuration files at all or not. - If running in isolated mode, we ignore all configuration files :returns: Dictionary of parsed configuration options :rtype: dict """ - if isolated: + if self.config_finder.ignore_config_files: LOG.debug( "Refusing to parse configuration files due to user-" "requested isolation" ) return {} - if cli_config: + if self.config_finder.config_file: LOG.debug( "Ignoring user and locally found configuration files. " 'Reading only configuration from "%s" specified via ' "--config by the user", - cli_config, + self.config_finder.config_file, ) - return self.parse_cli_config(cli_config) + return self.parse_cli_config(self.config_finder.config_file) return self.merge_user_and_local_config() -def get_local_plugins(config_finder, cli_config=None, isolated=False): +def get_local_plugins(config_finder): """Get local plugins lists from config files. :param flake8.options.config.ConfigFileFinder config_finder: The config file finder to use. - :param str cli_config: - Value of --config when specified at the command-line. Overrides - all other config files. - :param bool isolated: - Determines if we should parse configuration files at all or not. - If running in isolated mode, we ignore all configuration files :returns: LocalPlugins namedtuple containing two lists of plugin strings, one for extension (checker) plugins and one for report plugins. @@ -350,21 +324,21 @@ flake8.options.config.LocalPlugins """ local_plugins = LocalPlugins(extension=[], report=[], paths=[]) - if isolated: + if config_finder.ignore_config_files: LOG.debug( "Refusing to look for local plugins in configuration" "files due to user-requested isolation" ) return local_plugins - if cli_config: + if config_finder.config_file: LOG.debug( 'Reading local plugins only from "%s" specified via ' "--config by the user", - cli_config, + config_finder.config_file, ) - config = config_finder.cli_config(cli_config) - config_files = [cli_config] + config = config_finder.cli_config(config_finder.config_file) + config_files = [config_finder.config_file] else: config, config_files = config_finder.local_configs_with_files() diff -Nru python-flake8-3.7.9/src/flake8/options/manager.py python-flake8-3.8.3/src/flake8/options/manager.py --- python-flake8-3.7.9/src/flake8/options/manager.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/src/flake8/options/manager.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,55 +1,131 @@ """Option handling and Option management logic.""" +import argparse import collections +import contextlib +import enum +import functools import logging -import optparse # pylint: disable=deprecated-module -from typing import Any, Callable, Dict, List, Optional, Set +from typing import Any, Callable, cast, Dict, Generator, List, Mapping +from typing import Optional, Sequence, Set, Tuple, Union from flake8 import utils +if False: # TYPE_CHECKING + from typing import NoReturn + from typing import Type + LOG = logging.getLogger(__name__) +# represent a singleton of "not passed arguments". +# an enum is chosen to trick mypy +_ARG = enum.Enum("_ARG", "NO") + + +_optparse_callable_map = { + "int": int, + "long": int, + "string": str, + "float": float, + "complex": complex, + "choice": _ARG.NO, + # optparse allows this but does not document it + "str": str, +} # type: Dict[str, Union[Type[Any], _ARG]] + + +class _CallbackAction(argparse.Action): + """Shim for optparse-style callback actions.""" + + def __init__(self, *args, **kwargs): + # type: (*Any, **Any) -> None + self._callback = kwargs.pop("callback") + self._callback_args = kwargs.pop("callback_args", ()) + self._callback_kwargs = kwargs.pop("callback_kwargs", {}) + super(_CallbackAction, self).__init__(*args, **kwargs) + + def __call__( + self, + parser, # type: argparse.ArgumentParser + namespace, # type: argparse.Namespace + values, # type: Optional[Union[Sequence[str], str]] + option_string=None, # type: Optional[str] + ): + # type: (...) -> None + if not values: + values = None + elif isinstance(values, list) and len(values) > 1: + values = tuple(values) + self._callback( + self, + option_string, + values, + parser, + *self._callback_args, + **self._callback_kwargs + ) + + +def _flake8_normalize(value, *args, **kwargs): + # type: (str, *str, **bool) -> Union[str, List[str]] + comma_separated_list = kwargs.pop("comma_separated_list", False) + normalize_paths = kwargs.pop("normalize_paths", False) + if kwargs: + raise TypeError("Unexpected keyword args: {}".format(kwargs)) + + ret = value # type: Union[str, List[str]] + if comma_separated_list and isinstance(ret, utils.string_types): + ret = utils.parse_comma_separated_list(value) + + if normalize_paths: + if isinstance(ret, utils.string_types): + ret = utils.normalize_path(ret, *args) + else: + ret = utils.normalize_paths(ret, *args) + + return ret + class Option(object): - """Our wrapper around an optparse.Option object to add features.""" + """Our wrapper around an argparse argument parsers to add features.""" def __init__( self, - short_option_name=None, - long_option_name=None, + short_option_name=_ARG.NO, # type: Union[str, _ARG] + long_option_name=_ARG.NO, # type: Union[str, _ARG] # Options below here are taken from the optparse.Option class - action=None, - default=None, - type=None, - dest=None, - nargs=None, - const=None, - choices=None, - callback=None, - callback_args=None, - callback_kwargs=None, - help=None, - metavar=None, + action=_ARG.NO, # type: Union[str, Type[argparse.Action], _ARG] + default=_ARG.NO, # type: Union[Any, _ARG] + type=_ARG.NO, # type: Union[str, Callable[..., Any], _ARG] + dest=_ARG.NO, # type: Union[str, _ARG] + nargs=_ARG.NO, # type: Union[int, str, _ARG] + const=_ARG.NO, # type: Union[Any, _ARG] + choices=_ARG.NO, # type: Union[Sequence[Any], _ARG] + help=_ARG.NO, # type: Union[str, _ARG] + metavar=_ARG.NO, # type: Union[str, _ARG] + # deprecated optparse-only options + callback=_ARG.NO, # type: Union[Callable[..., Any], _ARG] + callback_args=_ARG.NO, # type: Union[Sequence[Any], _ARG] + callback_kwargs=_ARG.NO, # type: Union[Mapping[str, Any], _ARG] + # Options below are taken from argparse.ArgumentParser.add_argument + required=_ARG.NO, # type: Union[bool, _ARG] # Options below here are specific to Flake8 - parse_from_config=False, - comma_separated_list=False, - normalize_paths=False, - ): - """Initialize an Option instance wrapping optparse.Option. + parse_from_config=False, # type: bool + comma_separated_list=False, # type: bool + normalize_paths=False, # type: bool + ): # type: (...) -> None + """Initialize an Option instance. - The following are all passed directly through to optparse. + The following are all passed directly through to argparse. :param str short_option_name: The short name of the option (e.g., ``-x``). This will be the - first argument passed to :class:`~optparse.Option`. + first argument passed to ``ArgumentParser.add_argument`` :param str long_option_name: The long name of the option (e.g., ``--xtra-long-option``). This - will be the second argument passed to :class:`~optparse.Option`. - :param str action: - Any action allowed by :mod:`optparse`. + will be the second argument passed to + ``ArgumentParser.add_argument`` :param default: Default value of the option. - :param type: - Any type allowed by :mod:`optparse`. :param dest: Attribute name to store parsed option value as. :param nargs: @@ -59,16 +135,33 @@ conjuntion with ``action="store_const"``. :param iterable choices: Possible values for the option. - :param callable callback: - Callback used if the action is ``"callback"``. - :param iterable callback_args: - Additional positional arguments to the callback callable. - :param dictionary callback_kwargs: - Keyword arguments to the callback callable. :param str help: Help text displayed in the usage information. :param str metavar: Name to use instead of the long option name for help text. + :param bool required: + Whether this option is required or not. + + The following options may be passed directly through to :mod:`argparse` + but may need some massaging. + + :param type: + A callable to normalize the type (as is the case in + :mod:`argparse`). Deprecated: you can also pass through type + strings such as ``'int'`` which are handled by :mod:`optparse`. + :param str action: + Any action allowed by :mod:`argparse`. Deprecated: this also + understands the ``action='callback'`` action from :mod:`optparse`. + :param callable callback: + Callback used if the action is ``"callback"``. Deprecated: please + use ``action=`` instead. + :param iterable callback_args: + Additional positional arguments to the callback callable. + Deprecated: please use ``action=`` instead (probably with + ``functools.partial``). + :param dictionary callback_kwargs: + Keyword arguments to the callback callable. Deprecated: please + use ``action=`` instead (probably with ``functools.partial``). The following parameters are for Flake8's option handling alone. @@ -81,28 +174,87 @@ Whether the option is expecting a path or list of paths and should attempt to normalize the paths to absolute paths. """ + if ( + long_option_name is _ARG.NO + and short_option_name is not _ARG.NO + and short_option_name.startswith("--") + ): + short_option_name, long_option_name = _ARG.NO, short_option_name + + # optparse -> argparse `%default` => `%(default)s` + if help is not _ARG.NO and "%default" in help: + LOG.warning( + "option %s: please update `help=` text to use %%(default)s " + "instead of %%default -- this will be an error in the future", + long_option_name, + ) + help = help.replace("%default", "%(default)s") + + # optparse -> argparse for `callback` + if action == "callback": + LOG.warning( + "option %s: please update from optparse `action='callback'` " + "to argparse action classes -- this will be an error in the " + "future", + long_option_name, + ) + action = _CallbackAction + if type is _ARG.NO: + nargs = 0 + + # optparse -> argparse for `type` + if isinstance(type, utils.string_types): + LOG.warning( + "option %s: please update from optparse string `type=` to " + "argparse callable `type=` -- this will be an error in the " + "future", + long_option_name, + ) + type = _optparse_callable_map[type] + + # flake8 special type normalization + if comma_separated_list or normalize_paths: + type = functools.partial( + _flake8_normalize, + comma_separated_list=comma_separated_list, + normalize_paths=normalize_paths, + ) + self.short_option_name = short_option_name self.long_option_name = long_option_name self.option_args = [ - x for x in (short_option_name, long_option_name) if x is not None + x + for x in (short_option_name, long_option_name) + if x is not _ARG.NO ] + self.action = action + self.default = default + self.type = type + self.dest = dest + self.nargs = nargs + self.const = const + self.choices = choices + self.callback = callback + self.callback_args = callback_args + self.callback_kwargs = callback_kwargs + self.help = help + self.metavar = metavar + self.required = required self.option_kwargs = { - "action": action, - "default": default, - "type": type, - "dest": self._make_dest(dest), - "nargs": nargs, - "const": const, - "choices": choices, - "callback": callback, - "callback_args": callback_args, - "callback_kwargs": callback_kwargs, - "help": help, - "metavar": metavar, - } - # Set attributes for our option arguments - for key, value in self.option_kwargs.items(): - setattr(self, key, value) + "action": self.action, + "default": self.default, + "type": self.type, + "dest": self.dest, + "nargs": self.nargs, + "const": self.const, + "choices": self.choices, + "callback": self.callback, + "callback_args": self.callback_args, + "callback_kwargs": self.callback_kwargs, + "help": self.help, + "metavar": self.metavar, + "required": self.required, + } # type: Dict[str, Union[Any, _ARG]] # Set our custom attributes self.parse_from_config = parse_from_config @@ -111,7 +263,7 @@ self.config_name = None # type: Optional[str] if parse_from_config: - if not long_option_name: + if long_option_name is _ARG.NO: raise ValueError( "When specifying parse_from_config=True, " "a long_option_name must also be specified." @@ -120,46 +272,46 @@ self._opt = None - def __repr__(self): # noqa: D105 - return ( - "Option({0}, {1}, action={action}, default={default}, " - "dest={dest}, type={type}, callback={callback}, help={help}," - " callback={callback}, callback_args={callback_args}, " - "callback_kwargs={callback_kwargs}, metavar={metavar})" - ).format( - self.short_option_name, - self.long_option_name, - **self.option_kwargs - ) + @property + def filtered_option_kwargs(self): # type: () -> Dict[str, Any] + """Return any actually-specified arguments.""" + return { + k: v for k, v in self.option_kwargs.items() if v is not _ARG.NO + } - def _make_dest(self, dest): - if dest: - return dest - - if self.long_option_name: - return self.long_option_name[2:].replace("-", "_") - return self.short_option_name[1] + def __repr__(self): # type: () -> str # noqa: D105 + parts = [] + for arg in self.option_args: + parts.append(arg) + for k, v in self.filtered_option_kwargs.items(): + parts.append("{}={!r}".format(k, v)) + return "Option({})".format(", ".join(parts)) def normalize(self, value, *normalize_args): + # type: (Any, *str) -> Any """Normalize the value based on the option configuration.""" + if self.comma_separated_list and isinstance( + value, utils.string_types + ): + value = utils.parse_comma_separated_list(value) + if self.normalize_paths: - # Decide whether to parse a list of paths or a single path - normalize = utils.normalize_path # type: Callable[..., Any] - if self.comma_separated_list: - normalize = utils.normalize_paths - return normalize(value, *normalize_args) - elif self.comma_separated_list: - return utils.parse_comma_separated_list(value) + if isinstance(value, list): + value = utils.normalize_paths(value, *normalize_args) + else: + value = utils.normalize_path(value, *normalize_args) + return value def normalize_from_setuptools(self, value): + # type: (str) -> Union[int, float, complex, bool, str] """Normalize the value received from setuptools.""" value = self.normalize(value) - if self.type == "int" or self.action == "count": + if self.type is int or self.action == "count": return int(value) - elif self.type == "float": + elif self.type is float: return float(value) - elif self.type == "complex": + elif self.type is complex: return complex(value) if self.action in ("store_true", "store_false"): value = str(value).upper() @@ -169,13 +321,15 @@ return False return value - def to_optparse(self): - """Convert a Flake8 Option to an optparse Option.""" - if self._opt is None: - self._opt = optparse.Option( - *self.option_args, **self.option_kwargs - ) - return self._opt + def to_argparse(self): + # type: () -> Tuple[List[str], Dict[str, Any]] + """Convert a Flake8 Option to argparse ``add_argument`` arguments.""" + return self.option_args, self.filtered_option_kwargs + + @property + def to_optparse(self): # type: () -> NoReturn + """No longer functional.""" + raise AttributeError("to_optparse: flake8 now uses argparse") PluginVersion = collections.namedtuple( @@ -187,8 +341,12 @@ """Manage Options and OptionParser while adding post-processing.""" def __init__( - self, prog=None, version=None, usage="%prog [options] file file ..." - ): + self, + prog, + version, + usage="%(prog)s [options] file file ...", + parents=None, + ): # type: (str, str, str, Optional[List[argparse.ArgumentParser]]) -> None # noqa: E501 """Initialize an instance of an OptionManager. :param str prog: @@ -197,10 +355,24 @@ Version string for the program. :param str usage: Basic usage string used by the OptionParser. + :param argparse.ArgumentParser parents: + A list of ArgumentParser objects whose arguments should also be + included. """ - self.parser = optparse.OptionParser( - prog=prog, version=version, usage=usage + if parents is None: + parents = [] + + self.parser = argparse.ArgumentParser( + prog=prog, usage=usage, parents=parents + ) # type: argparse.ArgumentParser + self._current_group = None # type: Optional[argparse._ArgumentGroup] + self.version_action = cast( + "argparse._VersionAction", + self.parser.add_argument( + "--version", action="version", version=version + ), ) + self.parser.add_argument("filenames", nargs="*", metavar="filename") self.config_options_dict = {} # type: Dict[str, Option] self.options = [] # type: List[Option] self.program_name = prog @@ -209,12 +381,17 @@ self.extended_default_ignore = set() # type: Set[str] self.extended_default_select = set() # type: Set[str] - @staticmethod - def format_plugin(plugin): - """Convert a PluginVersion into a dictionary mapping name to value.""" - return {attr: getattr(plugin, attr) for attr in ["name", "version"]} + @contextlib.contextmanager + def group(self, name): # type: (str) -> Generator[None, None, None] + """Attach options to an argparse group during this context.""" + group = self.parser.add_argument_group(name) + self._current_group, orig_group = group, self._current_group + try: + yield + finally: + self._current_group = orig_group - def add_option(self, *args, **kwargs): + def add_option(self, *args, **kwargs): # type: (*Any, **Any) -> None """Create and register a new option. See parameters for :class:`~flake8.options.manager.Option` for @@ -223,12 +400,14 @@ .. note:: ``short_option_name`` and ``long_option_name`` may be specified - positionally as they are with optparse normally. + positionally as they are with argparse normally. """ - if len(args) == 1 and args[0].startswith("--"): - args = (None, args[0]) option = Option(*args, **kwargs) - self.parser.add_option(option.to_optparse()) + option_args, option_kwargs = option.to_argparse() + if self._current_group is not None: + self._current_group.add_argument(*option_args, **option_kwargs) + else: + self.parser.add_argument(*option_args, **option_kwargs) self.options.append(option) if option.parse_from_config: name = option.config_name @@ -238,6 +417,7 @@ LOG.debug('Registered option "%s".', option) def remove_from_default_ignore(self, error_codes): + # type: (Sequence[str]) -> None """Remove specified error codes from the default ignore list. :param list error_codes: @@ -256,6 +436,7 @@ ) def extend_default_ignore(self, error_codes): + # type: (Sequence[str]) -> None """Extend the default ignore list with the error codes provided. :param list error_codes: @@ -266,6 +447,7 @@ self.extended_default_ignore.update(error_codes) def extend_default_select(self, error_codes): + # type: (Sequence[str]) -> None """Extend the default select list with the error codes provided. :param list error_codes: @@ -278,43 +460,43 @@ def generate_versions( self, format_str="%(name)s: %(version)s", join_on=", " ): + # type: (str, str) -> str """Generate a comma-separated list of versions of plugins.""" return join_on.join( - format_str % self.format_plugin(plugin) + format_str % plugin._asdict() for plugin in sorted(self.registered_plugins) ) - def update_version_string(self): + def update_version_string(self): # type: () -> None """Update the flake8 version string.""" - self.parser.version = ( - self.version - + " (" - + self.generate_versions() - + ") " - + utils.get_python_version() + self.version_action.version = "{} ({}) {}".format( + self.version, self.generate_versions(), utils.get_python_version() ) - def generate_epilog(self): + def generate_epilog(self): # type: () -> None """Create an epilog with the version and name of each of plugin.""" plugin_version_format = "%(name)s: %(version)s" self.parser.epilog = "Installed plugins: " + self.generate_versions( plugin_version_format ) - def _normalize(self, options): - for option in self.options: - old_value = getattr(options, option.dest) - setattr(options, option.dest, option.normalize(old_value)) - - def parse_args(self, args=None, values=None): + def parse_args( + self, + args=None, # type: Optional[List[str]] + values=None, # type: Optional[argparse.Namespace] + ): + # type: (...) -> Tuple[argparse.Namespace, List[str]] """Proxy to calling the OptionParser's parse_args method.""" self.generate_epilog() self.update_version_string() - options, xargs = self.parser.parse_args(args, values) - self._normalize(options) - return options, xargs + if values: + self.parser.set_defaults(**vars(values)) + parsed_args = self.parser.parse_args(args) + # TODO: refactor callers to not need this + return parsed_args, parsed_args.filenames - def parse_known_args(self, args=None, values=None): + def parse_known_args(self, args=None): + # type: (Optional[List[str]]) -> Tuple[argparse.Namespace, List[str]] """Parse only the known arguments from the argument values. Replicate a little argparse behaviour while we're still on @@ -322,35 +504,10 @@ """ self.generate_epilog() self.update_version_string() - # Taken from optparse.OptionParser.parse_args - rargs = self.parser._get_args(args) - if values is None: - values = self.parser.get_default_values() - - self.parser.rargs = rargs - largs = [] # type: List[str] - self.parser.values = values - - while rargs: - # NOTE(sigmavirus24): If we only care about *known* options, then - # we should just shift the bad option over to the largs list and - # carry on. - # Unfortunately, we need to rely on a private method here. - try: - self.parser._process_args(largs, rargs, values) - except ( - optparse.BadOptionError, - optparse.OptionValueError, - ) as err: - # TODO: https://gitlab.com/pycqa/flake8/issues/541 - largs.append(err.opt_str) # type: ignore - - args = largs + rargs - options, xargs = self.parser.check_values(values, args) - self._normalize(options) - return options, xargs + return self.parser.parse_known_args(args) def register_plugin(self, name, version, local=False): + # type: (str, str, bool) -> None """Register a plugin relying on the OptionManager. :param str name: diff -Nru python-flake8-3.7.9/src/flake8/plugins/manager.py python-flake8-3.8.3/src/flake8/plugins/manager.py --- python-flake8-3.7.9/src/flake8/plugins/manager.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/src/flake8/plugins/manager.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,11 +1,10 @@ """Plugin loading and management logic and classes.""" import logging -from typing import Any, Dict, List, Set - -import entrypoints +from typing import Any, Dict, List, Optional, Set from flake8 import exceptions from flake8 import utils +from flake8._compat import importlib_metadata LOG = logging.getLogger(__name__) @@ -34,15 +33,15 @@ self.local = local self._plugin = None # type: Any self._parameters = None - self._parameter_names = None + self._parameter_names = None # type: Optional[List[str]] self._group = None self._plugin_name = None self._version = None - def __repr__(self): + def __repr__(self): # type: () -> str """Provide an easy to read description of the current plugin.""" return 'Plugin(name="{0}", entry_point="{1}")'.format( - self.name, self.entry_point + self.name, self.entry_point.value ) def to_dictionary(self): @@ -85,7 +84,7 @@ return self._parameters @property - def parameter_names(self): + def parameter_names(self): # type: () -> List[str] """List of argument names that need to be passed to the plugin.""" if self._parameter_names is None: self._parameter_names = list(self.parameters) @@ -101,15 +100,15 @@ return self._plugin @property - def version(self): + def version(self): # type: () -> str """Return the version of the plugin.""" - if self._version is None: + version = self._version + if version is None: if self.is_in_a_group(): - self._version = version_for(self) + version = self._version = version_for(self) else: - self._version = self.plugin.version - - return self._version + version = self._version = self.plugin.version + return version @property def plugin_name(self): @@ -159,7 +158,7 @@ except Exception as load_exception: LOG.exception(load_exception) failed_to_load = exceptions.FailedToLoadPlugin( - plugin=self, exception=load_exception + plugin_name=self.name, exception=load_exception ) LOG.critical(str(failed_to_load)) raise failed_to_load @@ -213,7 +212,8 @@ self.name, optmanager, ) - add_options(optmanager) + with optmanager.group(self.plugin_name): + add_options(optmanager) if self.off_by_default: self.disable(optmanager) @@ -223,6 +223,7 @@ """Find and manage plugins consistently.""" def __init__(self, namespace, local_plugins=None): + # type: (str, Optional[List[str]]) -> None """Initialize the manager. :param str namespace: @@ -245,12 +246,16 @@ for plugin_str in local_plugins: name, _, entry_str = plugin_str.partition("=") name, entry_str = name.strip(), entry_str.strip() - entry_point = entrypoints.EntryPoint.from_string(entry_str, name) + entry_point = importlib_metadata.EntryPoint(name, entry_str, None) self._load_plugin_from_entrypoint(entry_point, local=True) def _load_entrypoint_plugins(self): LOG.info('Loading entry-points for "%s".', self.namespace) - for entry_point in entrypoints.get_group_all(self.namespace): + eps = importlib_metadata.entry_points().get(self.namespace, ()) + # python2.7 occasionally gives duplicate results due to redundant + # `local/lib` -> `../lib` symlink on linux in virtualenvs so we + # eliminate duplicates here + for entry_point in sorted(frozenset(eps)): if entry_point.name == "per-file-ignores": LOG.warning( "flake8-per-file-ignores plugin is incompatible with " diff -Nru python-flake8-3.7.9/src/flake8/plugins/pyflakes.py python-flake8-3.8.3/src/flake8/plugins/pyflakes.py --- python-flake8-3.7.9/src/flake8/plugins/pyflakes.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/src/flake8/plugins/pyflakes.py 2020-06-08 12:15:48.000000000 -0700 @@ -26,6 +26,21 @@ "ImportStarUsage": "F405", "ImportStarNotPermitted": "F406", "FutureFeatureNotDefined": "F407", + "PercentFormatInvalidFormat": "F501", + "PercentFormatExpectedMapping": "F502", + "PercentFormatExpectedSequence": "F503", + "PercentFormatExtraNamedArguments": "F504", + "PercentFormatMissingArgument": "F505", + "PercentFormatMixedPositionalAndNamed": "F506", + "PercentFormatPositionalCountMismatch": "F507", + "PercentFormatStarRequiresSequence": "F508", + "PercentFormatUnsupportedFormatCharacter": "F509", + "StringDotFormatInvalidFormat": "F521", + "StringDotFormatExtraNamedArguments": "F522", + "StringDotFormatExtraPositionalArguments": "F523", + "StringDotFormatMissingArgument": "F524", + "StringDotFormatMixingAutomatic": "F525", + "FStringMissingPlaceholders": "F541", "MultiValueRepeatedKeyLiteral": "F601", "MultiValueRepeatedKeyVariable": "F602", "TooManyExpressionsInStarredAssignment": "F621", @@ -33,6 +48,7 @@ "AssertTuple": "F631", "IsLiteral": "F632", "InvalidPrintSyntax": "F633", + "IfTuple": "F634", "BreakOutsideLoop": "F701", "ContinueOutsideLoop": "F702", "ContinueInFinally": "F703", @@ -108,7 +124,7 @@ default=False, action="store_true", parse_from_config=True, - help="check syntax of the doctests", + help="also check syntax of the doctests", ) parser.add_option( "--include-in-doctest", @@ -118,7 +134,6 @@ comma_separated_list=True, normalize_paths=True, help="Run doctests only on these files", - type="string", ) parser.add_option( "--exclude-from-doctest", @@ -128,7 +143,6 @@ comma_separated_list=True, normalize_paths=True, help="Skip these files when running doctests", - type="string", ) @classmethod diff -Nru python-flake8-3.7.9/src/flake8/processor.py python-flake8-3.8.3/src/flake8/processor.py --- python-flake8-3.7.9/src/flake8/processor.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/src/flake8/processor.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,9 +1,11 @@ """Module containing our file processor that tokenizes a file for checks.""" +import argparse +import ast import contextlib import logging import sys import tokenize -from typing import Any, Dict, List, Tuple +from typing import Any, Dict, Generator, List, Optional, Tuple import flake8 from flake8 import defaults @@ -18,6 +20,10 @@ [tokenize.NL, tokenize.NEWLINE, tokenize.INDENT, tokenize.DEDENT] ) +_Token = Tuple[int, str, Tuple[int, int], Tuple[int, int], str] +_LogicalMapping = List[Tuple[int, Tuple[int, int]]] +_Logical = Tuple[List[str], List[str], _LogicalMapping] + class FileProcessor(object): """Processes a file and holdes state. @@ -47,7 +53,11 @@ - :attr:`verbose` """ + #: always ``False``, included for compatibility + noqa = False + def __init__(self, filename, options, lines=None): + # type: (str, argparse.Namespace, Optional[List[str]]) -> None """Initialice our file processor. :param str filename: @@ -55,9 +65,7 @@ """ self.options = options self.filename = filename - self.lines = lines - if lines is None: - self.lines = self.read_lines() + self.lines = lines if lines is not None else self.read_lines() self.strip_utf_bom() # Defaults for public attributes @@ -68,11 +76,11 @@ #: Checker states for each plugin? self._checker_states = {} # type: Dict[str, Dict[Any, Any]] #: Current checker state - self.checker_state = None # type: Dict[Any, Any] + self.checker_state = {} # type: Dict[Any, Any] #: User provided option for hang closing self.hang_closing = options.hang_closing #: Character used for indentation - self.indent_char = None + self.indent_char = None # type: Optional[str] #: Current level of indentation self.indent_level = 0 #: Line number in the file @@ -85,28 +93,26 @@ self.max_doc_length = options.max_doc_length #: Whether the current physical line is multiline self.multiline = False - #: Whether or not we're observing NoQA - self.noqa = False #: Previous level of indentation self.previous_indent_level = 0 #: Previous logical line self.previous_logical = "" #: Previous unindented (i.e. top-level) logical line self.previous_unindented_logical_line = "" - # fmt: off #: Current set of tokens - self.tokens = [] # type: List[Tuple[int, str, Tuple[int, int], Tuple[int, int], str]] # noqa: E501 - # fmt: on + self.tokens = [] # type: List[_Token] #: Total number of lines in the file self.total_lines = len(self.lines) #: Verbosity level of Flake8 self.verbose = options.verbose #: Statistics dictionary self.statistics = {"logical lines": 0} - self._file_tokens = None + self._file_tokens = None # type: Optional[List[_Token]] + # map from line number to the line we'll search for `noqa` in + self._noqa_line_mapping = None # type: Optional[Dict[int, str]] @property - def file_tokens(self): + def file_tokens(self): # type: () -> List[_Token] """Return the complete set of tokens for a file. Accessing this attribute *may* raise an InvalidSyntax exception. @@ -119,32 +125,34 @@ self._file_tokens = list( tokenize.generate_tokens(lambda: next(line_iter)) ) - except tokenize.TokenError as exc: + except (tokenize.TokenError, SyntaxError) as exc: raise exceptions.InvalidSyntax(exception=exc) return self._file_tokens @contextlib.contextmanager def inside_multiline(self, line_number): + # type: (int) -> Generator[None, None, None] """Context-manager to toggle the multiline attribute.""" self.line_number = line_number self.multiline = True yield self.multiline = False - def reset_blank_before(self): + def reset_blank_before(self): # type: () -> None """Reset the blank_before attribute to zero.""" self.blank_before = 0 - def delete_first_token(self): + def delete_first_token(self): # type: () -> None """Delete the first token in the list of tokens.""" del self.tokens[0] - def visited_new_blank_line(self): + def visited_new_blank_line(self): # type: () -> None """Note that we visited a new blank line.""" self.blank_lines += 1 def update_state(self, mapping): + # type: (_LogicalMapping) -> None """Update the indent level based on the logical line mapping.""" (start_row, start_col) = mapping[0][1] start_line = self.lines[start_row - 1] @@ -153,13 +161,14 @@ self.blank_before = self.blank_lines def update_checker_state_for(self, plugin): + # type: (Dict[str, Any]) -> None """Update the checker_state attribute for the plugin.""" if "checker_state" in plugin["parameters"]: self.checker_state = self._checker_states.setdefault( plugin["name"], {} ) - def next_logical_line(self): + def next_logical_line(self): # type: () -> None """Record the previous logical line. This also resets the tokens list and the blank_lines count. @@ -171,14 +180,14 @@ self.previous_unindented_logical_line = self.logical_line self.blank_lines = 0 self.tokens = [] - self.noqa = False - def build_logical_line_tokens(self): + def build_logical_line_tokens(self): # type: () -> _Logical """Build the mapping, comments, and logical line lists.""" logical = [] comments = [] + mapping = [] # type: _LogicalMapping length = 0 - previous_row = previous_column = mapping = None + previous_row = previous_column = None for token_type, text, start, end, line in self.tokens: if token_type in SKIP_TOKENS: continue @@ -207,21 +216,21 @@ (previous_row, previous_column) = end return comments, logical, mapping - def build_ast(self): + def build_ast(self): # type: () -> ast.AST """Build an abstract syntax tree from the list of lines.""" - return compile("".join(self.lines), "", "exec", PyCF_ONLY_AST) + return ast.parse("".join(self.lines)) def build_logical_line(self): + # type: () -> Tuple[str, str, _LogicalMapping] """Build a logical line from the current tokens list.""" comments, logical, mapping_list = self.build_logical_line_tokens() joined_comments = "".join(comments) self.logical_line = "".join(logical) - if defaults.NOQA_INLINE_REGEXP.search(joined_comments): - self.noqa = True self.statistics["logical lines"] += 1 return joined_comments, self.logical_line, mapping_list def split_line(self, token): + # type: (_Token) -> Generator[str, None, None] """Split a physical line's line based on new-lines. This also auto-increments the line number for the caller. @@ -231,6 +240,7 @@ self.line_number += 1 def keyword_arguments_for(self, parameters, arguments=None): + # type: (Dict[str, bool], Optional[Dict[str, Any]]) -> Dict[str, Any] """Generate the keyword arguments for a list of parameters.""" if arguments is None: arguments = {} @@ -251,12 +261,7 @@ ) return arguments - def check_physical_error(self, error_code, line): - """Update attributes based on error code and line.""" - if error_code == "E101": - self.indent_char = line[0] - - def generate_tokens(self): + def generate_tokens(self): # type: () -> Generator[_Token, None, None] """Tokenize the file and yield the tokens. :raises flake8.exceptions.InvalidSyntax: @@ -272,17 +277,53 @@ except (tokenize.TokenError, SyntaxError) as exc: raise exceptions.InvalidSyntax(exception=exc) - def line_for(self, line_number): - """Retrieve the physical line at the specified line number.""" - adjusted_line_number = line_number - 1 + def _noqa_line_range(self, min_line, max_line): + # type: (int, int) -> Dict[int, str] + line_range = range(min_line, max_line + 1) + joined = "".join(self.lines[min_line - 1 : max_line]) + return dict.fromkeys(line_range, joined) + + def noqa_line_for(self, line_number): # type: (int) -> Optional[str] + """Retrieve the line which will be used to determine noqa.""" + if self._noqa_line_mapping is None: + try: + file_tokens = self.file_tokens + except exceptions.InvalidSyntax: + # if we failed to parse the file tokens, we'll always fail in + # the future, so set this so the code does not try again + self._noqa_line_mapping = {} + else: + ret = {} + + min_line = len(self.lines) + 2 + max_line = -1 + for tp, _, (s_line, _), (e_line, _), _ in file_tokens: + if tp == tokenize.ENDMARKER: + break + + min_line = min(min_line, s_line) + max_line = max(max_line, e_line) + + if tp in (tokenize.NL, tokenize.NEWLINE): + ret.update(self._noqa_line_range(min_line, max_line)) + + min_line = len(self.lines) + 2 + max_line = -1 + + # in newer versions of python, a `NEWLINE` token is inserted + # at the end of the file even if it doesn't have one. + # on old pythons, they will not have hit a `NEWLINE` + if max_line != -1: + ret.update(self._noqa_line_range(min_line, max_line)) + + self._noqa_line_mapping = ret + # NOTE(sigmavirus24): Some plugins choose to report errors for empty # files on Line 1. In those cases, we shouldn't bother trying to # retrieve a physical line (since none exist). - if 0 <= adjusted_line_number < len(self.lines): - return self.lines[adjusted_line_number] - return None + return self._noqa_line_mapping.get(line_number) - def next_line(self): + def next_line(self): # type: () -> str """Get the next line from the list.""" if self.line_number >= self.total_lines: return "" @@ -330,7 +371,7 @@ def read_lines_from_stdin(self): # type: () -> List[str] """Read the lines from standard in.""" - return utils.stdin_get_value().splitlines(True) + return utils.stdin_get_lines() def should_ignore_file(self): # type: () -> bool @@ -342,7 +383,9 @@ :rtype: bool """ - if any(defaults.NOQA_FILE.match(line) for line in self.lines): + if not self.options.disable_noqa and any( + defaults.NOQA_FILE.match(line) for line in self.lines + ): return True elif any(defaults.NOQA_FILE.search(line) for line in self.lines): LOG.warning( @@ -371,17 +414,17 @@ self.lines[0] = self.lines[0][3:] -def is_eol_token(token): +def is_eol_token(token): # type: (_Token) -> bool """Check if the token is an end-of-line token.""" return token[0] in NEWLINE or token[4][token[3][1] :].lstrip() == "\\\n" -def is_multiline_string(token): +def is_multiline_string(token): # type: (_Token) -> bool """Check if this is a multiline string.""" return token[0] == tokenize.STRING and "\n" in token[1] -def token_is_newline(token): +def token_is_newline(token): # type: (_Token) -> bool """Check if the token type is a newline token type.""" return token[0] in NEWLINE @@ -396,7 +439,7 @@ return current_parentheses_count -def log_token(log, token): +def log_token(log, token): # type: (logging.Logger, _Token) -> None """Log a token to a provided logging object.""" if token[2][0] == token[3][0]: pos = "[%s:%s]" % (token[2][1] or "", token[3][1]) @@ -411,7 +454,7 @@ # NOTE(sigmavirus24): This was taken wholesale from # https://github.com/PyCQA/pycodestyle -def expand_indent(line): +def expand_indent(line): # type: (str) -> int r"""Return the amount of indentation. Tabs are expanded to the next multiple of 8. @@ -441,14 +484,14 @@ # NOTE(sigmavirus24): This was taken wholesale from # https://github.com/PyCQA/pycodestyle. The in-line comments were edited to be # more descriptive. -def mutate_string(text): +def mutate_string(text): # type: (str) -> str """Replace contents with 'xxx' to prevent syntax matching. - >>> mute_string('"abc"') + >>> mutate_string('"abc"') '"xxx"' - >>> mute_string("'''abc'''") + >>> mutate_string("'''abc'''") "'''xxx'''" - >>> mute_string("r'abc'") + >>> mutate_string("r'abc'") "r'xxx'" """ # NOTE(sigmavirus24): If there are string modifiers (e.g., b, u, r) diff -Nru python-flake8-3.7.9/src/flake8/style_guide.py python-flake8-3.8.3/src/flake8/style_guide.py --- python-flake8-3.7.9/src/flake8/style_guide.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/src/flake8/style_guide.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,4 +1,5 @@ """Implementation of the StyleGuide used by Flake8.""" +import argparse import collections import contextlib import copy @@ -6,25 +7,20 @@ import itertools import linecache import logging -import sys -from typing import Dict, List, Optional, Set, Union +from typing import Dict, Generator, List, Match, Optional, Sequence, Set +from typing import Tuple, Union from flake8 import defaults from flake8 import statistics from flake8 import utils +from flake8._compat import lru_cache +from flake8.formatting import base as base_formatter __all__ = ("StyleGuide",) LOG = logging.getLogger(__name__) -if sys.version_info < (3, 2): - from functools32 import lru_cache -else: - from functools import lru_cache - - -# TODO(sigmavirus24): Determine if we need to use enum/enum34 class Selected(enum.Enum): """Enum representing an explicitly or implicitly selected code.""" @@ -47,7 +43,7 @@ @lru_cache(maxsize=512) -def find_noqa(physical_line): +def find_noqa(physical_line): # type: (str) -> Optional[Match[str]] return defaults.NOQA_INLINE_REGEXP.search(physical_line) @@ -110,6 +106,7 @@ return False def is_in(self, diff): + # type: (Dict[str, Set[int]]) -> bool """Determine if the violation is included in a diff's line ranges. This function relies on the parsed data added via @@ -152,7 +149,7 @@ ignored. """ - def __init__(self, options): + def __init__(self, options): # type: (argparse.Namespace) -> None """Initialize the engine.""" self.cache = {} # type: Dict[str, Decision] self.selected = tuple(options.select) @@ -172,11 +169,11 @@ self.using_default_ignore = set(self.ignored) == set(defaults.IGNORE) self.using_default_select = set(self.selected) == set(defaults.SELECT) - def _in_all_selected(self, code): - return self.all_selected and code.startswith(self.all_selected) + def _in_all_selected(self, code): # type: (str) -> bool + return bool(self.all_selected) and code.startswith(self.all_selected) - def _in_extended_selected(self, code): - return self.extended_selected and code.startswith( + def _in_extended_selected(self, code): # type: (str) -> bool + return bool(self.extended_selected) and code.startswith( self.extended_selected ) @@ -223,7 +220,7 @@ return Selected.Implicitly def more_specific_decision_for(self, code): - # type: (Violation) -> Decision + # type: (str) -> Decision select = find_first_match(code, self.all_selected) extra_select = find_first_match(code, self.extended_selected) ignore = find_first_match(code, self.ignored) @@ -272,6 +269,7 @@ return Decision.Selected def make_decision(self, code): + # type: (str) -> Decision """Decide if code should be ignored or selected.""" LOG.debug('Deciding if "%s" should be reported', code) selected = self.was_selected(code) @@ -323,7 +321,12 @@ class StyleGuideManager(object): """Manage multiple style guides for a single run.""" - def __init__(self, options, formatter, decider=None): + def __init__( + self, + options, # type: argparse.Namespace + formatter, # type: base_formatter.BaseFormatter + decider=None, # type: Optional[DecisionEngine] + ): # type: (...) -> None """Initialize our StyleGuide. .. todo:: Add parameter documentation. @@ -344,12 +347,13 @@ ) def populate_style_guides_with(self, options): + # type: (argparse.Namespace) -> Generator[StyleGuide, None, None] """Generate style guides from the per-file-ignores option. :param options: The original options parsed from the CLI and config file. :type options: - :class:`~optparse.Values` + :class:`~argparse.Namespace` :returns: A copy of the default style guide with overridden values. :rtype: @@ -364,7 +368,7 @@ ) @lru_cache(maxsize=None) - def style_guide_for(self, filename): + def style_guide_for(self, filename): # type: (str) -> StyleGuide """Find the StyleGuide for the filename in particular.""" guides = sorted( (g for g in self.style_guides if g.applies_to(filename)), @@ -376,6 +380,7 @@ @contextlib.contextmanager def processing_file(self, filename): + # type: (str) -> Generator[StyleGuide, None, None] """Record the fact that we're processing the file's results.""" guide = self.style_guide_for(filename) with guide.processing_file(filename): @@ -436,7 +441,12 @@ """Manage a Flake8 user's style guide.""" def __init__( - self, options, formatter, stats, filename=None, decider=None + self, + options, # type: argparse.Namespace + formatter, # type: base_formatter.BaseFormatter + stats, # type: statistics.Statistics + filename=None, # type: Optional[str] + decider=None, # type: Optional[DecisionEngine] ): """Initialize our StyleGuide. @@ -451,11 +461,12 @@ self.filename = utils.normalize_path(self.filename) self._parsed_diff = {} # type: Dict[str, Set[int]] - def __repr__(self): + def __repr__(self): # type: () -> str """Make it easier to debug which StyleGuide we're using.""" return "".format(self.filename) - def copy(self, filename=None, extend_ignore_with=None, **kwargs): + def copy(self, filename=None, extend_ignore_with=None): + # type: (Optional[str], Optional[Sequence[str]]) -> StyleGuide """Create a copy of this style guide with different values.""" filename = filename or self.filename options = copy.deepcopy(self.options) @@ -466,12 +477,13 @@ @contextlib.contextmanager def processing_file(self, filename): + # type: (str) -> Generator[StyleGuide, None, None] """Record the fact that we're processing the file's results.""" self.formatter.beginning(filename) yield self self.formatter.finished(filename) - def applies_to(self, filename): + def applies_to(self, filename): # type: (str) -> bool """Check if this StyleGuide applies to the file. :param str filename: @@ -580,13 +592,14 @@ self._parsed_diff = diffinfo -def find_more_specific(selected, ignored): +def find_more_specific(selected, ignored): # type: (str, str) -> Decision if selected.startswith(ignored) and selected != ignored: return Decision.Selected return Decision.Ignored def find_first_match(error_code, code_list): + # type: (str, Tuple[str, ...]) -> Optional[str] startswith = error_code.startswith for code in code_list: if startswith(code): diff -Nru python-flake8-3.7.9/src/flake8/utils.py python-flake8-3.8.3/src/flake8/utils.py --- python-flake8-3.7.9/src/flake8/utils.py 2019-10-28 10:30:33.000000000 -0700 +++ python-flake8-3.8.3/src/flake8/utils.py 2020-06-08 12:15:48.000000000 -0700 @@ -13,6 +13,7 @@ from typing import Sequence, Set, Tuple, Union from flake8 import exceptions +from flake8._compat import lru_cache if False: # `typing.TYPE_CHECKING` was introduced in 3.5.2 from flake8.plugins.manager import Plugin @@ -24,11 +25,11 @@ def parse_comma_separated_list(value, regexp=COMMA_SEPARATED_LIST_RE): - # type: (Union[Sequence[str], str], Pattern[str]) -> List[str] + # type: (str, Pattern[str]) -> List[str] """Parse a comma-separated list. :param value: - String or list of strings to be parsed and normalized. + String to be parsed and normalized. :param regexp: Compiled regular expression used to split the value when it is a string. @@ -39,13 +40,10 @@ :rtype: list """ - if not value: - return [] + assert isinstance(value, string_types), value - if isinstance(value, string_types): - value = regexp.split(value) - - item_gen = (item.strip() for item in value) + separated = regexp.split(value) + item_gen = (item.strip() for item in separated) return [item for item in item_gen if item] @@ -158,17 +156,16 @@ def normalize_paths(paths, parent=os.curdir): - # type: (Union[Sequence[str], str], str) -> List[str] - """Parse a comma-separated list of paths. + # type: (Sequence[str], str) -> List[str] + """Normalize a list of paths relative to a parent directory. :returns: The normalized paths. :rtype: [str] """ - return [ - normalize_path(p, parent) for p in parse_comma_separated_list(paths) - ] + assert isinstance(paths, list), paths + return [normalize_path(p, parent) for p in paths] def normalize_path(path, parent=os.curdir): @@ -193,28 +190,31 @@ return path.rstrip(separator + alternate_separator) -def _stdin_get_value_py3(): # type: () -> io.StringIO +def _stdin_get_value_py3(): # type: () -> str stdin_value = sys.stdin.buffer.read() fd = io.BytesIO(stdin_value) try: - (coding, lines) = tokenize.detect_encoding(fd.readline) - return io.StringIO(stdin_value.decode(coding)) + coding, _ = tokenize.detect_encoding(fd.readline) + return stdin_value.decode(coding) except (LookupError, SyntaxError, UnicodeError): - return io.StringIO(stdin_value.decode("utf-8")) + return stdin_value.decode("utf-8") -def stdin_get_value(): - # type: () -> str +@lru_cache(maxsize=1) +def stdin_get_value(): # type: () -> str """Get and cache it so plugins can use it.""" - cached_value = getattr(stdin_get_value, "cached_stdin", None) - if cached_value is None: - if sys.version_info < (3, 0): - stdin_value = io.BytesIO(sys.stdin.read()) - else: - stdin_value = _stdin_get_value_py3() - stdin_get_value.cached_stdin = stdin_value # type: ignore - cached_value = stdin_get_value.cached_stdin # type: ignore - return cached_value.getvalue() + if sys.version_info < (3,): + return sys.stdin.read() + else: + return _stdin_get_value_py3() + + +def stdin_get_lines(): # type: () -> List[str] + """Return lines of stdin split according to file splitting.""" + if sys.version_info < (3,): + return list(io.BytesIO(stdin_get_value())) + else: + return list(io.StringIO(stdin_get_value())) def parse_unified_diff(diff=None): @@ -280,7 +280,7 @@ 1 if not group else int(group) for group in hunk_match.groups() ] - assert current_path is not None # nosec (for mypy) + assert current_path is not None parsed_paths[current_path].update( range(row, row + number_of_rows) ) @@ -355,15 +355,14 @@ for filename in files: joined = os.path.join(root, filename) - if predicate(joined) or predicate(filename): - continue - yield joined + if not predicate(joined): + yield joined else: yield arg def fnmatch(filename, patterns): - # type: (str, List[str]) -> bool + # type: (str, Sequence[str]) -> bool """Wrap :func:`fnmatch.fnmatch` to add some functionality. :param str filename: @@ -430,7 +429,7 @@ def matches_filename(path, patterns, log_message, logger): - # type: (str, List[str], str, logging.Logger) -> bool + # type: (str, Sequence[str], str, logging.Logger) -> bool """Use fnmatch to discern if a path exists in patterns. :param str path: @@ -449,7 +448,7 @@ if not patterns: return False basename = os.path.basename(path) - if fnmatch(basename, patterns): + if basename not in {".", ".."} and fnmatch(basename, patterns): logger.debug(log_message, {"path": basename, "whether": ""}) return True diff -Nru python-flake8-3.7.9/src/flake8.egg-info/PKG-INFO python-flake8-3.8.3/src/flake8.egg-info/PKG-INFO --- python-flake8-3.7.9/src/flake8.egg-info/PKG-INFO 2019-10-28 10:36:48.000000000 -0700 +++ python-flake8-3.8.3/src/flake8.egg-info/PKG-INFO 2020-06-08 12:30:20.000000000 -0700 @@ -1,7 +1,7 @@ Metadata-Version: 1.2 Name: flake8 -Version: 3.7.9 -Summary: the modular source code checker: pep8, pyflakes and co +Version: 3.8.3 +Summary: the modular source code checker: pep8 pyflakes and co Home-page: https://gitlab.com/pycqa/flake8 Author: Tarek Ziade Author-email: tarek@ziade.org @@ -100,6 +100,7 @@ Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Software Development :: Quality Assurance -Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.* +Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7 diff -Nru python-flake8-3.7.9/src/flake8.egg-info/requires.txt python-flake8-3.8.3/src/flake8.egg-info/requires.txt --- python-flake8-3.7.9/src/flake8.egg-info/requires.txt 2019-10-28 10:36:48.000000000 -0700 +++ python-flake8-3.8.3/src/flake8.egg-info/requires.txt 2020-06-08 12:30:20.000000000 -0700 @@ -1,14 +1,16 @@ -entrypoints<0.4.0,>=0.3.0 -pyflakes<2.2.0,>=2.1.0 -pycodestyle<2.6.0,>=2.5.0 +pyflakes<2.3.0,>=2.2.0 +pycodestyle<2.7.0,>=2.6.0a1 mccabe<0.7.0,>=0.6.0 -[:python_version<'3.2'] +[:python_version < "3.2"] configparser functools32 -[:python_version<'3.4'] +[:python_version < "3.4"] enum34 -[:python_version<'3.5'] +[:python_version < "3.5"] typing + +[:python_version < "3.8"] +importlib-metadata diff -Nru python-flake8-3.7.9/src/flake8.egg-info/SOURCES.txt python-flake8-3.8.3/src/flake8.egg-info/SOURCES.txt --- python-flake8-3.7.9/src/flake8.egg-info/SOURCES.txt 2019-10-28 10:36:48.000000000 -0700 +++ python-flake8-3.8.3/src/flake8.egg-info/SOURCES.txt 2020-06-08 12:30:20.000000000 -0700 @@ -88,6 +88,10 @@ docs/source/release-notes/3.7.7.rst docs/source/release-notes/3.7.8.rst docs/source/release-notes/3.7.9.rst +docs/source/release-notes/3.8.0.rst +docs/source/release-notes/3.8.1.rst +docs/source/release-notes/3.8.2.rst +docs/source/release-notes/3.8.3.rst docs/source/release-notes/index.rst docs/source/user/configuration.rst docs/source/user/error-codes.rst @@ -100,6 +104,7 @@ docs/source/user/violations.rst src/flake8/__init__.py src/flake8/__main__.py +src/flake8/_compat.py src/flake8/checker.py src/flake8/defaults.py src/flake8/exceptions.py diff -Nru python-flake8-3.7.9/tests/fixtures/config_files/cli-specified.ini python-flake8-3.8.3/tests/fixtures/config_files/cli-specified.ini --- python-flake8-3.7.9/tests/fixtures/config_files/cli-specified.ini 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/tests/fixtures/config_files/cli-specified.ini 2020-06-08 12:15:48.000000000 -0700 @@ -7,5 +7,4 @@ foo/, bar/, bogus/ -verbose = 2 quiet = 1 diff -Nru python-flake8-3.7.9/tests/integration/subdir/aplugin.py python-flake8-3.8.3/tests/integration/subdir/aplugin.py --- python-flake8-3.7.9/tests/integration/subdir/aplugin.py 2019-10-28 08:59:37.000000000 -0700 +++ python-flake8-3.8.3/tests/integration/subdir/aplugin.py 2020-06-08 12:15:48.000000000 -0700 @@ -9,8 +9,6 @@ def __init__(self, tree): """Construct an instance of test plugin.""" - pass def run(self): """Do nothing.""" - pass diff -Nru python-flake8-3.7.9/tests/integration/test_aggregator.py python-flake8-3.8.3/tests/integration/test_aggregator.py --- python-flake8-3.7.9/tests/integration/test_aggregator.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/tests/integration/test_aggregator.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,4 +1,5 @@ """Test aggregation of config files and command-line options.""" +import argparse import os import pytest @@ -14,9 +15,12 @@ @pytest.fixture def optmanager(): """Create a new OptionManager.""" + prelim_parser = argparse.ArgumentParser(add_help=False) + options.register_preliminary_options(prelim_parser) option_manager = manager.OptionManager( prog='flake8', version='3.0.0', + parents=[prelim_parser], ) options.register_default_options(option_manager) return option_manager @@ -24,13 +28,14 @@ def test_aggregate_options_with_config(optmanager): """Verify we aggregate options and config values appropriately.""" - arguments = ['flake8', '--config', CLI_SPECIFIED_CONFIG, '--select', + arguments = ['flake8', '--select', 'E11,E34,E402,W,F', '--exclude', 'tests/*'] - config_finder = config.ConfigFileFinder('flake8', arguments, []) + config_finder = config.ConfigFileFinder( + 'flake8', + config_file=CLI_SPECIFIED_CONFIG) options, args = aggregator.aggregate_options( optmanager, config_finder, arguments) - assert options.config == CLI_SPECIFIED_CONFIG assert options.select == ['E11', 'E34', 'E402', 'W', 'F'] assert options.ignore == ['E123', 'W234', 'E111'] assert options.exclude == [os.path.abspath('tests/*')] @@ -38,14 +43,14 @@ def test_aggregate_options_when_isolated(optmanager): """Verify we aggregate options and config values appropriately.""" - arguments = ['flake8', '--isolated', '--select', 'E11,E34,E402,W,F', + arguments = ['flake8', '--select', 'E11,E34,E402,W,F', '--exclude', 'tests/*'] - config_finder = config.ConfigFileFinder('flake8', arguments, []) + config_finder = config.ConfigFileFinder( + 'flake8', ignore_config_files=True) optmanager.extend_default_ignore(['E8']) options, args = aggregator.aggregate_options( optmanager, config_finder, arguments) - assert options.isolated is True assert options.select == ['E11', 'E34', 'E402', 'W', 'F'] assert sorted(options.ignore) == [ 'E121', 'E123', 'E126', 'E226', 'E24', 'E704', 'E8', 'W503', 'W504', diff -Nru python-flake8-3.7.9/tests/integration/test_checker.py python-flake8-3.8.3/tests/integration/test_checker.py --- python-flake8-3.7.9/tests/integration/test_checker.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/tests/integration/test_checker.py 2020-06-08 12:15:48.000000000 -0700 @@ -3,7 +3,9 @@ import pytest from flake8 import checker +from flake8._compat import importlib_metadata from flake8.plugins import manager +from flake8.processor import FileProcessor PHYSICAL_LINE = "# Physical line content" @@ -14,7 +16,7 @@ 0, 1, 'Expected Message', - PHYSICAL_LINE, + None, ) @@ -100,7 +102,11 @@ entry_point.load.return_value = plugin_target # Load the checker plugins using the entry point mock - with mock.patch('entrypoints.get_group_all', return_value=[entry_point]): + with mock.patch.object( + importlib_metadata, + 'entry_points', + return_value={'flake8.extension': [entry_point]}, + ): checks = manager.Checkers() # Prevent it from reading lines from stdin or somewhere else @@ -148,11 +154,31 @@ """Test the FileChecker class handling results from line checks.""" file_checker = mock_file_checker_with_plugin(plugin_target) - # Results will be store in an internal array + # Results will be stored in an internal array file_checker.run_physical_checks(PHYSICAL_LINE) - assert file_checker.results == [ - EXPECTED_RESULT_PHYSICAL_LINE - ] * len_results + expected = [EXPECTED_RESULT_PHYSICAL_LINE] * len_results + assert file_checker.results == expected + + +def test_logical_line_offset_out_of_bounds(): + """Ensure that logical line offsets that are out of bounds do not crash.""" + + @plugin_func + def _logical_line_out_of_bounds(logical_line): + yield 10000, 'L100 test' + + file_checker = mock_file_checker_with_plugin(_logical_line_out_of_bounds) + + logical_ret = ( + '', + 'print("xxxxxxxxxxx")', + [(0, (1, 0)), (5, (1, 5)), (6, (1, 6)), (19, (1, 19)), (20, (1, 20))], + ) + with mock.patch.object( + FileProcessor, 'build_logical_line', return_value=logical_ret, + ): + file_checker.run_logical_checks() + assert file_checker.results == [('L100', 0, 0, 'test', None)] PLACEHOLDER_CODE = 'some_line = "of" * code' @@ -206,13 +232,12 @@ file_checker.results = results file_checker.display_name = 'placeholder' - style_guide = mock.Mock(spec=['options']) - style_guide.processing_file = mock.MagicMock() + style_guide = mock.MagicMock(spec=['options', 'processing_file']) # Create a placeholder manager without arguments or plugins # Just add one custom file checker which just provides the results manager = checker.Manager(style_guide, [], []) - manager.checkers = [file_checker] + manager.checkers = manager._all_checkers = [file_checker] # _handle_results is the first place which gets the sorted result # Should something non-private be mocked instead? diff -Nru python-flake8-3.7.9/tests/integration/test_main.py python-flake8-3.8.3/tests/integration/test_main.py --- python-flake8-3.7.9/tests/integration/test_main.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/tests/integration/test_main.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,8 +1,18 @@ """Integration tests for the main entrypoint of flake8.""" +import json +import os + import mock +import pytest from flake8 import utils -from flake8.main import application +from flake8.main import cli + + +def _call_main(argv, retv=0): + with pytest.raises(SystemExit) as excinfo: + cli.main(argv) + assert excinfo.value.code == retv def test_diff_option(tmpdir, capsys): @@ -34,22 +44,60 @@ with mock.patch.object(utils, 'stdin_get_value', return_value=diff): with tmpdir.as_cwd(): tmpdir.join('t.py').write(t_py_contents) - - app = application.Application() - app.run(['--diff']) + _call_main(['--diff'], retv=1) out, err = capsys.readouterr() assert out == "t.py:8:1: F821 undefined name 'y'\n" assert err == '' +def test_form_feed_line_split(tmpdir, capsys): + """Test that form feed is treated the same for stdin.""" + src = 'x=1\n\f\ny=1\n' + expected_out = '''\ +t.py:1:2: E225 missing whitespace around operator +t.py:3:2: E225 missing whitespace around operator +''' + + with tmpdir.as_cwd(): + tmpdir.join('t.py').write(src) + + with mock.patch.object(utils, 'stdin_get_value', return_value=src): + _call_main(['-', '--stdin-display-name=t.py'], retv=1) + out, err = capsys.readouterr() + assert out == expected_out + assert err == '' + + _call_main(['t.py'], retv=1) + out, err = capsys.readouterr() + assert out == expected_out + assert err == '' + + +def test_e101_indent_char_does_not_reset(tmpdir, capsys): + """Ensure that E101 with an existing indent_char does not reset it.""" + t_py_contents = """\ +if True: + print('space indented') + +s = '''\ +\ttab indented +''' # noqa: E101 + +if True: + print('space indented') +""" + + with tmpdir.as_cwd(): + tmpdir.join('t.py').write(t_py_contents) + _call_main(['t.py']) + + def test_statistics_option(tmpdir, capsys): """Ensure that `flake8 --statistics` works.""" with tmpdir.as_cwd(): tmpdir.join('t.py').write('import os\nimport sys\n') - - app = application.Application() - app.run(['--statistics', 't.py']) + _call_main(['--statistics', 't.py'], retv=1) out, err = capsys.readouterr() assert out == '''\ @@ -60,6 +108,23 @@ assert err == '' +def test_extend_exclude(tmpdir, capsys): + """Ensure that `flake8 --extend-exclude` works.""" + for d in ['project', 'vendor', 'legacy', '.git', '.tox', '.hg']: + tmpdir.mkdir(d).join('t.py').write('import os\nimport sys\n') + + with tmpdir.as_cwd(): + _call_main(['--extend-exclude=vendor,legacy/'], retv=1) + + out, err = capsys.readouterr() + expected_out = '''\ +./project/t.py:1:1: F401 'os' imported but unused +./project/t.py:2:1: F401 'sys' imported but unused +''' + assert out == expected_out.replace('/', os.sep) + assert err == '' + + def test_malformed_per_file_ignores_error(tmpdir, capsys): """Test the error message for malformed `per-file-ignores`.""" setup_cfg = '''\ @@ -71,9 +136,7 @@ with tmpdir.as_cwd(): tmpdir.join('setup.cfg').write(setup_cfg) - - app = application.Application() - app.run(['.']) + _call_main(['.'], retv=1) out, err = capsys.readouterr() assert out == '''\ @@ -92,10 +155,119 @@ with tmpdir.as_cwd(): # this is a crash in the tokenizer, but not in the ast tmpdir.join('t.py').write("b'foo' \\\n") - - app = application.Application() - app.run(['t.py']) + _call_main(['t.py'], retv=1) out, err = capsys.readouterr() assert out == 't.py:1:1: E902 TokenError: EOF in multi-line statement\n' assert err == '' + + +def test_tokenization_error_is_a_syntax_error(tmpdir, capsys): + """Test when tokenize raises a SyntaxError.""" + with tmpdir.as_cwd(): + tmpdir.join('t.py').write('if True:\n pass\n pass\n') + _call_main(['t.py'], retv=1) + + out, err = capsys.readouterr() + assert out == 't.py:1:1: E902 IndentationError: unindent does not match any outer indentation level\n' # noqa: E501 + assert err == '' + + +def test_bug_report_successful(capsys): + """Test that --bug-report does not crash.""" + _call_main(['--bug-report']) + out, err = capsys.readouterr() + assert json.loads(out) + assert err == '' + + +def test_specific_noqa_does_not_clobber_pycodestyle_noqa(tmpdir, capsys): + """See https://gitlab.com/pycqa/flake8/issues/552.""" + with tmpdir.as_cwd(): + tmpdir.join('t.py').write("test = ('ABC' == None) # noqa: E501\n") + _call_main(['t.py'], retv=1) + + out, err = capsys.readouterr() + assert out == '''\ +t.py:1:15: E711 comparison to None should be 'if cond is None:' +''' + + +def test_specific_noqa_on_line_with_continuation(tmpdir, capsys): + """See https://gitlab.com/pycqa/flake8/issues/375.""" + t_py_src = '''\ +from os \\ + import path # noqa: F401 + +x = """ + trailing whitespace: \n +""" # noqa: W291 +''' + + with tmpdir.as_cwd(): + tmpdir.join('t.py').write(t_py_src) + _call_main(['t.py'], retv=0) + + out, err = capsys.readouterr() + assert out == err == '' + + +def test_obtaining_args_from_sys_argv_when_not_explicity_provided(capsys): + """Test that arguments are obtained from 'sys.argv'.""" + with mock.patch('sys.argv', ['flake8', '--help']): + _call_main(None) + + out, err = capsys.readouterr() + assert out.startswith('usage: flake8 [options] file file ...\n') + assert err == '' + + +def test_cli_config_option_respected(tmp_path): + """Test --config is used.""" + config = tmp_path / "flake8.ini" + config.write_text(u"""\ +[flake8] +ignore = F401 +""") + + py_file = tmp_path / "t.py" + py_file.write_text(u"import os\n") + + _call_main(["--config", str(config), str(py_file)]) + + +def test_cli_isolated_overrides_config_option(tmp_path): + """Test --isolated overrides --config.""" + config = tmp_path / "flake8.ini" + config.write_text(u"""\ +[flake8] +ignore = F401 +""") + + py_file = tmp_path / "t.py" + py_file.write_text(u"import os\n") + + _call_main(["--isolated", "--config", str(config), str(py_file)], retv=1) + + +def test_file_not_found(tmpdir, capsys): + """Ensure that a not-found file / directory is an error.""" + with tmpdir.as_cwd(): + _call_main(["i-do-not-exist"], retv=1) + out, err = capsys.readouterr() + assert out.startswith("i-do-not-exist:0:1: E902") + assert err == "" + + +def test_output_file(tmpdir, capsys): + """Ensure that --output-file is honored.""" + tmpdir.join('t.py').write('import os\n') + + with tmpdir.as_cwd(): + _call_main(['t.py', '--output-file=f'], retv=1) + + out, err = capsys.readouterr() + assert out == err == "" + + expected = "t.py:1:1: F401 'os' imported but unused\n" + assert tmpdir.join('f').read() == expected diff -Nru python-flake8-3.7.9/tests/integration/test_plugins.py python-flake8-3.8.3/tests/integration/test_plugins.py --- python-flake8-3.7.9/tests/integration/test_plugins.py 2019-10-28 08:59:37.000000000 -0700 +++ python-flake8-3.8.3/tests/integration/test_plugins.py 2020-06-08 12:15:48.000000000 -0700 @@ -14,11 +14,9 @@ def __init__(self, tree): """Construct an instance of test plugin.""" - pass def run(self): """Do nothing.""" - pass @classmethod def add_options(cls, parser): @@ -34,11 +32,9 @@ def __init__(self, tree): """Construct an instance of test plugin.""" - pass def run(self): """Do nothing.""" - pass def test_enable_local_plugin_from_config(): diff -Nru python-flake8-3.7.9/tests/unit/conftest.py python-flake8-3.8.3/tests/unit/conftest.py --- python-flake8-3.7.9/tests/unit/conftest.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/tests/unit/conftest.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,5 +1,5 @@ """Shared fixtures between unit tests.""" -import optparse +import argparse import pytest @@ -11,7 +11,8 @@ kwargs.setdefault('max_doc_length', None) kwargs.setdefault('verbose', False) kwargs.setdefault('stdin_display_name', 'stdin') - return optparse.Values(kwargs) + kwargs.setdefault('disable_noqa', False) + return argparse.Namespace(**kwargs) @pytest.fixture diff -Nru python-flake8-3.7.9/tests/unit/test_application.py python-flake8-3.8.3/tests/unit/test_application.py --- python-flake8-3.7.9/tests/unit/test_application.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/tests/unit/test_application.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,5 +1,5 @@ """Tests for the Application class.""" -import optparse +import argparse import sys import mock @@ -9,12 +9,12 @@ def options(**kwargs): - """Generate optparse.Values for our Application.""" + """Generate argparse.Namespace for our Application.""" kwargs.setdefault('verbose', 0) kwargs.setdefault('output_file', None) kwargs.setdefault('count', False) kwargs.setdefault('exit_zero', False) - return optparse.Values(kwargs) + return argparse.Namespace(**kwargs) @pytest.fixture @@ -24,28 +24,15 @@ @pytest.mark.parametrize( - 'result_count, catastrophic, exit_zero', [ - (0, True, True), - (2, False, True), - (2, True, True), - ] -) -def test_exit_does_not_raise(result_count, catastrophic, exit_zero, - application): - """Verify Application.exit doesn't raise SystemExit.""" - application.result_count = result_count - application.catastrophic_failure = catastrophic - application.options = options(exit_zero=exit_zero) - - assert application.exit() is None - - -@pytest.mark.parametrize( 'result_count, catastrophic, exit_zero, value', [ (0, False, False, False), (0, True, False, True), (2, False, False, True), (2, True, False, True), + + (0, True, True, True), + (2, False, True, False), + (2, True, True, True), ] ) def test_exit_does_raise(result_count, catastrophic, exit_zero, value, @@ -92,18 +79,28 @@ def test_prelim_opts_args(application): """Verify we get sensible prelim opts and args.""" - application.parse_preliminary_options_and_args( - ['flake8', '--foo', '--verbose', 'src', 'setup.py', '--statistics']) + opts, args = application.parse_preliminary_options( + ['--foo', '--verbose', 'src', 'setup.py', '--statistics', '--version']) + + assert opts.verbose + assert args == ['--foo', 'src', 'setup.py', '--statistics', '--version'] + + +def test_prelim_opts_ignore_help(application): + """Verify -h/--help is not handled.""" + # GIVEN + + # WHEN + _, args = application.parse_preliminary_options(['--help', '-h']) - assert application.prelim_opts.statistics - assert application.prelim_opts.verbose - assert application.prelim_args == ['src', 'setup.py'] + # THEN + assert args == ['--help', '-h'] def test_prelim_opts_handles_empty(application): """Verify empty argv lists are handled correctly.""" irrelevant_args = ['myexe', '/path/to/foo'] with mock.patch.object(sys, 'argv', irrelevant_args): - application.parse_preliminary_options_and_args([]) + opts, args = application.parse_preliminary_options([]) - assert application.prelim_args == [] + assert args == [] diff -Nru python-flake8-3.7.9/tests/unit/test_base_formatter.py python-flake8-3.8.3/tests/unit/test_base_formatter.py --- python-flake8-3.7.9/tests/unit/test_base_formatter.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/tests/unit/test_base_formatter.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,5 +1,5 @@ """Tests for the BaseFormatter object.""" -import optparse +import argparse import mock import pytest @@ -9,10 +9,10 @@ def options(**kwargs): - """Create an optparse.Values instance.""" + """Create an argparse.Namespace instance.""" kwargs.setdefault('output_file', None) kwargs.setdefault('tee', False) - return optparse.Values(kwargs) + return argparse.Namespace(**kwargs) @pytest.mark.parametrize('filename', [None, 'out.txt']) @@ -65,19 +65,29 @@ ) == '' -@pytest.mark.parametrize('line, column', [ - ('x=1\n', 2), - (' x=(1\n +2)\n', 5), - # TODO(sigmavirus24): Add more examples +@pytest.mark.parametrize(('line1', 'line2', 'column'), [ + ( + 'x=1\n', + ' ^', + 2, + ), + ( + ' x=(1\n +2)\n', + ' ^', + 5, + ), + ( + '\tx\t=\ty\n', + '\t \t \t^', + 6, + ), ]) -def test_show_source_updates_physical_line_appropriately(line, column): +def test_show_source_updates_physical_line_appropriately(line1, line2, column): """Ensure the error column is appropriately indicated.""" formatter = base.BaseFormatter(options(show_source=True)) - error = style_guide.Violation('A000', 'file.py', 1, column, 'error', line) + error = style_guide.Violation('A000', 'file.py', 1, column, 'error', line1) output = formatter.show_source(error) - assert output - _, pointer = output.rsplit('\n', 1) - assert pointer.count(' ') == (column - 1) + assert output == line1 + line2 @pytest.mark.parametrize('tee', [False, True]) @@ -137,7 +147,7 @@ def test_after_init_is_always_called(): """Verify after_init is called.""" formatter = AfterInitFormatter(options()) - assert getattr(formatter, 'post_initialized') is True + assert formatter.post_initialized is True class FormatFormatter(base.BaseFormatter): diff -Nru python-flake8-3.7.9/tests/unit/test_checker_manager.py python-flake8-3.8.3/tests/unit/test_checker_manager.py --- python-flake8-3.7.9/tests/unit/test_checker_manager.py 2019-10-28 10:30:33.000000000 -0700 +++ python-flake8-3.8.3/tests/unit/test_checker_manager.py 2020-06-08 12:15:48.000000000 -0700 @@ -5,15 +5,15 @@ import pytest from flake8 import checker +from flake8.main.options import JobsArgument -def style_guide_mock(**kwargs): +def style_guide_mock(): """Create a mock StyleGuide object.""" - kwargs.setdefault('diff', False) - kwargs.setdefault('jobs', '4') - style_guide = mock.Mock() - style_guide.options = mock.Mock(**kwargs) - return style_guide + return mock.MagicMock(**{ + 'options.diff': False, + 'options.jobs': JobsArgument("4"), + }) def _parallel_checker_manager(): @@ -74,5 +74,7 @@ with mock.patch('flake8.processor.FileProcessor'): manager.make_checkers() - for file_checker in manager.checkers: + assert manager._all_checkers + for file_checker in manager._all_checkers: assert file_checker.filename in files + assert not manager.checkers # the files don't exist diff -Nru python-flake8-3.7.9/tests/unit/test_config_file_finder.py python-flake8-3.8.3/tests/unit/test_config_file_finder.py --- python-flake8-3.7.9/tests/unit/test_config_file_finder.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/tests/unit/test_config_file_finder.py 2020-06-08 12:15:48.000000000 -0700 @@ -2,7 +2,6 @@ """Tests for the ConfigFileFinder.""" import configparser import os -import sys import mock import pytest @@ -13,130 +12,72 @@ BROKEN_CONFIG_PATH = 'tests/fixtures/config_files/broken.ini' -def test_uses_default_args(): - """Show that we default the args value.""" - finder = config.ConfigFileFinder('flake8', None, []) - assert finder.parent == os.path.abspath('.') - - -@pytest.mark.parametrize('platform,is_windows', [ - ('win32', True), - ('linux', False), - ('darwin', False), -]) -def test_windows_detection(platform, is_windows): - """Verify we detect Windows to the best of our knowledge.""" - with mock.patch.object(sys, 'platform', platform): - finder = config.ConfigFileFinder('flake8', None, []) - assert finder.is_windows is is_windows - - def test_cli_config(): """Verify opening and reading the file specified via the cli.""" cli_filepath = CLI_SPECIFIED_FILEPATH - finder = config.ConfigFileFinder('flake8', None, []) + finder = config.ConfigFileFinder('flake8') parsed_config = finder.cli_config(cli_filepath) assert parsed_config.has_section('flake8') -def test_cli_config_double_read(): - """Second request for CLI config is cached.""" - finder = config.ConfigFileFinder('flake8', None, []) - - parsed_config = finder.cli_config(CLI_SPECIFIED_FILEPATH) - boom = Exception("second request for CLI config not cached") - with mock.patch.object(finder, '_read_config', side_effect=boom): - parsed_config_2 = finder.cli_config(CLI_SPECIFIED_FILEPATH) - - assert parsed_config is parsed_config_2 - - -@pytest.mark.parametrize('args,expected', [ - # No arguments, common prefix of abspath('.') - ([], - [os.path.abspath('setup.cfg'), - os.path.abspath('tox.ini')]), - # Common prefix of "flake8/" - (['flake8/options', 'flake8/'], +@pytest.mark.parametrize('cwd,expected', [ + # Root directory of project + (os.path.abspath('.'), [os.path.abspath('setup.cfg'), os.path.abspath('tox.ini')]), - # Common prefix of "flake8/options" - (['flake8/options', 'flake8/options/sub'], + # Subdirectory of project directory + (os.path.abspath('src'), [os.path.abspath('setup.cfg'), os.path.abspath('tox.ini')]), + # Outside of project directory + (os.path.abspath('/'), + []), ]) -def test_generate_possible_local_files(args, expected): +def test_generate_possible_local_files(cwd, expected): """Verify generation of all possible config paths.""" - finder = config.ConfigFileFinder('flake8', args, []) + finder = config.ConfigFileFinder('flake8') - assert (list(finder.generate_possible_local_files()) - == expected) + with mock.patch.object(os, 'getcwd', return_value=cwd): + config_files = list(finder.generate_possible_local_files()) + assert config_files == expected -@pytest.mark.parametrize('args,extra_config_files,expected', [ - # No arguments, common prefix of abspath('.') - ([], - [], - [os.path.abspath('setup.cfg'), - os.path.abspath('tox.ini')]), - # Common prefix of "flake8/" - (['flake8/options', 'flake8/'], - [], - [os.path.abspath('setup.cfg'), - os.path.abspath('tox.ini')]), - # Common prefix of "flake8/options" - (['flake8/options', 'flake8/options/sub'], - [], - [os.path.abspath('setup.cfg'), - os.path.abspath('tox.ini')]), - # Common prefix of "flake8/" with extra config files specified - (['flake8/'], - [CLI_SPECIFIED_FILEPATH], + +@pytest.mark.parametrize('extra_config_files,expected', [ + # Extra config files specified + ([CLI_SPECIFIED_FILEPATH], [os.path.abspath('setup.cfg'), os.path.abspath('tox.ini'), os.path.abspath(CLI_SPECIFIED_FILEPATH)]), - # Common prefix of "flake8/" with missing extra config files specified - (['flake8/'], - [CLI_SPECIFIED_FILEPATH, - 'tests/fixtures/config_files/missing.ini'], + # Missing extra config files specified + ([CLI_SPECIFIED_FILEPATH, + 'tests/fixtures/config_files/missing.ini'], [os.path.abspath('setup.cfg'), os.path.abspath('tox.ini'), os.path.abspath(CLI_SPECIFIED_FILEPATH)]), ]) -def test_local_config_files(args, extra_config_files, expected): +def test_local_config_files(extra_config_files, expected): """Verify discovery of local config files.""" - finder = config.ConfigFileFinder('flake8', args, extra_config_files) + finder = config.ConfigFileFinder('flake8', extra_config_files) assert list(finder.local_config_files()) == expected def test_local_configs(): """Verify we return a ConfigParser.""" - finder = config.ConfigFileFinder('flake8', None, []) + finder = config.ConfigFileFinder('flake8') assert isinstance(finder.local_configs(), configparser.RawConfigParser) -def test_local_configs_double_read(): - """Second request for local configs is cached.""" - finder = config.ConfigFileFinder('flake8', None, []) - - first_read = finder.local_configs() - boom = Exception("second request for local configs not cached") - with mock.patch.object(finder, '_read_config', side_effect=boom): - second_read = finder.local_configs() - - assert first_read is second_read - - @pytest.mark.parametrize('files', [ [BROKEN_CONFIG_PATH], [CLI_SPECIFIED_FILEPATH, BROKEN_CONFIG_PATH], ]) def test_read_config_catches_broken_config_files(files): """Verify that we do not allow the exception to bubble up.""" - _, parsed = config.ConfigFileFinder._read_config(files) + _, parsed = config.ConfigFileFinder._read_config(*files) assert BROKEN_CONFIG_PATH not in parsed @@ -147,3 +88,35 @@ setup_cfg.write_binary(b'[x]\ny = \x81\x8d\x90\x9d') _, parsed = config.ConfigFileFinder._read_config(setup_cfg.strpath) assert parsed == [] + + +def test_config_file_default_value(): + """Verify the default 'config_file' attribute value.""" + finder = config.ConfigFileFinder('flake8') + assert finder.config_file is None + + +def test_setting_config_file_value(): + """Verify the 'config_file' attribute matches constructed value.""" + config_file_value = 'flake8.ini' + finder = config.ConfigFileFinder('flake8', config_file=config_file_value) + assert finder.config_file == config_file_value + + +def test_ignore_config_files_default_value(): + """Verify the default 'ignore_config_files' attribute value.""" + finder = config.ConfigFileFinder('flake8') + assert finder.ignore_config_files is False + + +@pytest.mark.parametrize('ignore_config_files_arg', [ + False, + True, +]) +def test_setting_ignore_config_files_value(ignore_config_files_arg): + """Verify the 'ignore_config_files' attribute matches constructed value.""" + finder = config.ConfigFileFinder( + 'flake8', + ignore_config_files=ignore_config_files_arg + ) + assert finder.ignore_config_files is ignore_config_files_arg diff -Nru python-flake8-3.7.9/tests/unit/test_debug.py python-flake8-3.8.3/tests/unit/test_debug.py --- python-flake8-3.7.9/tests/unit/test_debug.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/tests/unit/test_debug.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,5 +1,4 @@ """Tests for our debugging module.""" -import entrypoints import mock import pytest @@ -9,9 +8,7 @@ def test_dependencies(): """Verify that we format our dependencies appropriately.""" - expected = [{'dependency': 'entrypoints', - 'version': entrypoints.__version__}] - assert expected == debug.dependencies() + assert [] == debug.dependencies() @pytest.mark.parametrize('plugins, expected', [ @@ -46,8 +43,7 @@ 'is_local': False}, {'plugin': 'pycodestyle', 'version': '2.0.0', 'is_local': False}], - 'dependencies': [{'dependency': 'entrypoints', - 'version': entrypoints.__version__}], + 'dependencies': [], 'platform': { 'python_implementation': 'CPython', 'python_version': '3.5.3', @@ -73,9 +69,10 @@ def test_print_information_no_plugins(dumps, information, print_mock): """Verify we print and exit only when we have plugins.""" option_manager = mock.Mock(registered_plugins=set()) - assert debug.print_information( - None, None, None, None, option_manager=option_manager, - ) is None + action = debug.DebugAction( + "--bug-report", dest="bug_report", option_manager=option_manager, + ) + assert action(None, None, None, None) is None assert dumps.called is False assert information.called is False assert print_mock.called is False @@ -91,10 +88,11 @@ manager.PluginVersion('mccabe', '0.5.9', False), ] option_manager = mock.Mock(registered_plugins=set(plugins)) + action = debug.DebugAction( + "--bug-report", dest="bug_report", option_manager=option_manager, + ) with pytest.raises(SystemExit): - debug.print_information( - None, None, None, None, option_manager=option_manager, - ) + action(None, None, None, None) print_mock.assert_called_once_with('{}') dumps.assert_called_once_with({}, indent=2, sort_keys=True) information.assert_called_once_with(option_manager) diff -Nru python-flake8-3.7.9/tests/unit/test_decision_engine.py python-flake8-3.8.3/tests/unit/test_decision_engine.py --- python-flake8-3.7.9/tests/unit/test_decision_engine.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/tests/unit/test_decision_engine.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,5 +1,5 @@ """Tests for the flake8.style_guide.DecisionEngine class.""" -import optparse +import argparse import pytest @@ -8,14 +8,14 @@ def create_options(**kwargs): - """Create and return an instance of optparse.Values.""" + """Create and return an instance of argparse.Namespace.""" kwargs.setdefault('select', []) kwargs.setdefault('extended_default_select', []) kwargs.setdefault('ignore', []) kwargs.setdefault('extend_ignore', []) kwargs.setdefault('disable_noqa', False) kwargs.setdefault('enable_extensions', []) - return optparse.Values(kwargs) + return argparse.Namespace(**kwargs) @pytest.mark.parametrize('ignore_list,extend_ignore,error_code', [ diff -Nru python-flake8-3.7.9/tests/unit/test_exceptions.py python-flake8-3.8.3/tests/unit/test_exceptions.py --- python-flake8-3.7.9/tests/unit/test_exceptions.py 2019-10-28 08:59:37.000000000 -0700 +++ python-flake8-3.8.3/tests/unit/test_exceptions.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,10 +1,7 @@ """Tests for the flake8.exceptions module.""" import pickle -import entrypoints - from flake8 import exceptions -from flake8.plugins import manager as plugins_manager class _ExceptionTest: @@ -22,10 +19,7 @@ """Tests for the FailedToLoadPlugin exception.""" err = exceptions.FailedToLoadPlugin( - plugin=plugins_manager.Plugin( - 'plugin_name', - entrypoints.EntryPoint('plugin_name', 'os.path', None), - ), + plugin_name='plugin_name', exception=ValueError('boom!'), ) diff -Nru python-flake8-3.7.9/tests/unit/test_filenameonly_formatter.py python-flake8-3.8.3/tests/unit/test_filenameonly_formatter.py --- python-flake8-3.7.9/tests/unit/test_filenameonly_formatter.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/tests/unit/test_filenameonly_formatter.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,15 +1,15 @@ """Tests for the FilenameOnly formatter object.""" -import optparse +import argparse from flake8 import style_guide from flake8.formatting import default def options(**kwargs): - """Create an optparse.Values instance.""" + """Create an argparse.Namespace instance.""" kwargs.setdefault('output_file', None) kwargs.setdefault('tee', False) - return optparse.Values(kwargs) + return argparse.Namespace(**kwargs) def test_caches_filenames_already_printed(): diff -Nru python-flake8-3.7.9/tests/unit/test_file_processor.py python-flake8-3.8.3/tests/unit/test_file_processor.py --- python-flake8-3.7.9/tests/unit/test_file_processor.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/tests/unit/test_file_processor.py 2020-06-08 12:15:48.000000000 -0700 @@ -13,8 +13,7 @@ file_processor = processor.FileProcessor(__file__, default_options) lines = file_processor.lines assert len(lines) > 5 - assert any('"""Tests for the FileProcessor class."""' in line.rstrip() - for line in lines) + assert lines[0].strip() == '"""Tests for the FileProcessor class."""' def _lines_from_file(tmpdir, contents, options): @@ -76,23 +75,28 @@ assert file_processor.should_ignore_file() is expected +def test_should_ignore_file_to_handle_disable_noqa(default_options): + """Verify that we ignore a file if told to.""" + lines = ['# flake8: noqa'] + file_processor = processor.FileProcessor('-', default_options, lines) + assert file_processor.should_ignore_file() is True + default_options.disable_noqa = True + file_processor = processor.FileProcessor('-', default_options, lines) + assert file_processor.should_ignore_file() is False + + @mock.patch('flake8.utils.stdin_get_value') def test_read_lines_from_stdin(stdin_get_value, default_options): """Verify that we use our own utility function to retrieve stdin.""" - stdin_value = mock.Mock() - stdin_value.splitlines.return_value = [] - stdin_get_value.return_value = stdin_value + stdin_get_value.return_value = '' processor.FileProcessor('-', default_options) stdin_get_value.assert_called_once_with() - stdin_value.splitlines.assert_called_once_with(True) @mock.patch('flake8.utils.stdin_get_value') def test_stdin_filename_attribute(stdin_get_value, default_options): """Verify that we update the filename attribute.""" - stdin_value = mock.Mock() - stdin_value.splitlines.return_value = [] - stdin_get_value.return_value = stdin_value + stdin_get_value.return_value = '' file_processor = processor.FileProcessor('-', default_options) assert file_processor.filename == 'stdin' @@ -101,9 +105,7 @@ def test_read_lines_uses_display_name(stdin_get_value, default_options): """Verify that when processing stdin we use a display name if present.""" default_options.stdin_display_name = 'display_name.py' - stdin_value = mock.Mock() - stdin_value.splitlines.return_value = [] - stdin_get_value.return_value = stdin_value + stdin_get_value.return_value = '' file_processor = processor.FileProcessor('-', default_options) assert file_processor.filename == 'display_name.py' @@ -113,24 +115,62 @@ stdin_get_value, default_options, ): """Verify that when processing stdin we use a display name if present.""" - stdin_value = mock.Mock() - stdin_value.splitlines.return_value = [] - stdin_get_value.return_value = stdin_value + stdin_get_value.return_value = '' default_options.stdin_display_name = '' file_processor = processor.FileProcessor('-', default_options) assert file_processor.filename == 'stdin' -def test_line_for(default_options): +def test_noqa_line_for(default_options): """Verify we grab the correct line from the cached lines.""" file_processor = processor.FileProcessor('-', default_options, lines=[ - 'Line 1', - 'Line 2', - 'Line 3', + 'Line 1\n', + 'Line 2\n', + 'Line 3\n', ]) for i in range(1, 4): - assert file_processor.line_for(i) == 'Line {0}'.format(i) + assert file_processor.noqa_line_for(i) == 'Line {0}\n'.format(i) + + +def test_noqa_line_for_continuation(default_options): + """Verify that the correct "line" is retrieved for continuation.""" + src = '''\ +from foo \\ + import bar # 2 + +x = """ +hello +world +""" # 7 +''' + lines = src.splitlines(True) + file_processor = processor.FileProcessor('-', default_options, lines=lines) + + assert file_processor.noqa_line_for(0) is None + + l_1_2 = 'from foo \\\n import bar # 2\n' + assert file_processor.noqa_line_for(1) == l_1_2 + assert file_processor.noqa_line_for(2) == l_1_2 + + assert file_processor.noqa_line_for(3) == '\n' + + l_4_7 = 'x = """\nhello\nworld\n""" # 7\n' + for i in (4, 5, 6, 7): + assert file_processor.noqa_line_for(i) == l_4_7 + + assert file_processor.noqa_line_for(8) is None + + +def test_noqa_line_for_no_eol_at_end_of_file(default_options): + """Verify that we properly handle noqa line at the end of the file.""" + src = 'from foo \\\nimport bar' # no end of file newline + lines = src.splitlines(True) + file_processor = processor.FileProcessor('-', default_options, lines=lines) + + l_1_2 = 'from foo \\\nimport bar' + assert file_processor.noqa_line_for(1) == l_1_2 + assert file_processor.noqa_line_for(2) == l_1_2 def test_next_line(default_options): @@ -146,24 +186,6 @@ assert file_processor.line_number == i -@pytest.mark.parametrize('error_code, line, expected_indent_char', [ - ('E101', '\t\ta = 1', '\t'), - ('E101', ' a = 1', ' '), - ('W101', 'frobulate()', None), - ('F821', 'class FizBuz:', None), -]) -def test_check_physical_error( - error_code, line, expected_indent_char, default_options, -): - """Verify we update the indet char for the appropriate error code.""" - file_processor = processor.FileProcessor('-', default_options, lines=[ - 'Line 1', - ]) - - file_processor.check_physical_error(error_code, line) - assert file_processor.indent_char == expected_indent_char - - @pytest.mark.parametrize('params, args, expected_kwargs', [ ({'blank_before': True, 'blank_lines': True}, None, @@ -196,7 +218,7 @@ ]) with pytest.raises(AttributeError): - file_processor.keyword_arguments_for(['fake']) + file_processor.keyword_arguments_for({'fake': True}) @pytest.mark.parametrize('unsplit_line, expected_lines', [ @@ -211,7 +233,8 @@ 'Line 1', ]) - actual_lines = list(file_processor.split_line((1, unsplit_line))) + token = (1, unsplit_line, (0, 0), (0, 0), '') + actual_lines = list(file_processor.split_line(token)) assert expected_lines == actual_lines assert len(actual_lines) == file_processor.line_number diff -Nru python-flake8-3.7.9/tests/unit/test_get_local_plugins.py python-flake8-3.8.3/tests/unit/test_get_local_plugins.py --- python-flake8-3.7.9/tests/unit/test_get_local_plugins.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/tests/unit/test_get_local_plugins.py 2020-06-08 12:15:48.000000000 -0700 @@ -7,8 +7,9 @@ def test_get_local_plugins_respects_isolated(): """Verify behaviour of get_local_plugins with isolated=True.""" config_finder = mock.MagicMock() + config_finder.ignore_config_files = True - local_plugins = config.get_local_plugins(config_finder, isolated=True) + local_plugins = config.get_local_plugins(config_finder) assert local_plugins.extension == [] assert local_plugins.report == [] @@ -21,17 +22,20 @@ config_obj = mock.Mock() config_finder = mock.MagicMock() config_finder.cli_config.return_value = config_obj + config_finder.ignore_config_files = False config_obj.get.return_value = '' + config_file_value = 'foo.ini' + config_finder.config_file = config_file_value - config.get_local_plugins(config_finder, cli_config='foo.ini') + config.get_local_plugins(config_finder) - config_finder.cli_config.assert_called_once_with('foo.ini') + config_finder.cli_config.assert_called_once_with(config_file_value) def test_get_local_plugins(): """Verify get_local_plugins returns expected plugins.""" config_fixture_path = 'tests/fixtures/config_files/local-plugin.ini' - config_finder = config.ConfigFileFinder('flake8', [], []) + config_finder = config.ConfigFileFinder('flake8') with mock.patch.object(config_finder, 'local_config_files') as localcfs: localcfs.return_value = [config_fixture_path] diff -Nru python-flake8-3.7.9/tests/unit/test_legacy_api.py python-flake8-3.8.3/tests/unit/test_legacy_api.py --- python-flake8-3.7.9/tests/unit/test_legacy_api.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/tests/unit/test_legacy_api.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,26 +1,40 @@ """Tests for Flake8's legacy API.""" +import argparse + import mock import pytest from flake8.api import legacy as api from flake8.formatting import base as formatter +from flake8.options.config import ConfigFileFinder def test_get_style_guide(): """Verify the methods called on our internal Application.""" + prelim_opts = argparse.Namespace( + append_config=[], + config=None, + isolated=False, + output_file=None, + verbose=0, + ) mockedapp = mock.Mock() - mockedapp.prelim_opts.verbose = 0 - mockedapp.prelim_opts.output_file = None - with mock.patch('flake8.main.application.Application') as application: - application.return_value = mockedapp - style_guide = api.get_style_guide() + mockedapp.parse_preliminary_options.return_value = (prelim_opts, []) + mockedapp.program = 'flake8' + with mock.patch('flake8.api.legacy.config.ConfigFileFinder') as mock_config_finder: # noqa: E501 + config_finder = ConfigFileFinder(mockedapp.program) + mock_config_finder.return_value = config_finder + + with mock.patch('flake8.main.application.Application') as application: + application.return_value = mockedapp + style_guide = api.get_style_guide() application.assert_called_once_with() - mockedapp.parse_preliminary_options_and_args.assert_called_once_with([]) - mockedapp.make_config_finder.assert_called_once_with() - mockedapp.find_plugins.assert_called_once_with() + mockedapp.parse_preliminary_options.assert_called_once_with([]) + mockedapp.find_plugins.assert_called_once_with(config_finder) mockedapp.register_plugin_options.assert_called_once_with() - mockedapp.parse_configuration_and_cli.assert_called_once_with([]) + mockedapp.parse_configuration_and_cli.assert_called_once_with( + config_finder, []) mockedapp.make_formatter.assert_called_once_with() mockedapp.make_guide.assert_called_once_with() mockedapp.make_file_checker_manager.assert_called_once_with() @@ -112,7 +126,7 @@ class FakeFormatter(formatter.BaseFormatter): def format(self, *args): - pass + raise NotImplementedError style_guide.init_report(FakeFormatter) app.make_formatter.assert_called_once_with(FakeFormatter) diff -Nru python-flake8-3.7.9/tests/unit/test_merged_config_parser.py python-flake8-3.8.3/tests/unit/test_merged_config_parser.py --- python-flake8-3.7.9/tests/unit/test_merged_config_parser.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/tests/unit/test_merged_config_parser.py 2020-06-08 12:15:48.000000000 -0700 @@ -17,7 +17,7 @@ @pytest.fixture def config_finder(): """Generate a simple ConfigFileFinder.""" - return config.ConfigFileFinder('flake8', [], []) + return config.ConfigFileFinder('flake8') def test_parse_cli_config(optmanager, config_finder): @@ -27,23 +27,21 @@ normalize_paths=True) optmanager.add_option('--ignore', parse_from_config=True, comma_separated_list=True) - optmanager.add_option('--verbose', parse_from_config=True, - action='count') optmanager.add_option('--quiet', parse_from_config=True, action='count') parser = config.MergedConfigParser(optmanager, config_finder) - parsed_config = parser.parse_cli_config( - 'tests/fixtures/config_files/cli-specified.ini' - ) + config_file = 'tests/fixtures/config_files/cli-specified.ini' + parsed_config = parser.parse_cli_config(config_file) + + config_dir = os.path.dirname(config_file) assert parsed_config == { 'ignore': ['E123', 'W234', 'E111'], 'exclude': [ - os.path.abspath('foo/'), - os.path.abspath('bar/'), - os.path.abspath('bogus/'), + os.path.abspath(os.path.join(config_dir, 'foo/')), + os.path.abspath(os.path.join(config_dir, 'bar/')), + os.path.abspath(os.path.join(config_dir, 'bogus/')), ], - 'verbose': 2, 'quiet': 1, } @@ -68,15 +66,13 @@ normalize_paths=True) optmanager.add_option('--ignore', parse_from_config=True, comma_separated_list=True) - optmanager.add_option('--verbose', parse_from_config=True, - action='count') optmanager.add_option('--quiet', parse_from_config=True, action='count') parser = config.MergedConfigParser(optmanager, config_finder) - with mock.patch.object(parser.config_finder, 'user_config_file') as usercf: - usercf.return_value = 'tests/fixtures/config_files/cli-specified.ini' - parsed_config = parser.parse_user_config() + config_finder.user_config_file = ('tests/fixtures/config_files/' + 'cli-specified.ini') + parsed_config = parser.parse_user_config() assert parsed_config == { 'ignore': ['E123', 'W234', 'E111'], @@ -85,7 +81,6 @@ os.path.abspath('bar/'), os.path.abspath('bogus/'), ], - 'verbose': 2, 'quiet': 1, } @@ -97,8 +92,6 @@ normalize_paths=True) optmanager.add_option('--ignore', parse_from_config=True, comma_separated_list=True) - optmanager.add_option('--verbose', parse_from_config=True, - action='count') optmanager.add_option('--quiet', parse_from_config=True, action='count') parser = config.MergedConfigParser(optmanager, config_finder) @@ -116,7 +109,6 @@ os.path.abspath('bar/'), os.path.abspath('bogus/'), ], - 'verbose': 2, 'quiet': 1, } @@ -136,11 +128,9 @@ localcfs.return_value = [ 'tests/fixtures/config_files/local-config.ini' ] - with mock.patch.object(config_finder, - 'user_config_file') as usercf: - usercf.return_value = ('tests/fixtures/config_files/' - 'user-config.ini') - parsed_config = parser.merge_user_and_local_config() + config_finder.user_config_file = ('tests/fixtures/config_files/' + 'user-config.ini') + parsed_config = parser.merge_user_and_local_config() assert parsed_config == { 'exclude': [ @@ -154,20 +144,24 @@ def test_parse_isolates_config(optmanager): """Verify behaviour of the parse method with isolated=True.""" config_finder = mock.MagicMock() + config_finder.ignore_config_files = True parser = config.MergedConfigParser(optmanager, config_finder) - assert parser.parse(isolated=True) == {} + assert parser.parse() == {} assert config_finder.local_configs.called is False assert config_finder.user_config.called is False def test_parse_uses_cli_config(optmanager): """Verify behaviour of the parse method with a specified config.""" + config_file_value = 'foo.ini' config_finder = mock.MagicMock() + config_finder.config_file = config_file_value + config_finder.ignore_config_files = False parser = config.MergedConfigParser(optmanager, config_finder) - parser.parse(cli_config='foo.ini') - config_finder.cli_config.assert_called_once_with('foo.ini') + parser.parse() + config_finder.cli_config.assert_called_once_with(config_file_value) @pytest.mark.parametrize('config_fixture_path', [ @@ -192,7 +186,7 @@ localcfs.return_value = [config_fixture_path] with mock.patch.object(config_finder, 'user_config_file') as usercf: - usercf.return_value = [] + usercf.return_value = '' parsed_config = parser.merge_user_and_local_config() assert parsed_config['ignore'] == ['E123', 'W234', 'E111'] @@ -215,7 +209,7 @@ max_line_length in our config files. """ optmanager.add_option('--max-line-length', parse_from_config=True, - type='int') + type=int) optmanager.add_option('--enable-extensions', parse_from_config=True, comma_separated_list=True) parser = config.MergedConfigParser(optmanager, config_finder) @@ -224,7 +218,7 @@ localcfs.return_value = [config_file] with mock.patch.object(config_finder, 'user_config_file') as usercf: - usercf.return_value = [] + usercf.return_value = '' parsed_config = parser.merge_user_and_local_config() assert parsed_config['max_line_length'] == 110 diff -Nru python-flake8-3.7.9/tests/unit/test_nothing_formatter.py python-flake8-3.8.3/tests/unit/test_nothing_formatter.py --- python-flake8-3.7.9/tests/unit/test_nothing_formatter.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/tests/unit/test_nothing_formatter.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,15 +1,15 @@ """Tests for the Nothing formatter obbject.""" -import optparse +import argparse from flake8 import style_guide from flake8.formatting import default def options(**kwargs): - """Create an optparse.Values instance.""" + """Create an argparse.Namespace instance.""" kwargs.setdefault('output_file', None) kwargs.setdefault('tee', False) - return optparse.Values(kwargs) + return argparse.Namespace(**kwargs) def test_format_returns_nothing(): diff -Nru python-flake8-3.7.9/tests/unit/test_option_manager.py python-flake8-3.8.3/tests/unit/test_option_manager.py --- python-flake8-3.7.9/tests/unit/test_option_manager.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/tests/unit/test_option_manager.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,11 +1,12 @@ """Unit tests for flake.options.manager.OptionManager.""" -import optparse +import argparse import os import mock import pytest from flake8 import utils +from flake8.main.options import JobsArgument from flake8.options import manager TEST_VERSION = '3.0.0b1' @@ -19,8 +20,39 @@ def test_option_manager_creates_option_parser(optmanager): """Verify that a new manager creates a new parser.""" - assert optmanager.parser is not None - assert isinstance(optmanager.parser, optparse.OptionParser) is True + assert isinstance(optmanager.parser, argparse.ArgumentParser) + + +def test_option_manager_including_parent_options(): + """Verify parent options are included in the parsed options.""" + # GIVEN + parent_parser = argparse.ArgumentParser(add_help=False) + parent_parser.add_argument('--parent') + + # WHEN + optmanager = manager.OptionManager( + prog='flake8', + version=TEST_VERSION, + parents=[parent_parser]) + option, _ = optmanager.parse_args(['--parent', 'foo']) + + # THEN + assert option.parent == 'foo' + + +def test_parse_args_forwarding_default_values(optmanager): + """Verify default provided values are present in the final result.""" + namespace = argparse.Namespace(foo='bar') + options, args = optmanager.parse_args([], namespace) + assert options.foo == 'bar' + + +def test_parse_args_forwarding_type_coercion(optmanager): + """Verify default provided values are type converted from add_option.""" + optmanager.add_option('--foo', type=int) + namespace = argparse.Namespace(foo='5') + options, args = optmanager.parse_args([], namespace) + assert options.foo == 5 def test_add_option_short_option_only(optmanager): @@ -38,7 +70,7 @@ assert optmanager.config_options_dict == {} optmanager.add_option('--long', help='Test long opt') - assert optmanager.options[0].short_option_name is None + assert optmanager.options[0].short_option_name is manager._ARG.NO assert optmanager.options[0].long_option_name == '--long' @@ -119,15 +151,6 @@ ] -def test_format_plugin(): - """Verify that format_plugin turns a tuple into a dictionary.""" - plugin = manager.OptionManager.format_plugin( - manager.PluginVersion('Testing', '0.0.0', False) - ) - assert plugin['name'] == 'Testing' - assert plugin['version'] == '0.0.0' - - def test_generate_versions(optmanager): """Verify a comma-separated string is generated of registered plugins.""" optmanager.registered_plugins = [ @@ -171,7 +194,7 @@ def test_update_version_string(optmanager): """Verify we update the version string idempotently.""" assert optmanager.version == TEST_VERSION - assert optmanager.parser.version == TEST_VERSION + assert optmanager.version_action.version == TEST_VERSION optmanager.registered_plugins = [ manager.PluginVersion('Testing 100', '0.0.0', False), @@ -182,7 +205,7 @@ optmanager.update_version_string() assert optmanager.version == TEST_VERSION - assert (optmanager.parser.version == TEST_VERSION + assert (optmanager.version_action.version == TEST_VERSION + ' (Testing 100: 0.0.0, Testing 101: 0.0.0, Testing 300: 0.0.0) ' + utils.get_python_version()) @@ -211,9 +234,7 @@ assert optmanager.extended_default_ignore == set() optmanager.extend_default_ignore(['T100', 'T101', 'T102']) - assert optmanager.extended_default_ignore == {'T100', - 'T101', - 'T102'} + assert optmanager.extended_default_ignore == {'T100', 'T101', 'T102'} def test_parse_known_args(optmanager): @@ -222,3 +243,139 @@ optmanager.parse_known_args(['--max-complexity', '5']) assert sysexit.called is False + + +def test_optparse_normalize_callback_option_legacy(optmanager): + """Test the optparse shim for `callback=`.""" + callback_foo = mock.Mock() + optmanager.add_option( + '--foo', + action='callback', + callback=callback_foo, + callback_args=(1, 2), + callback_kwargs={'a': 'b'}, + ) + callback_bar = mock.Mock() + optmanager.add_option( + '--bar', + action='callback', + type='string', + callback=callback_bar, + ) + callback_baz = mock.Mock() + optmanager.add_option( + '--baz', + action='callback', + type='string', + nargs=2, + callback=callback_baz, + ) + + optmanager.parse_args(['--foo', '--bar', 'bararg', '--baz', '1', '2']) + + callback_foo.assert_called_once_with( + mock.ANY, # the option / action instance + '--foo', + None, + mock.ANY, # the OptionParser / ArgumentParser + 1, + 2, + a='b', + ) + callback_bar.assert_called_once_with( + mock.ANY, # the option / action instance + '--bar', + 'bararg', + mock.ANY, # the OptionParser / ArgumentParser + ) + callback_baz.assert_called_once_with( + mock.ANY, # the option / action instance + '--baz', + ('1', '2'), + mock.ANY, # the OptionParser / ArgumentParser + ) + + +@pytest.mark.parametrize( + ('type_s', 'input_val', 'expected'), + ( + ('int', '5', 5), + ('long', '6', 6), + ('string', 'foo', 'foo'), + ('float', '1.5', 1.5), + ('complex', '1+5j', 1 + 5j), + # optparse allows this but does not document it + ('str', 'foo', 'foo'), + ), +) +def test_optparse_normalize_types(optmanager, type_s, input_val, expected): + """Test the optparse shim for type="typename".""" + optmanager.add_option('--foo', type=type_s) + opts, args = optmanager.parse_args(['--foo', input_val]) + assert opts.foo == expected + + +def test_optparse_normalize_choice_type(optmanager): + """Test the optparse shim for type="choice".""" + optmanager.add_option('--foo', type='choice', choices=('1', '2', '3')) + opts, args = optmanager.parse_args(['--foo', '1']) + assert opts.foo == '1' + # fails to parse + with pytest.raises(SystemExit): + optmanager.parse_args(['--foo', '4']) + + +def test_optparse_normalize_help(optmanager, capsys): + """Test the optparse shim for %default in help text.""" + optmanager.add_option('--foo', default='bar', help='default: %default') + with pytest.raises(SystemExit): + optmanager.parse_args(['--help']) + out, err = capsys.readouterr() + output = out + err + assert 'default: bar' in output + + +def test_optmanager_group(optmanager, capsys): + """Test that group(...) causes options to be assigned to a group.""" + with optmanager.group('groupname'): + optmanager.add_option('--foo') + with pytest.raises(SystemExit): + optmanager.parse_args(['--help']) + out, err = capsys.readouterr() + output = out + err + assert '\ngroupname:\n' in output + + +@pytest.mark.parametrize( + ("s", "is_auto", "n_jobs"), + ( + ("auto", True, -1), + ("4", False, 4), + ), +) +def test_parse_valid_jobs_argument(s, is_auto, n_jobs): + """Test that --jobs properly parses valid arguments.""" + jobs_opt = JobsArgument(s) + assert is_auto == jobs_opt.is_auto + assert n_jobs == jobs_opt.n_jobs + + +def test_parse_invalid_jobs_argument(optmanager, capsys): + """Test that --jobs properly rejects invalid arguments.""" + namespace = argparse.Namespace() + optmanager.add_option("--jobs", type=JobsArgument) + with pytest.raises(SystemExit): + optmanager.parse_args(["--jobs=foo"], namespace) + out, err = capsys.readouterr() + output = out + err + expected = ( + "\nflake8: error: argument --jobs: " + "'foo' must be 'auto' or an integer.\n" + ) + assert expected in output + + +def test_jobs_argument_str(): + """Test that JobsArgument has a correct __str__.""" + assert str(JobsArgument("auto")) == "auto" + assert str(JobsArgument("123")) == "123" diff -Nru python-flake8-3.7.9/tests/unit/test_option.py python-flake8-3.8.3/tests/unit/test_option.py --- python-flake8-3.7.9/tests/unit/test_option.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/tests/unit/test_option.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,12 +1,14 @@ """Unit tests for flake8.options.manager.Option.""" +import functools + import mock import pytest from flake8.options import manager -def test_to_optparse(): - """Test conversion to an optparse.Option class.""" +def test_to_argparse(): + """Test conversion to an argparse arguments.""" opt = manager.Option( short_option_name='-t', long_option_name='--test', @@ -17,45 +19,26 @@ assert opt.normalize_paths is True assert opt.parse_from_config is True - optparse_opt = opt.to_optparse() - assert not hasattr(optparse_opt, 'parse_from_config') - assert not hasattr(optparse_opt, 'normalize_paths') - assert optparse_opt.action == 'count' - - -@pytest.mark.parametrize('opttype,str_val,expected', [ - ('float', '2', 2.0), - ('complex', '2', (2 + 0j)), -]) -def test_to_support_optparses_standard_types(opttype, str_val, expected): - """Show that optparse converts float and complex types correctly.""" - opt = manager.Option('-t', '--test', type=opttype) - assert opt.normalize_from_setuptools(str_val) == expected - - -@mock.patch('optparse.Option') -def test_to_optparse_creates_an_option_as_we_expect(Option): # noqa: N803 - """Show that we pass all keyword args to optparse.Option.""" - opt = manager.Option('-t', '--test', action='count') - opt.to_optparse() - option_kwargs = { - 'action': 'count', - 'default': None, - 'type': None, - 'dest': 'test', - 'nargs': None, - 'const': None, - 'choices': None, - 'callback': None, - 'callback_args': None, - 'callback_kwargs': None, - 'help': None, - 'metavar': None, - } + args, kwargs = opt.to_argparse() + assert args == ['-t', '--test'] + assert kwargs == {'action': 'count', 'type': mock.ANY} + assert isinstance(kwargs['type'], functools.partial) - Option.assert_called_once_with( - '-t', '--test', **option_kwargs - ) + +def test_to_optparse(): + """Test that .to_optparse() produces a useful error message.""" + with pytest.raises(AttributeError) as excinfo: + manager.Option('--foo').to_optparse + msg, = excinfo.value.args + assert msg == 'to_optparse: flake8 now uses argparse' + + +def test_to_argparse_creates_an_option_as_we_expect(): + """Show that we pass all keyword args to argparse.""" + opt = manager.Option('-t', '--test', action='count') + args, kwargs = opt.to_argparse() + assert args == ['-t', '--test'] + assert kwargs == {'action': 'count'} def test_config_name_generation(): @@ -75,4 +58,4 @@ def test_dest_is_not_overridden(): """Show that we do not override custom destinations.""" opt = manager.Option('-s', '--short', dest='something_not_short') - assert opt.dest == 'something_not_short' # type: ignore + assert opt.dest == 'something_not_short' diff -Nru python-flake8-3.7.9/tests/unit/test_plugin_manager.py python-flake8-3.8.3/tests/unit/test_plugin_manager.py --- python-flake8-3.7.9/tests/unit/test_plugin_manager.py 2019-10-28 08:59:37.000000000 -0700 +++ python-flake8-3.8.3/tests/unit/test_plugin_manager.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,62 +1,58 @@ """Tests for flake8.plugins.manager.PluginManager.""" import mock +from flake8._compat import importlib_metadata from flake8.plugins import manager -def create_entry_point_mock(name): - """Create a mocked EntryPoint.""" - ep = mock.Mock(spec=['name']) - ep.name = name - return ep - - -@mock.patch('entrypoints.get_group_all') -def test_calls_entrypoints_on_instantiation(get_group_all): - """Verify that we call get_group_all when we create a manager.""" - get_group_all.return_value = [] +@mock.patch.object(importlib_metadata, 'entry_points') +def test_calls_entrypoints_on_instantiation(entry_points_mck): + """Verify that we call entry_points() when we create a manager.""" + entry_points_mck.return_value = {} manager.PluginManager(namespace='testing.entrypoints') - - get_group_all.assert_called_once_with('testing.entrypoints') + entry_points_mck.assert_called_once_with() -@mock.patch('entrypoints.get_group_all') -def test_calls_entrypoints_creates_plugins_automaticaly(get_group_all): +@mock.patch.object(importlib_metadata, 'entry_points') +def test_calls_entrypoints_creates_plugins_automaticaly(entry_points_mck): """Verify that we create Plugins on instantiation.""" - get_group_all.return_value = [ - create_entry_point_mock('T100'), - create_entry_point_mock('T200'), - ] + entry_points_mck.return_value = { + 'testing.entrypoints': [ + importlib_metadata.EntryPoint('T100', '', None), + importlib_metadata.EntryPoint('T200', '', None), + ], + } plugin_mgr = manager.PluginManager(namespace='testing.entrypoints') - get_group_all.assert_called_once_with('testing.entrypoints') + entry_points_mck.assert_called_once_with() assert 'T100' in plugin_mgr.plugins assert 'T200' in plugin_mgr.plugins assert isinstance(plugin_mgr.plugins['T100'], manager.Plugin) assert isinstance(plugin_mgr.plugins['T200'], manager.Plugin) -@mock.patch('entrypoints.get_group_all') -def test_handles_mapping_functions_across_plugins(get_group_all): +@mock.patch.object(importlib_metadata, 'entry_points') +def test_handles_mapping_functions_across_plugins(entry_points_mck): """Verify we can use the PluginManager call functions on all plugins.""" - entry_point_mocks = [ - create_entry_point_mock('T100'), - create_entry_point_mock('T200'), - ] - get_group_all.return_value = entry_point_mocks + entry_points_mck.return_value = { + 'testing.entrypoints': [ + importlib_metadata.EntryPoint('T100', '', None), + importlib_metadata.EntryPoint('T200', '', None), + ], + } plugin_mgr = manager.PluginManager(namespace='testing.entrypoints') plugins = [plugin_mgr.plugins[name] for name in plugin_mgr.names] assert list(plugin_mgr.map(lambda x: x)) == plugins -@mock.patch('entrypoints.get_group_all') -def test_local_plugins(get_group_all): +@mock.patch.object(importlib_metadata, 'entry_points') +def test_local_plugins(entry_points_mck): """Verify PluginManager can load given local plugins.""" - get_group_all.return_value = [] + entry_points_mck.return_value = {} plugin_mgr = manager.PluginManager( namespace='testing.entrypoints', local_plugins=['X = path.to:Plugin'] ) - assert plugin_mgr.plugins['X'].entry_point.module_name == 'path.to' + assert plugin_mgr.plugins['X'].entry_point.value == 'path.to:Plugin' diff -Nru python-flake8-3.7.9/tests/unit/test_plugin.py python-flake8-3.8.3/tests/unit/test_plugin.py --- python-flake8-3.7.9/tests/unit/test_plugin.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/tests/unit/test_plugin.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,10 +1,11 @@ """Tests for flake8.plugins.manager.Plugin.""" -import optparse +import argparse import mock import pytest from flake8 import exceptions +from flake8.options import manager as options_manager from flake8.plugins import manager @@ -91,7 +92,7 @@ entry_point = mock.Mock(spec=['load']) plugin_obj = mock.Mock(spec_set=['name', 'version', 'add_options', 'parse_options']) - option_manager = mock.Mock(spec=['register_plugin']) + option_manager = mock.MagicMock(spec=options_manager.OptionManager) plugin = manager.Plugin('T000', entry_point) plugin._plugin = plugin_obj @@ -124,7 +125,7 @@ entry_point = mock.Mock(spec=['load']) plugin_obj = mock.Mock(spec_set=['name', 'version', 'add_options', 'parse_options']) - option_values = optparse.Values({'enable_extensions': []}) + option_values = argparse.Namespace(enable_extensions=[]) option_manager = mock.Mock() plugin = manager.Plugin('T000', entry_point) plugin._plugin = plugin_obj diff -Nru python-flake8-3.7.9/tests/unit/test_plugin_type_manager.py python-flake8-3.8.3/tests/unit/test_plugin_type_manager.py --- python-flake8-3.7.9/tests/unit/test_plugin_type_manager.py 2019-10-28 08:59:37.000000000 -0700 +++ python-flake8-3.8.3/tests/unit/test_plugin_type_manager.py 2020-06-08 12:15:48.000000000 -0700 @@ -13,7 +13,7 @@ plugin = mock.create_autospec(manager.Plugin, instance=True) if raise_exception: plugin.load_plugin.side_effect = exceptions.FailedToLoadPlugin( - plugin=mock.Mock(name='T101'), + plugin_name='T101', exception=ValueError('Test failure'), ) return plugin diff -Nru python-flake8-3.7.9/tests/unit/test_style_guide.py python-flake8-3.8.3/tests/unit/test_style_guide.py --- python-flake8-3.7.9/tests/unit/test_style_guide.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/tests/unit/test_style_guide.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,5 +1,5 @@ """Tests for the flake8.style_guide.StyleGuide class.""" -import optparse +import argparse import mock import pytest @@ -11,7 +11,7 @@ def create_options(**kwargs): - """Create and return an instance of optparse.Values.""" + """Create and return an instance of argparse.Namespace.""" kwargs.setdefault('select', []) kwargs.setdefault('extended_default_select', []) kwargs.setdefault('ignore', []) @@ -19,7 +19,7 @@ kwargs.setdefault('disable_noqa', False) kwargs.setdefault('enable_extensions', []) kwargs.setdefault('per_file_ignores', []) - return optparse.Values(kwargs) + return argparse.Namespace(**kwargs) def test_handle_error_does_not_raise_type_errors(): @@ -78,10 +78,12 @@ options = create_options(per_file_ignores=PER_FILE_IGNORES_UNPARSED) guide = style_guide.StyleGuideManager(options, formatter=formatter) assert len(guide.style_guides) == 5 - assert list(map(utils.normalize_path, - ["first_file.py", "second_file.py", "third_file.py", - "sub_dir/*"]) - ) == [g.filename for g in guide.style_guides[1:]] + expected = [ + utils.normalize_path(p) for p in [ + "first_file.py", "second_file.py", "third_file.py", "sub_dir/*", + ] + ] + assert expected == [g.filename for g in guide.style_guides[1:]] @pytest.mark.parametrize('ignores,violation,filename,handle_error_return', [ diff -Nru python-flake8-3.7.9/tests/unit/test_utils.py python-flake8-3.8.3/tests/unit/test_utils.py --- python-flake8-3.7.9/tests/unit/test_utils.py 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/tests/unit/test_utils.py 2020-06-08 12:15:48.000000000 -0700 @@ -1,4 +1,5 @@ """Tests for flake8's utils module.""" +import logging import os import mock @@ -22,11 +23,7 @@ ("E123,W234,,E206,,", ["E123", "W234", "E206"]), ("E123, W234,, E206,,", ["E123", "W234", "E206"]), ("E123,,W234,,E206,,", ["E123", "W234", "E206"]), - (["E123", "W234", "E206"], ["E123", "W234", "E206"]), - (["E123", "\n\tW234", "\n E206"], ["E123", "W234", "E206"]), - (["E123", "\n\tW234", "\n E206", "\n"], ["E123", "W234", "E206"]), - (["E123", "\n\tW234", "", "\n E206", "\n"], ["E123", "W234", "E206"]), - (["E123", "\n\tW234", "", "\n E206", ""], ["E123", "W234", "E206"]), + ("", []), ]) def test_parse_comma_separated_list(value, expected): """Verify that similar inputs produce identical outputs.""" @@ -132,14 +129,13 @@ @pytest.mark.parametrize("value,expected", [ - ("flake8,pep8,pyflakes,mccabe", ["flake8", "pep8", "pyflakes", "mccabe"]), - ("flake8,\n\tpep8,\n pyflakes,\n\n mccabe", + (["flake8", "pep8", "pyflakes", "mccabe"], ["flake8", "pep8", "pyflakes", "mccabe"]), - ("../flake8,../pep8,../pyflakes,../mccabe", + (["../flake8", "../pep8", "../pyflakes", "../mccabe"], [os.path.abspath("../" + p) for p in RELATIVE_PATHS]), ]) def test_normalize_paths(value, expected): - """Verify we normalize comma-separated paths provided to the tool.""" + """Verify we normalizes a sequence of paths provided to the tool.""" assert utils.normalize_paths(value) == expected @@ -163,29 +159,68 @@ assert utils.fnmatch(filename, patterns) is expected +@pytest.fixture +def files_dir(tmpdir): + """Create test dir for testing filenames_from.""" + with tmpdir.as_cwd(): + tmpdir.join('a/b/c.py').ensure() + tmpdir.join('a/b/d.py').ensure() + tmpdir.join('a/b/e/f.py').ensure() + yield tmpdir + + +def _normpath(s): + return s.replace('/', os.sep) + + +def _normpaths(pths): + return {_normpath(pth) for pth in pths} + + +@pytest.mark.usefixtures('files_dir') def test_filenames_from_a_directory(): """Verify that filenames_from walks a directory.""" - filenames = list(utils.filenames_from('src/flake8/')) - assert len(filenames) > 2 - assert 'src/flake8/__init__.py' in filenames + filenames = set(utils.filenames_from(_normpath('a/b/'))) + # should include all files + expected = _normpaths(('a/b/c.py', 'a/b/d.py', 'a/b/e/f.py')) + assert filenames == expected +@pytest.mark.usefixtures('files_dir') def test_filenames_from_a_directory_with_a_predicate(): """Verify that predicates filter filenames_from.""" - filenames = list(utils.filenames_from( - arg='src/flake8/', - predicate=lambda filename: filename == 'flake8/__init__.py', + filenames = set(utils.filenames_from( + arg=_normpath('a/b/'), + predicate=lambda path: path.endswith(_normpath('b/c.py')), )) - assert len(filenames) > 2 - assert 'flake8/__init__.py' not in filenames + # should not include c.py + expected = _normpaths(('a/b/d.py', 'a/b/e/f.py')) + assert filenames == expected +@pytest.mark.usefixtures('files_dir') +def test_filenames_from_a_directory_with_a_predicate_from_the_current_dir(): + """Verify that predicates filter filenames_from.""" + filenames = set(utils.filenames_from( + arg=_normpath('./a/b'), + predicate=lambda path: path == 'c.py', + )) + # none should have matched the predicate so all returned + expected = _normpaths(('./a/b/c.py', './a/b/d.py', './a/b/e/f.py')) + assert filenames == expected + + +@pytest.mark.usefixtures('files_dir') def test_filenames_from_a_single_file(): """Verify that we simply yield that filename.""" - filenames = list(utils.filenames_from('flake8/__init__.py')) + filenames = set(utils.filenames_from(_normpath('a/b/c.py'))) + assert filenames == {_normpath('a/b/c.py')} - assert len(filenames) == 1 - assert ['flake8/__init__.py'] == filenames + +def test_filenames_from_a_single_file_does_not_exist(): + """Verify that a passed filename which does not exist is returned back.""" + filenames = set(utils.filenames_from(_normpath('d/n/e.py'))) + assert filenames == {_normpath('d/n/e.py')} def test_filenames_from_exclude_doesnt_exclude_directory_names(tmpdir): @@ -207,7 +242,7 @@ """Verify that we can retrieve the parameters for a class plugin.""" class FakeCheck(object): def __init__(self, tree): - pass + raise NotImplementedError plugin = plugin_manager.Plugin('plugin-name', object()) plugin._plugin = FakeCheck @@ -217,7 +252,7 @@ def test_parameters_for_function_plugin(): """Verify that we retrieve the parameters for a function plugin.""" def fake_plugin(physical_line, self, tree, optional=None): - pass + raise NotImplementedError plugin = plugin_manager.Plugin('plugin-name', object()) plugin._plugin = fake_plugin @@ -262,3 +297,10 @@ def test_parse_unified_diff(diff, parsed_diff): """Verify that what we parse from a diff matches expectations.""" assert utils.parse_unified_diff(diff) == parsed_diff + + +def test_matches_filename_for_excluding_dotfiles(): + """Verify that `.` and `..` are not matched by `.*`.""" + logger = logging.Logger(__name__) + assert not utils.matches_filename('.', ('.*',), '', logger) + assert not utils.matches_filename('..', ('.*',), '', logger) diff -Nru python-flake8-3.7.9/tox.ini python-flake8-3.8.3/tox.ini --- python-flake8-3.7.9/tox.ini 2019-10-28 10:30:21.000000000 -0700 +++ python-flake8-3.8.3/tox.ini 2020-06-08 12:15:48.000000000 -0700 @@ -1,16 +1,18 @@ [tox] minversion=2.3.1 -envlist = py27,py34,py35,py36,py37,flake8,linters,docs +envlist = py27,py34,py35,py36,py37,py38,flake8,linters,docs [testenv] deps = mock>=2.0.0 - pytest!=3.0.5 + pytest!=3.0.5,!=5.2.3 coverage commands = - coverage run --parallel-mode -m pytest {posargs} + coverage run -m pytest {posargs} coverage combine - coverage report -m + coverage report + # ensure 100% coverage of tests + coverage report --fail-under 100 --include tests/* [testenv:venv] deps = @@ -19,7 +21,7 @@ # Dogfood our current master version [testenv:dogfood] -basepython = python2.7 +basepython = python3 skip_install = true deps = wheel @@ -45,10 +47,9 @@ skip_install = true deps = flake8 + flake8-bugbear flake8-colors - flake8-docstrings>=0.2.7 - # remove when https://gitlab.com/pycqa/flake8-docstrings/issues/36 is fixed - pydocstyle<4 + flake8-docstrings>=1.3.1 flake8-import-order>=0.9 flake8-typing-imports>=1.1 pep8-naming @@ -60,7 +61,7 @@ skip_install = true deps = pyflakes - pylint + pylint!=2.5.0 commands = pylint src/flake8