diff --git a/.gitignore b/.gitignore index 167e8d932..3a0dd8a09 100644 --- a/.gitignore +++ b/.gitignore @@ -203,3 +203,6 @@ fabric.properties # Used for testing: ex.py setup.py + +# Usually you don't need to gitignore this file, but we use it for testing: +/.flake8-baseline.json diff --git a/.importlinter b/.importlinter index e58072ee8..32c4c5b25 100644 --- a/.importlinter +++ b/.importlinter @@ -13,6 +13,7 @@ containers = layers = checker formatter + patches transformations presets visitors @@ -74,6 +75,7 @@ ignore_imports = # These modules must import from flake8 to provide required API: wemake_python_styleguide.checker -> flake8 wemake_python_styleguide.formatter -> flake8 + wemake_python_styleguide.patches.baseline -> flake8 wemake_python_styleguide.options.config -> flake8 # We disallow direct imports of our dependencies from anywhere, except: wemake_python_styleguide.formatter -> pygments diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a0a5671b..47f33dff8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ removing dependencies that can be removed, and fixing bugs. There are breaking changes ahead! -We also have this [nice migration guide](https://wemake-python-stylegui.de/en/latest/pages/changelog/migration_to_0_14.html). +We also have this [0.14 migration guide](https://wemake-python-stylegui.de/en/latest/pages/changelog/migration_to_0_14.html). ### Features @@ -461,7 +461,7 @@ We had to fix it before it is too late. So, we broke existing error codes. And now we can promise not to do it ever again. -We also have this [nice migration guide](https://wemake-python-stylegui.de/en/latest/pages/changelog/migration_to_0_11.html) +We also have this [0.11 migration guide](https://wemake-python-stylegui.de/en/latest/pages/changelog/migration_to_0_11.html) for you to rename your violations with a script. ### Features diff --git a/README.md b/README.md index fe34b456a..caab1de9c 100644 --- a/README.md +++ b/README.md @@ -30,16 +30,11 @@ pip install wemake-python-styleguide You will also need to create a `setup.cfg` file with the [configuration](https://wemake-python-stylegui.de/en/latest/pages/usage/configuration.html). -We highly recommend to also use: - -- [flakehell](https://wemake-python-stylegui.de/en/latest/pages/usage/integrations/flakehell.html) for easy integration into a **legacy** codebase -- [nitpick](https://wemake-python-stylegui.de/en/latest/pages/usage/integrations/nitpick.html) for sharing and validating configuration across multiple projects - ## Running ```bash -flake8 your_module.py +flake8 your_project ``` This app is still just good old `flake8`! @@ -50,6 +45,17 @@ And it won't change your existing workflow. alt="invocation resuts">

+We also support just a **single command** +for [incremental adoption](https://wemake-python-stylegui.de/en/latest/pages/usage/setup.html#incremental-adoption) +of this linter into any existing codebase: + +```bash +flake8 --baseline your_project +``` + +Done! Now linter will not report any old violations. +And will only report new ones created after the baseline was generated. + See ["Usage" section](https://wemake-python-stylegui.de/en/latest/pages/usage/setup.html) in the docs for examples and integrations. diff --git a/docs/_static/baseline-contents.png b/docs/_static/baseline-contents.png new file mode 100644 index 000000000..c58b05d99 Binary files /dev/null and b/docs/_static/baseline-contents.png differ diff --git a/docs/_static/baseline-existing.png b/docs/_static/baseline-existing.png new file mode 100644 index 000000000..a86716464 Binary files /dev/null and b/docs/_static/baseline-existing.png differ diff --git a/docs/_static/baseline-initial.png b/docs/_static/baseline-initial.png new file mode 100644 index 000000000..61d4cc8b4 Binary files /dev/null and b/docs/_static/baseline-initial.png differ diff --git a/docs/_static/baseline-new-violations.png b/docs/_static/baseline-new-violations.png new file mode 100644 index 000000000..fd10a2960 Binary files /dev/null and b/docs/_static/baseline-new-violations.png differ diff --git a/docs/pages/usage/integrations/flakehell.rst b/docs/pages/usage/integrations/flakehell.rst deleted file mode 100644 index 009e65ed4..000000000 --- a/docs/pages/usage/integrations/flakehell.rst +++ /dev/null @@ -1,89 +0,0 @@ -.. _flakehell: - -flakehell ---------- - -``flakehell`` is a `legacy-first `_ -wrapper around ``flake8`` linter to make it awesome. - -.. image:: https://raw.githubusercontent.com/life4/flakehell/master/assets/logo.png - -What does it mean? It means, that it adds some useful -features to the core ``flake8`` with the new command line utility: - -.. code:: bash - - pip install flakehell # however we recommend to use `poetry` - -Then you will have to configure ``flakehell`` inside your ``pyproject.toml``: - -- You can run ``flakehell plugins`` to see what plugins are you missing - and configure it properly -- Or you can use our `preset `_ - as ``base`` configuration like so: - - .. code:: toml - - [tool.flakehell] - # optionally inherit from remote config (or local if you want) - base = "https://github.com/wemake-services/wemake-python-styleguide/blob/master/styles/flakehell.toml" - -And then: - -.. code:: bash - - flakehell lint # accepts the same arguments, does the same as `flake8` - -The most exciting feature for us is ``baseline`` generation. - -.. _flakehell-legacy: - -Legacy first -~~~~~~~~~~~~ - -When you project is old you cannot just install and use a new linter. -Since your codebase will contain hundreds or even thousands of violations. - -Some of them can be auto-formatted, some of them can be silenced. -But, what if there are still too many of them to fix right here and right now? - -Let me introduce the ``baseline`` concept: - -1. The first step is to create a ``baseline`` via: - - .. code:: bash - - flakehell baseline > .flakehell_baseline - - It will contain all your current violations list - with exact locations and quantity. -2. Then specify the ``baseline`` in the configuration: - - .. code:: ini - - # Inside your pyproject.toml - [tool.flakehell] - baseline = ".flakehell_baseline" - -3. Run your linter again with ``flakehell lint``. You will see no violations! -4. Try to add a new one into your source code. - And run your linter again. It will be reported! - -The ``baseline`` method allows you to report any new violations -and fix the old ones little by little. -So, the integration is almost painless. - -That's why we call it "legacy-first". -Enjoy your new linter in your old project! - -Support -~~~~~~~ - -``flakehell`` is officially supported by ``wemake-python-styleguide`` -and developed by the same people. - -Further reading -~~~~~~~~~~~~~~~ - -- Our :ref:`legacy` guide -- Official docs: `flakehell.readthedocs.io `_ diff --git a/docs/pages/usage/integrations/index.rst b/docs/pages/usage/integrations/index.rst index 75c5b50fc..d730f32a8 100644 --- a/docs/pages/usage/integrations/index.rst +++ b/docs/pages/usage/integrations/index.rst @@ -1,12 +1,11 @@ .. toctree:: :hidden: + legacy.rst plugins.rst editors.rst auto-formatters.rst nitpick.rst - legacy.rst - flakehell.rst docker.rst github-actions.rst ci.rst diff --git a/docs/pages/usage/integrations/legacy.rst b/docs/pages/usage/integrations/legacy.rst index cc9b57957..9b968d6a9 100644 --- a/docs/pages/usage/integrations/legacy.rst +++ b/docs/pages/usage/integrations/legacy.rst @@ -1,30 +1,161 @@ .. _legacy: Legacy projects ---------------- +=============== Introducing this package to a legacy project is going to be a challenge. Due to our strict quality, consistency, and complexity rules. But, you still can do several things to integrate this linter step by step: -1. Fix consistency, naming and best-practices violations, +1. Generate a baseline to ignore existing violations + and only report new ones that were created after the baseline. +2. Fix consistency, naming and best-practices violations, they are the easiest to clean up. -2. Per-file ignore complexity checks that are failing for your project. +3. Per-file ignore complexity checks that are failing for your project. Sometimes it is possible to rewrite several parts of your code, but generally complexity rules are the hardest to fix. -3. Use `boyscout rule `_: always leave +4. Use `boyscout rule `_: always leave your code better than you found it. To make sure "boyscout rule" works we offically support ``--diff`` mode. The main idea of it is simple: we only lint things that we touch. -We also support :ref:`flakehell-legacy` (external tool) -to create a ``baseline`` of your currect violations -and start to lint only new one from this point. - Choose what suits you best. +.. _baseline: + +Baseline +-------- + +You can start using our linter with just a single command! + +.. code:: bash + + flake8 --baseline your_project + +This guide will explain how it works. + +Steps +~~~~~ + +There are several steps in how baseline works. + +We can run the linter with ``--baseline`` mode enabled. +What will happen? + +If you don't have ``.flake8-baseline.json``, +then a new one will be created containing all the violations you have. +The first time all violations will be reported. +We do this to show the contents of the future baseline to the developer. +Futher runs won't report any of the violations inside the baseline. + +If you already have ``.flake8-baseline.json`` file, +than your baselined violations will be ignored. + +However, new violations will still be reported. + +Updating baseline +~~~~~~~~~~~~~~~~~ + +To update a baseline you can delete the old one: + +.. code:: bash + + rm .flake8-baseline.json + +And create a new one with ``--baseline`` flag: + +.. code:: bash + + flake8 --baseline your_project + +Baseline contents +~~~~~~~~~~~~~~~~~ + +Things we care when working with baselines: + +1. Violation codes and text descriptions +2. Filenames + +When these values change +(for example: file is renamed or violation code is changed), +we will treat these violations as new ones. +And report them to the user as usual. + +Things we don't care when working with baselines: + +1. Violation locations, because lines and columns + can be easily changed by simple refactoring +2. Activated plugins +3. Config values +4. Target files and directories + +So, when you add new plugins or change any config values, +then you might want ot update the baseline as well. + +Full baseline example +~~~~~~~~~~~~~~~~~~~~~ + +You start with a legacy file that looks like this: + +.. code:: python + + # ex.py + x = 1 + +Let's lint it and ignore existing errors! + +.. code:: bash + + flake8 --baseline ex.py + +.. image:: https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/master/docs/_static/baseline-initial.png + +We are seeing our violation. Works as expected. +Also, now your baseline is generated. Let's see that it works: + +.. code:: bash + + cat .flake8-baseline.json + +.. image:: https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/master/docs/_static/baseline-contents.png + +Yes, here it is. It contains a single violation from your ``ex.py`` file. +Let's run ``flake8`` again to see that no violations are going +to be reported with a baseline: + +.. code:: bash + + flake8 --baseline ex.py + +.. image:: https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/master/docs/_static/baseline-existing.png + +That works! No violations are reported. +Because baseline covers all existing ones. +Let's add some new ones to test that it will raise a violation: + +.. code:: python + + # ex.py + x = 1 + y = 1 + +And run the linter: + +.. code:: bash + + flake8 --baseline ex.py + +.. image:: https://raw.githubusercontent.com/wemake-services/wemake-python-styleguide/master/docs/_static/baseline-new-violations.png + +And yes, new violation is reported! It works just as we planned. +Enjoy your incremental adoption! + + +Linting diffs +------------- + Existing legacy ~~~~~~~~~~~~~~~ @@ -119,7 +250,3 @@ And you are forced to improve things you write. At some point in time, you will have 100% perfect code. Good linters and constant refactoring is the key to the success. - -.. rubric:: Further reading - -- :ref:`Creating baselines for legacy projects ` diff --git a/docs/pages/usage/setup.rst b/docs/pages/usage/setup.rst index 08943f5cf..4c4e1cf3d 100644 --- a/docs/pages/usage/setup.rst +++ b/docs/pages/usage/setup.rst @@ -17,9 +17,8 @@ We also recommend to use `poetry `_ instead of a default ``pip``. You might want to also install optional tools -that pairs nicely with ``wemake-python-styleguide``: +that pair nicely with ``wemake-python-styleguide``: -- :ref:`flakehell` for easy integration into a **legacy** codebase - :ref:`nitpick` for sharing and validating configuration across multiple projects @@ -41,15 +40,51 @@ See the ``flake8`` docs for `options `_. Golden rule is to run your linter on each commit locally and inside the CI. -And to fail the build if there are any style violations. +And to fail the build if there are any violations. Check out how we do it in our different templates: - ``django`` and ``gitlab-ci``: https://github.com/wemake-services/wemake-django-template - ``python`` package and ``travis``: https://github.com/wemake-services/wemake-python-package + +Incremental adoption +-------------------- + +A very popular use-case is when you already +have a relatively large codebase and want to addopt a new linter. + +Usually, it is a big pain: you have to spend a lot of time +silencing tens or hundreds of violations. +And you will end up with lots +of silencing individual violations and refactoring. + +It is doable, but takes a lot of time. +And makes the adoption of this linter pretty complicated. + +Let me introduce you ``--baseline`` option: + +.. code:: bash + + flake8 --baseline your_project + +Here's what it does: + +1. When you run ``--baseline`` mode for the first time + it will report and record all the violations you have at the moment + into a new file called ``.flake8-baseline.json`` + +2. If you try to run the same command once again, it will report no violations. + Why? Because all of them was saved as existing ones. + Now linter will report only new violations + that are not saved into the baseline. + +Done! Now you can integrate this linter +into any codebase with just a single command! + + .. rubric:: Further reading - :ref:`Configuring and ignoring violations ` -- :ref:`Intoducing this linter to a legacy codebase ` +- :ref:`Baseline usage ` - :ref:`Sharing configuration across multiple projects ` diff --git a/poetry.lock b/poetry.lock index 3d8a66f89..28787f425 100644 --- a/poetry.lock +++ b/poetry.lock @@ -634,7 +634,7 @@ marker = "python_version < \"3.7\"" name = "importlib-resources" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -version = "1.3.1" +version = "1.4.0" [package.dependencies] [package.dependencies.importlib-metadata] @@ -646,7 +646,7 @@ python = "<3.8" version = ">=0.4" [package.extras] -docs = ["sphinx", "docutils (0.12)", "rst.linker"] +docs = ["sphinx", "rst.linker", "jaraco.packaging"] [[package]] category = "dev" @@ -1045,7 +1045,7 @@ wcwidth = "*" [[package]] category = "dev" description = "Run a subprocess in a pseudo terminal" -marker = "python_version >= \"3.4\" and sys_platform != \"win32\" or sys_platform != \"win32\" or python_version >= \"3.4\" and sys_platform != \"win32\" and (python_version >= \"3.4\" and sys_platform != \"win32\" or sys_platform != \"win32\")" +marker = "python_version >= \"3.4\" and sys_platform != \"win32\" or sys_platform != \"win32\"" name = "ptyprocess" optional = false python-versions = "*" @@ -1186,7 +1186,7 @@ description = "YAML parser and emitter for Python" name = "pyyaml" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" -version = "5.3" +version = "5.3.1" [[package]] category = "dev" @@ -1565,7 +1565,7 @@ description = "Virtual Python Environment builder" name = "virtualenv" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" -version = "20.0.10" +version = "20.0.13" [package.dependencies] appdirs = ">=1.4.3,<2" @@ -1608,7 +1608,7 @@ description = "Measures number of Terminal column cells of wide-character codes" name = "wcwidth" optional = false python-versions = "*" -version = "0.1.8" +version = "0.1.9" [[package]] category = "main" @@ -1872,8 +1872,8 @@ importlib-metadata = [ {file = "importlib_metadata-1.5.0.tar.gz", hash = "sha256:06f5b3a99029c7134207dd882428a66992a9de2bef7c2b699b5641f9886c3302"}, ] importlib-resources = [ - {file = "importlib_resources-1.3.1-py2.py3-none-any.whl", hash = "sha256:1dff36d42d94bd523eeb847c25c7dd327cb56686d74a26dfcc8d67c504922d59"}, - {file = "importlib_resources-1.3.1.tar.gz", hash = "sha256:7f0e1b2b5f3981e39c52da0f99b2955353c5a139c314994d1126c2551ace9bdf"}, + {file = "importlib_resources-1.4.0-py2.py3-none-any.whl", hash = "sha256:dd98ceeef3f5ad2ef4cc287b8586da4ebad15877f351e9688987ad663a0a29b8"}, + {file = "importlib_resources-1.4.0.tar.gz", hash = "sha256:4019b6a9082d8ada9def02bece4a76b131518866790d58fdda0b5f8c603b36c2"}, ] ipdb = [ {file = "ipdb-0.12.3.tar.gz", hash = "sha256:5d9a4a0e3b7027a158fc6f2929934341045b9c3b0b86ed5d7e84e409653f72fd"}, @@ -2084,17 +2084,17 @@ pytz = [ {file = "pytz-2019.3.tar.gz", hash = "sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"}, ] pyyaml = [ - {file = "PyYAML-5.3-cp27-cp27m-win32.whl", hash = "sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d"}, - {file = "PyYAML-5.3-cp27-cp27m-win_amd64.whl", hash = "sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6"}, - {file = "PyYAML-5.3-cp35-cp35m-win32.whl", hash = "sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e"}, - {file = "PyYAML-5.3-cp35-cp35m-win_amd64.whl", hash = "sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689"}, - {file = "PyYAML-5.3-cp36-cp36m-win32.whl", hash = "sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994"}, - {file = "PyYAML-5.3-cp36-cp36m-win_amd64.whl", hash = "sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e"}, - {file = "PyYAML-5.3-cp37-cp37m-win32.whl", hash = "sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5"}, - {file = "PyYAML-5.3-cp37-cp37m-win_amd64.whl", hash = "sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf"}, - {file = "PyYAML-5.3-cp38-cp38-win32.whl", hash = "sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811"}, - {file = "PyYAML-5.3-cp38-cp38-win_amd64.whl", hash = "sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20"}, - {file = "PyYAML-5.3.tar.gz", hash = "sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615"}, + {file = "PyYAML-5.3.1-cp27-cp27m-win32.whl", hash = "sha256:74809a57b329d6cc0fdccee6318f44b9b8649961fa73144a98735b0aaf029f1f"}, + {file = "PyYAML-5.3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:240097ff019d7c70a4922b6869d8a86407758333f02203e0fc6ff79c5dcede76"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win32.whl", hash = "sha256:4f4b913ca1a7319b33cfb1369e91e50354d6f07a135f3b901aca02aa95940bd2"}, + {file = "PyYAML-5.3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:cc8955cfbfc7a115fa81d85284ee61147059a753344bc51098f3ccd69b0d7e0c"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win32.whl", hash = "sha256:7739fc0fa8205b3ee8808aea45e968bc90082c10aef6ea95e855e10abf4a37b2"}, + {file = "PyYAML-5.3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:69f00dca373f240f842b2931fb2c7e14ddbacd1397d57157a9b005a6a9942648"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win32.whl", hash = "sha256:d13155f591e6fcc1ec3b30685d50bf0711574e2c0dfffd7644babf8b5102ca1a"}, + {file = "PyYAML-5.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:73f099454b799e05e5ab51423c7bcf361c58d3206fa7b0d555426b1f4d9a3eaf"}, + {file = "PyYAML-5.3.1-cp38-cp38-win32.whl", hash = "sha256:06a0d7ba600ce0b2d2fe2e78453a470b5a6e000a985dd4a4e54e436cc36b0e97"}, + {file = "PyYAML-5.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:95f71d2af0ff4227885f7a6605c37fd53d3a106fcab511b8860ecca9fcf400ee"}, + {file = "PyYAML-5.3.1.tar.gz", hash = "sha256:b8eac752c5e14d3eca0e6dd9199cd627518cb5ec06add0de9d32baeee6fe645d"}, ] requests = [ {file = "requests-2.23.0-py2.py3-none-any.whl", hash = "sha256:43999036bfa82904b6af1d99e4882b560e5e2c68e5c4b0aa03b655f3d7d73fee"}, @@ -2256,8 +2256,8 @@ urllib3 = [ {file = "urllib3-1.25.8.tar.gz", hash = "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"}, ] virtualenv = [ - {file = "virtualenv-20.0.10-py2.py3-none-any.whl", hash = "sha256:10750cac3b5a9e6eed54d0f1f8222c550dc47f84609c95cbc504d44a58a048b8"}, - {file = "virtualenv-20.0.10.tar.gz", hash = "sha256:8512e83f1d90f8e481024d58512ac9c053bf16f54d9138520a0929396820dd78"}, + {file = "virtualenv-20.0.13-py2.py3-none-any.whl", hash = "sha256:87831f1070534b636fea2241dd66f3afe37ac9041bcca6d0af3215cdcfbf7d82"}, + {file = "virtualenv-20.0.13.tar.gz", hash = "sha256:f3128d882383c503003130389bf892856341c1da12c881ae24d6358c82561b55"}, ] virtualenv-clone = [ {file = "virtualenv-clone-0.5.3.tar.gz", hash = "sha256:c88ae171a11b087ea2513f260cdac9232461d8e9369bcd1dc143fc399d220557"}, @@ -2272,8 +2272,8 @@ wasmer = [ {file = "wasmer-0.4.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:fa1c8479781c91e6b814ef006f6e099b6550eba12601a8617475fb514fc09365"}, ] wcwidth = [ - {file = "wcwidth-0.1.8-py2.py3-none-any.whl", hash = "sha256:8fd29383f539be45b20bd4df0dc29c20ba48654a41e661925e612311e9f3c603"}, - {file = "wcwidth-0.1.8.tar.gz", hash = "sha256:f28b3e8a6483e5d49e7f8949ac1a78314e740333ae305b4ba5defd3e74fb37a8"}, + {file = "wcwidth-0.1.9-py2.py3-none-any.whl", hash = "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1"}, + {file = "wcwidth-0.1.9.tar.gz", hash = "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"}, ] zipp = [ {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, diff --git a/setup.cfg b/setup.cfg index bda17d33b..c88032d0a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -106,15 +106,16 @@ addopts = [coverage:run] # Coverage configuration: https://coverage.readthedocs.io/ -# We don't need to cover some files. They are fully checked with mypy. -# And don't contain any logic. -omit = - wemake_python_styleguide/types.py - # Here we specify plugins for coverage to be used: plugins = coverage_conditional_plugin +[coverage:report] +# We exclude two lines from default coverage: +exclude_lines = + \# pragma: no cover\b + ^ +\.\.\.$ + [coverage:coverage_conditional_plugin] # Here we specify our pragma rules: rules = diff --git a/styles/flakehell.toml b/styles/flakehell.toml deleted file mode 100644 index 9903d1046..000000000 --- a/styles/flakehell.toml +++ /dev/null @@ -1,33 +0,0 @@ -# `flakehell` specific configuration. -# This file is served as `base` config under `[tool.flakehell]` - -# Make sure to install it with: -# `pip install flakehell` - -# See: -# https://wemake-python-stylegui.de/en/latest/pages/usage/integrations/flakehell.html - -# This file is not related to `nitpick` at all. -# This file is optional, not required for `flake8` users. - -[tool.flakehell] -format = "grouped" -show_source = true -statistics = false -doctests = true -enable_extensions = "G" - -accept_encodings = "utf-8" -max_complexity = 6 -max_line_length = 80 - -ignore = "D100, D104, D401, W504, RST303, RST304, DAR103, DAR203" - -[tool.flakehell.plugins] -"flake8-*" = ["+*"] -mccabe = ["+*"] -nitpick = ["+*"] -"pep8-naming" = ["+*"] -pycodestyle = ["+*"] -pyflakes = ["+*"] -"wemake-python-styleguide" = ["+*"] diff --git a/tests/conftest.py b/tests/conftest.py index bcc138889..f46576679 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,3 @@ -import os from collections import namedtuple import pytest @@ -6,6 +5,7 @@ from wemake_python_styleguide.options.config import Configuration pytest_plugins = [ + 'plugins.files', 'plugins.violations', 'plugins.ast_tree', 'plugins.tokenize_parser', @@ -13,15 +13,6 @@ ] -@pytest.fixture(scope='session') -def absolute_path(): - """Fixture to create full path relative to `contest.py` inside tests.""" - def factory(*files: str): - dirname = os.path.dirname(__file__) - return os.path.join(dirname, *files) - return factory - - @pytest.fixture(scope='session') def options(): """Returns the options builder.""" diff --git a/tests/plugins/files.py b/tests/plugins/files.py new file mode 100644 index 000000000..aa879881e --- /dev/null +++ b/tests/plugins/files.py @@ -0,0 +1,46 @@ +import os + +import pytest + +_TEMP_FOLDER = 'tmp' +_MODE_EXECUTABLE = 0o755 +_MODE_NON_EXECUTABLE = 0o644 + + +@pytest.fixture() +def make_file(tmp_path): + """Fixture to make a temporary executable or non executable file.""" + def factory( + filename: str, + file_content: str, + *, + is_executable: bool = False, + ) -> str: + temp_folder = tmp_path / _TEMP_FOLDER + temp_folder.mkdir(exist_ok=True) + test_file = temp_folder / filename + file_mode = _MODE_EXECUTABLE if is_executable else _MODE_NON_EXECUTABLE + + test_file.write_text(file_content) + os.chmod(test_file.as_posix(), file_mode) + + return test_file.as_posix() + return factory + + +@pytest.fixture(scope='session') +def read_file(absolute_path): + """Fixture to get the file contents.""" + def factory(filename: str) -> str: + with open(filename) as file_obj: + return file_obj.read() + return factory + + +@pytest.fixture(scope='session') +def absolute_path(): + """Fixture to create full path relative to `contest.py` inside tests.""" + def factory(*files: str): + dirname = os.path.dirname(os.path.dirname(__file__)) + return os.path.join(dirname, *files) + return factory diff --git a/tests/plugins/tokenize_parser.py b/tests/plugins/tokenize_parser.py index 9c5df52f8..0ad9fb832 100644 --- a/tests/plugins/tokenize_parser.py +++ b/tests/plugins/tokenize_parser.py @@ -19,6 +19,5 @@ def parse_file_tokens(parse_tokens): """Parses tokens from a file.""" def factory(filename: str): with open(filename, 'r', encoding='utf-8') as test_file: - file_content = test_file.read() - return parse_tokens(file_content) + return parse_tokens(test_file.read()) return factory diff --git a/tests/test_patches/test_baseline/test_baseline_option/test_baseline_command.py b/tests/test_patches/test_baseline/test_baseline_option/test_baseline_command.py new file mode 100644 index 000000000..4336e34e7 --- /dev/null +++ b/tests/test_patches/test_baseline/test_baseline_option/test_baseline_command.py @@ -0,0 +1,233 @@ +""" +We use this test to ensure that ``--baseline`` works correctly. + +Here are several important things about this example: + +1. There are two violations with the same code and message: ``E225`` + +2. There are two violations with the same code, + but different message: ``WPS110`` + +3. There's a unique violation: ``WPS303`` + +We also have the second file around with just one ``WPS304`` inside. + +All violations in this example are covered by the baseline. +""" + +import os +import subprocess + +import pytest + +from wemake_python_styleguide.logic.baseline import BASELINE_FILE + +baseline = """{ + "paths": { + "other_wrong.py": { + "e61cb3c3de1cbdac603069903e4af07c": 1 + }, + "wrong.py": { + "132954ef45e1a84ab72bb6e30126a117": 1, + "71b49f6407bbc09ac76c372014207dfb": 1, + "a37c5ca31e3d75d49a018c0bd3ff83f5": 1, + "dd2402e2213add848d53f8580452417e": 2 + } + } +}""" + +# Templates: + +wrong_template = """ +value =1 +result= 2 +undescored_number = 10_0 +{0} +""" + +wrong_other = 'partial = .5' + +# Filenames: + +filename_wrong = 'wrong.py' +filename_other = 'other_wrong.py' + + +def _assert_output(output: str, errors): + for error_code, error_count in errors.items(): + assert output.count(error_code) == error_count + + +def test_without_baseline(make_file, read_file): + """End-to-End test for no baseline yet, initial mode.""" + filename = make_file(filename_wrong, wrong_template.format('')) + make_file(filename_other, wrong_other) + cwd = os.path.dirname(filename) + + process = subprocess.Popen( + [ + 'flake8', + '--isolated', + '--baseline', + '--select', + 'WPS,E', + filename_wrong, + filename_other, + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + encoding='utf8', + cwd=cwd, + ) + output, _ = process.communicate() + + _assert_output(output, {'WPS110': 2, 'WPS303': 1, 'WPS304': 1, 'E225': 2}) + assert process.returncode == 1 + assert read_file(os.path.join(cwd, BASELINE_FILE)) == baseline + + +@pytest.mark.parametrize('files_to_check', [ + (filename_wrong,), + (filename_wrong, filename_other), + (filename_wrong, 'missing.py'), + (filename_wrong, filename_other, 'missing.py'), +]) +def test_with_baseline(make_file, read_file, files_to_check): + """End-to-End test for baseline generation.""" + filename = make_file(filename_wrong, wrong_template.format('')) + make_file(filename_other, wrong_other) + baseline_path = make_file(BASELINE_FILE, baseline) + + process = subprocess.Popen( + [ + 'flake8', + '--isolated', + '--baseline', + '--select', + 'WPS,E', + *files_to_check, + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + encoding='utf8', + cwd=os.path.dirname(filename), + ) + output, _ = process.communicate() + + assert output == '' + assert process.returncode == 0 + assert read_file(baseline_path) == baseline + + +def test_with_baseline_empty(make_file, read_file): + """End-to-End test that removed violations are fine.""" + filename = make_file(filename_wrong, '_SOME_CONSTANT = 1') + baseline_path = make_file(BASELINE_FILE, baseline) + + process = subprocess.Popen( + [ + 'flake8', + '--isolated', + '--baseline', + '--select', + 'WPS,E', + filename_wrong, + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + encoding='utf8', + cwd=os.path.dirname(filename), + ) + output, _ = process.communicate() + + assert output == '' + assert process.returncode == 0 + assert read_file(baseline_path) == baseline + + +def test_with_baseline_new_violations(make_file, read_file): + """End-to-End test to test that baseline still generates new violations.""" + filename = make_file(filename_wrong, wrong_template.format('x = 1')) + baseline_path = make_file(BASELINE_FILE, baseline) + + process = subprocess.Popen( + [ + 'flake8', + '--isolated', + '--baseline', + '--select', + 'WPS,E', + filename_wrong, + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + encoding='utf8', + cwd=os.path.dirname(filename), + ) + output, _ = process.communicate() + + _assert_output(output, {'WPS111': 1}) + assert process.returncode == 1 + assert read_file(baseline_path) == baseline + + +def test_with_baseline_new_correct_files(make_file, read_file): + """End-to-End test to test that baseline still generates new violations.""" + filename = make_file(filename_wrong, wrong_template.format('')) + make_file('correct.py', 'SOME_CONSTANT = 1') + baseline_path = make_file(BASELINE_FILE, baseline) + + process = subprocess.Popen( + [ + 'flake8', + '--isolated', + '--baseline', + '--select', + 'WPS,E', + filename_wrong, + 'correct.py', + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + encoding='utf8', + cwd=os.path.dirname(filename), + ) + output, _ = process.communicate() + + assert output == '' + assert process.returncode == 0 + assert read_file(baseline_path) == baseline + + +def test_with_baseline_new_wrong_files(make_file, read_file): + """End-to-End test to test that baseline still generates new violations.""" + filename = make_file(filename_wrong, wrong_template.format('')) + new_wrong = make_file('new_wrong.py', 'undescored_number = 10_0') + baseline_path = make_file(BASELINE_FILE, baseline) + + process = subprocess.Popen( + [ + 'flake8', + '--isolated', + '--baseline', + '--select', + 'WPS,E', + filename_wrong, + new_wrong, + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + encoding='utf8', + cwd=os.path.dirname(filename), + ) + output, _ = process.communicate() + + _assert_output(output, {'WPS303': 1}) + assert process.returncode == 1 + assert read_file(baseline_path) == baseline diff --git a/tests/test_patches/test_baseline/test_baseline_option/test_no_baseline.py b/tests/test_patches/test_baseline/test_baseline_option/test_no_baseline.py new file mode 100644 index 000000000..864254076 --- /dev/null +++ b/tests/test_patches/test_baseline/test_baseline_option/test_no_baseline.py @@ -0,0 +1,32 @@ +import os +import subprocess + +import pytest + +from wemake_python_styleguide.logic.baseline import BASELINE_FILE + + +def test_no_baseline_option(make_file, read_file): + """End-to-End test for no baseline, regular mode.""" + filename = make_file('wrong.py', 'x = 1') + cwd = os.path.dirname(filename) + + process = subprocess.Popen( + [ + 'flake8', + '--isolated', + '--select', + 'WPS,E', + filename, + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + encoding='utf8', + cwd=cwd, + ) + output, _ = process.communicate() + + assert process.returncode == 1 + with pytest.raises(IOError, match=BASELINE_FILE): + read_file(os.path.join(cwd, BASELINE_FILE)) diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 9aa54dd7c..35dfd5cc0 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -43,8 +43,6 @@ def test_external_plugins(absolute_path): 'flake8', '--disable-noqa', '--isolated', - '--enable-extensions', - 'G', filename, ], stdout=subprocess.PIPE, @@ -77,8 +75,6 @@ def test_external_plugins_diff(absolute_path): 'flake8', '--disable-noqa', '--isolated', - '--enable-extensions', - 'G', '--diff', # is required to test diffs! ;) '--exit-zero', # to allow failures ], diff --git a/tests/test_violations/test_docs.py b/tests/test_violations/test_docs.py index 96b958e70..ff0290a87 100644 --- a/tests/test_violations/test_docs.py +++ b/tests/test_violations/test_docs.py @@ -1,5 +1,9 @@ from wemake_python_styleguide.options.config import Configuration +_IGNORED_OPTIONS = frozenset(( + '--baseline', +)) + def test_all_violations_are_documented(all_module_violations): """Ensures that all violations are documented.""" @@ -43,6 +47,7 @@ def test_configuration(all_violations): option_listed = { option.long_option_name: False for option in Configuration._options # noqa: WPS437 + if option.long_option_name not in _IGNORED_OPTIONS } for violation in all_violations: diff --git a/tests/test_visitors/test_tokenize/test_comments/conftest.py b/tests/test_visitors/test_tokenize/test_comments/conftest.py deleted file mode 100644 index 0a90449c7..000000000 --- a/tests/test_visitors/test_tokenize/test_comments/conftest.py +++ /dev/null @@ -1,30 +0,0 @@ -# -*- coding: utf-8 -*- - -from os import chmod - -import pytest - -TEMP_FOLDER = 'tmp' -MODE_EXECUTABLE = 0o755 -MODE_NON_EXECUTABLE = 0o644 - - -@pytest.fixture() -def make_file(tmp_path): - """Fixture to make a temporary executable or non executable file.""" - def factory( - filename: str, - file_content: str, - is_executable: bool, - ) -> str: - temp_folder = tmp_path / TEMP_FOLDER - temp_folder.mkdir() - test_file = temp_folder / filename - file_mode = MODE_EXECUTABLE if is_executable else MODE_NON_EXECUTABLE - - test_file.write_text(file_content) - chmod(test_file.as_posix(), file_mode) - - return test_file.as_posix() - - return factory diff --git a/tests/test_visitors/test_tokenize/test_comments/test_shebang.py b/tests/test_visitors/test_tokenize/test_comments/test_shebang.py index 6335a3067..103790e53 100644 --- a/tests/test_visitors/test_tokenize/test_comments/test_shebang.py +++ b/tests/test_visitors/test_tokenize/test_comments/test_shebang.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import pytest from wemake_python_styleguide.violations.best_practices import ShebangViolation @@ -34,7 +32,11 @@ def test_correct_shebang_executable1( executable, ): """Testing cases when no errors should be reported.""" - path_to_file = make_file('test_file.py', template.format(code), executable) + path_to_file = make_file( + 'test_file.py', + template.format(code), + is_executable=executable, + ) file_tokens = parse_file_tokens(path_to_file) visitor = comments.ShebangVisitor( @@ -67,7 +69,11 @@ def test_correct_shebang_executable2( executable, ): """Testing cases when no errors should be reported.""" - path_to_file = make_file('test_file.py', template.format(code), executable) + path_to_file = make_file( + 'test_file.py', + template.format(code), + is_executable=executable, + ) file_tokens = parse_file_tokens(path_to_file) visitor = comments.ShebangVisitor( @@ -104,7 +110,11 @@ def test_shebang_on_windows( ): """Testing cases when no errors should be reported.""" monkeypatch.setattr(comments, 'is_windows', lambda: True) - path_to_file = make_file('test_file.py', template.format(code), executable) + path_to_file = make_file( + 'test_file.py', + template.format(code), + is_executable=executable, + ) file_tokens = parse_file_tokens(path_to_file) visitor = comments.ShebangVisitor( @@ -140,7 +150,11 @@ def test_shebang_with_stdin( executable, ): """Testing cases when no errors should be reported.""" - path_to_file = make_file('test_file.py', template.format(code), executable) + path_to_file = make_file( + 'test_file.py', + template.format(code), + is_executable=executable, + ) file_tokens = parse_file_tokens(path_to_file) visitor = comments.ShebangVisitor( @@ -171,7 +185,11 @@ def test_wrong_shebang_executable( executable, ): """Testing cases when no errors should be reported.""" - path_to_file = make_file('test_file.py', template.format(code), executable) + path_to_file = make_file( + 'test_file.py', + template.format(code), + is_executable=executable, + ) file_tokens = parse_file_tokens(path_to_file) visitor = comments.ShebangVisitor( @@ -202,7 +220,9 @@ def test_wrong_shebang_format( ): """Testing cases when no errors should be reported.""" path_to_file = make_file( - 'test_file.py', template.format(code), is_executable=True, + 'test_file.py', + template.format(code), + is_executable=True, ) file_tokens = parse_file_tokens(path_to_file) diff --git a/wemake_python_styleguide/checker.py b/wemake_python_styleguide/checker.py index 13d81b860..52a24f5d8 100644 --- a/wemake_python_styleguide/checker.py +++ b/wemake_python_styleguide/checker.py @@ -48,6 +48,7 @@ from wemake_python_styleguide import version as pkg_version from wemake_python_styleguide.options.config import Configuration from wemake_python_styleguide.options.validation import validate_options +from wemake_python_styleguide.patches import baseline from wemake_python_styleguide.presets.types import file_tokens as tokens_preset from wemake_python_styleguide.presets.types import filename as filename_preset from wemake_python_styleguide.presets.types import tree as tree_preset @@ -135,6 +136,8 @@ def add_options(cls, parser: OptionManager) -> None: def parse_options(cls, options: types.ConfigurationOptions) -> None: """Parses registered options for providing them to each visitor.""" cls.options = validate_options(options) + if cls.options.baseline: + baseline.apply_patch() def run(self) -> Iterator[types.CheckResult]: """ diff --git a/wemake_python_styleguide/compat/packaging.py b/wemake_python_styleguide/compat/packaging.py index 48c7a1fa6..26e8001b7 100644 --- a/wemake_python_styleguide/compat/packaging.py +++ b/wemake_python_styleguide/compat/packaging.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import sys # Note that we use ``sys.version_info`` directly, diff --git a/wemake_python_styleguide/logic/baseline.py b/wemake_python_styleguide/logic/baseline.py new file mode 100644 index 000000000..c440bd273 --- /dev/null +++ b/wemake_python_styleguide/logic/baseline.py @@ -0,0 +1,120 @@ +import json +import os +from collections import Counter, defaultdict +from hashlib import md5 +from typing import DefaultDict, Dict, Iterable, List, Mapping, Optional, Tuple + +import attr +from typing_extensions import Final, final + +#: That's a constant filename where we store our baselines. +BASELINE_FILE: Final = '.flake8-baseline.json' + +#: Content is: `error_code, line_number, column, text, physical_line`. +CheckReport = Tuple[str, int, int, str, str] + +#: Mapping of filename and the report result. +SavedReports = Dict[str, List[CheckReport]] + + +def _baseline_fullpath() -> str: + """We only store baselines in the current (main) directory.""" + return os.path.join(os.curdir, BASELINE_FILE) + + +def _unique_paths_converter( + mapping: Mapping[str, Iterable[str]], +) -> Mapping[str, Dict[str, int]]: + return { + path: Counter(violations) + for path, violations in mapping.items() + } + + +@final +@attr.dataclass(slots=True, frozen=True) +class _BaselineFile(object): + """ + Baseline file representation. + + How paths are stored? + We use ``path`` -> ``violations`` mapping, here ``violations`` is + a mutable dict of ``digest`` and ``count``. + + We mutate ``count`` to mark violation as found. + Once there are no more violations to find in the baseline, + we start to report them! + + """ + + paths: Mapping[str, Dict[str, int]] = attr.ib( + converter=_unique_paths_converter, + ) + + def has(self, filename: str, error_code: str, text: str) -> bool: + """ + Tells whether or not this violation is saved in the baseline. + + This operation is impure. Because we mutate the object's state. + After we find a violation once, it's counter is decreased. + That's how we controll violations' count inside a single file. + """ + if filename not in self.paths: + return False + + per_file = self.paths[filename] + digest = self._generate_violation_hash(error_code, text) + + per_file[digest] = per_file[digest] - 1 + return per_file[digest] >= 0 + + @classmethod + def from_report( + cls, saved_reports: SavedReports, + ) -> '_BaselineFile': + """Factory method to construct baselines from ``flake8`` like stats.""" + paths: DefaultDict[str, List[str]] = defaultdict(list) + + for filename, reports in saved_reports.items(): + for report in reports: + paths[filename].append( + cls._generate_violation_hash(report[0], report[3]), + ) + return cls(paths) + + @classmethod + def _generate_violation_hash(cls, error_code: str, message: str) -> str: + digest = md5() # noqa: S303 + digest.update(error_code.encode()) + digest.update(message.encode()) + return digest.hexdigest() + + +def load_from_file() -> Optional[_BaselineFile]: + """ + Loads baseline ``json`` files from current workdir. + + It might return ``None`` when file does not exist. + It means, that we run ``--baseline`` for the very first time. + """ + try: + with open(_baseline_fullpath()) as baseline_file: + return _BaselineFile(**json.load(baseline_file)) + except IOError: + # There was probably no baseline file, that's ok. + # We will create a new one later. + return None + + +def save_to_file(saved_reports: SavedReports) -> _BaselineFile: + """Creates new baseline ``json`` files in current workdir.""" + baseline = _BaselineFile.from_report(saved_reports) + with open(_baseline_fullpath(), 'w') as baseline_file: + json.dump( + attr.asdict(baseline), + baseline_file, + sort_keys=True, + indent=2, + ) + + return baseline diff --git a/wemake_python_styleguide/logic/system.py b/wemake_python_styleguide/logic/system.py index 9370baf83..b78d23253 100644 --- a/wemake_python_styleguide/logic/system.py +++ b/wemake_python_styleguide/logic/system.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import os from wemake_python_styleguide.constants import WINDOWS_OS diff --git a/wemake_python_styleguide/options/config.py b/wemake_python_styleguide/options/config.py index c91521a51..96215ae88 100644 --- a/wemake_python_styleguide/options/config.py +++ b/wemake_python_styleguide/options/config.py @@ -56,6 +56,8 @@ :str:`wemake_python_styleguide.options.defaults.ALLOWED_DOMAIN_NAMES` - ``forbidden-domain-names`` - list of forbidden domain names, defaults to :str:`wemake_python_styleguide.options.defaults.FORBIDDEN_DOMAIN_NAMES` +- ``baseline`` - run linter in legacy-first mode and ignore current violations, + defaults to :str:`wemake_python_styleguide.options.defaults.BASELINE` .. rubric:: Complexity options @@ -230,6 +232,7 @@ class Configuration(object): type='string', comma_separated_list=True, ), + _Option( '--allowed-domain-names', defaults.ALLOWED_DOMAIN_NAMES, @@ -237,6 +240,7 @@ class Configuration(object): type='string', comma_separated_list=True, ), + _Option( '--forbidden-domain-names', defaults.FORBIDDEN_DOMAIN_NAMES, @@ -245,6 +249,15 @@ class Configuration(object): comma_separated_list=True, ), + _Option( + '--baseline', + defaults.BASELINE, + 'Run linter in legacy-first mode and ignore current violations.', + action='store_true', + type=None, + dest='baseline', + ), + # Complexity: _Option( diff --git a/wemake_python_styleguide/options/defaults.py b/wemake_python_styleguide/options/defaults.py index 12529c33f..2fe41b7b8 100644 --- a/wemake_python_styleguide/options/defaults.py +++ b/wemake_python_styleguide/options/defaults.py @@ -42,6 +42,9 @@ #: Domain names that extends variable names' blacklist. FORBIDDEN_DOMAIN_NAMES: Final = () +#: Baseline mode trigger, turn it on to integrate this linter into legacy. +BASELINE: Final = False + # =========== # Complexity: diff --git a/wemake_python_styleguide/options/validation.py b/wemake_python_styleguide/options/validation.py index 86a7cc8be..594e3ca3c 100644 --- a/wemake_python_styleguide/options/validation.py +++ b/wemake_python_styleguide/options/validation.py @@ -65,6 +65,7 @@ class _ValidatedOptions(object): nested_classes_whitelist: Tuple[str, ...] = attr.ib(converter=tuple) allowed_domain_names: Tuple[str, ...] = attr.ib(converter=tuple) forbidden_domain_names: Tuple[str, ...] = attr.ib(converter=tuple) + baseline: bool # Complexity: max_arguments: int = attr.ib(validator=[_min_max(min=1)]) diff --git a/wemake_python_styleguide/patches/__init__.py b/wemake_python_styleguide/patches/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/wemake_python_styleguide/patches/baseline.py b/wemake_python_styleguide/patches/baseline.py new file mode 100644 index 000000000..290ca9508 --- /dev/null +++ b/wemake_python_styleguide/patches/baseline.py @@ -0,0 +1,71 @@ +from collections import defaultdict +from typing import Iterable, Tuple, Type + +from flake8.checker import Manager + +from wemake_python_styleguide.logic import baseline + + +def _patch_report(manager: Type[Manager]) -> None: + original_report = manager.report + + def report(self) -> Tuple[int, int]: # noqa: WPS430 + # --- patch start + saved_reports: baseline.SavedReports = defaultdict(list) + self.saved_reports = saved_reports # mypy requires that! + # --- patch end + + report_result = original_report(self) + + # --- patch start + if self.baseline is None: + self.baseline = baseline.save_to_file(self.saved_reports) + # --- patch end + + return report_result + + manager.report = report + manager.baseline = baseline.load_from_file() + + +def _patch_handle_results( # noqa: WPS210, WPS231 + manager: Type[Manager], +) -> None: + def _handle_results( # noqa: WPS210, WPS430 + self, + filename: str, + results: Iterable[baseline.CheckReport], # noqa: WPS110 + ) -> int: + style_guide = self.style_guide + reported_results_count = 0 + for (error_code, line_number, column, text, physical_line) in results: + # --- patch start + # Here we ignore violations present in the baseline. + if self.baseline and self.baseline.has(filename, error_code, text): + continue + + handled_error = style_guide.handle_error( + code=error_code, + filename=filename, + line_number=line_number, + column_number=column, + text=text, + physical_line=physical_line, + ) + + reported_results_count += handled_error + + if handled_error: + self.saved_reports[filename].append( + (error_code, line_number, column, text, physical_line), + ) + # --- patch end + return reported_results_count + + manager._handle_results = _handle_results # noqa: WPS437 + + +def apply_patch() -> None: + """This is the only function we export to apply all the patches.""" + _patch_report(Manager) + _patch_handle_results(Manager) diff --git a/wemake_python_styleguide/types.py b/wemake_python_styleguide/types.py index 77eec8591..ef79aa64d 100644 --- a/wemake_python_styleguide/types.py +++ b/wemake_python_styleguide/types.py @@ -144,6 +144,10 @@ def allowed_domain_names(self) -> Tuple[str, ...]: def forbidden_domain_names(self) -> Tuple[str, ...]: ... + @property + def baseline(self) -> bool: + ... + # Complexity: @property def max_arguments(self) -> int: diff --git a/wemake_python_styleguide/violations/complexity.py b/wemake_python_styleguide/violations/complexity.py index b632d72ba..0132735a4 100644 --- a/wemake_python_styleguide/violations/complexity.py +++ b/wemake_python_styleguide/violations/complexity.py @@ -329,7 +329,7 @@ def first_function(param): def second_function(argument): second_var = 1 argument = int(argument) - third_var, _ = some_call() + third_var, _unused = some_call() In this example we will count as locals only several variables: @@ -338,8 +338,9 @@ def second_function(argument): 3. ``argument``, because it is reassigned inside the function's body 4. ``third_var``, because it is assigned inside the function's body - Please, note that ``_`` is a special case. It is not counted as a local - variable. Since by design it means: do not count me as a real variable. + Please, note that ``_unused`` is a special case. + It is not counted as a local variable. + Since by design it means: do not count me as a real variable. Configuration: This rule is configurable with ``--max-local-variables``.