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``.