From 79bfbcd5a1249c5a98e939f4714ea84058eae1df Mon Sep 17 00:00:00 2001 From: sobolevn Date: Thu, 20 Sep 2018 21:40:25 +0300 Subject: [PATCH 1/7] WIP: version 0.0.14 --- CHANGELOG.md | 21 +- pyproject.lock | 116 ++++++++++- pyproject.toml | 10 +- setup.cfg | 2 + tests/fixtures/config/wrong_arguments.py | 1 - tests/fixtures/noqa.py | 3 + tests/test_visitors/conftest.py | 2 +- .../test_counts/test_imports_counts.py | 53 +++++ .../test_counts/test_module_counts.py | 8 +- .../test_wrong_function_calls.py | 2 +- .../test_dotted_raw_import.py | 2 +- .../test_wrong_import/test_future_imports.py | 2 +- .../test_wrong_import/test_nested_imports.py | 2 +- .../test_relative_imports.py | 2 +- .../test_same_alias_import.py | 2 +- .../test_wrong_keyword/test_del.py | 2 +- .../test_wrong_keyword/test_global.py | 2 +- .../test_wrong_keyword/test_pass.py | 2 +- .../test_raise_notimplemented.py | 2 +- .../test_wrong_name/test_class_attributes.py | 2 +- .../test_function_args_names.py | 2 +- .../test_wrong_name/test_function_names.py | 2 +- .../test_wrong_name/test_import_alias.py | 2 +- .../test_wrong_name/test_module_metadata.py | 2 +- .../test_wrong_name/test_variable_names.py | 2 +- .../{ => test_general}/test_wrong_string.py | 2 +- .../test_empty_init.py | 2 +- .../test_empty_modules.py | 2 +- tests/test_visitors/test_base.py | 11 +- wemake_python_styleguide/checker.py | 124 ++++++----- wemake_python_styleguide/errors/complexity.py | 64 +++++- wemake_python_styleguide/logics/limits.py | 21 -- wemake_python_styleguide/options/config.py | 193 ++++++++++-------- wemake_python_styleguide/options/defaults.py | 16 +- wemake_python_styleguide/types.py | 21 +- .../visitors/ast/complexity/counts.py | 49 ++++- .../visitors/ast/complexity/function.py | 121 ++++++----- .../visitors/ast/complexity/jones.py | 2 +- .../visitors/ast/general/__init__.py | 1 + .../ast/{ => general}/wrong_function_call.py | 0 .../ast/{ => general}/wrong_import.py | 0 .../ast/{ => general}/wrong_keyword.py | 0 .../visitors/ast/{ => general}/wrong_name.py | 0 .../ast/{ => general}/wrong_string.py | 0 .../{wrong_contents.py => wrong_module.py} | 0 wemake_python_styleguide/visitors/base.py | 70 ++++--- 46 files changed, 626 insertions(+), 321 deletions(-) create mode 100644 tests/test_visitors/test_ast/test_complexity/test_counts/test_imports_counts.py rename tests/test_visitors/test_ast/{ => test_general}/test_wrong_function_call/test_wrong_function_calls.py (95%) rename tests/test_visitors/test_ast/{ => test_general}/test_wrong_import/test_dotted_raw_import.py (96%) rename tests/test_visitors/test_ast/{ => test_general}/test_wrong_import/test_future_imports.py (95%) rename tests/test_visitors/test_ast/{ => test_general}/test_wrong_import/test_nested_imports.py (95%) rename tests/test_visitors/test_ast/{ => test_general}/test_wrong_import/test_relative_imports.py (94%) rename tests/test_visitors/test_ast/{ => test_general}/test_wrong_import/test_same_alias_import.py (93%) rename tests/test_visitors/test_ast/{ => test_general}/test_wrong_keyword/test_del.py (87%) rename tests/test_visitors/test_ast/{ => test_general}/test_wrong_keyword/test_global.py (90%) rename tests/test_visitors/test_ast/{ => test_general}/test_wrong_keyword/test_pass.py (91%) rename tests/test_visitors/test_ast/{ => test_general}/test_wrong_keyword/test_raise_notimplemented.py (95%) rename tests/test_visitors/test_ast/{ => test_general}/test_wrong_name/test_class_attributes.py (97%) rename tests/test_visitors/test_ast/{ => test_general}/test_wrong_name/test_function_args_names.py (97%) rename tests/test_visitors/test_ast/{ => test_general}/test_wrong_name/test_function_names.py (96%) rename tests/test_visitors/test_ast/{ => test_general}/test_wrong_name/test_import_alias.py (96%) rename tests/test_visitors/test_ast/{ => test_general}/test_wrong_name/test_module_metadata.py (95%) rename tests/test_visitors/test_ast/{ => test_general}/test_wrong_name/test_variable_names.py (97%) rename tests/test_visitors/test_ast/{ => test_general}/test_wrong_string.py (94%) rename tests/test_visitors/test_ast/{test_wrong_contents => test_wrong_module}/test_empty_init.py (95%) rename tests/test_visitors/test_ast/{test_wrong_contents => test_wrong_module}/test_empty_modules.py (89%) delete mode 100644 wemake_python_styleguide/logics/limits.py create mode 100644 wemake_python_styleguide/visitors/ast/general/__init__.py rename wemake_python_styleguide/visitors/ast/{ => general}/wrong_function_call.py (100%) rename wemake_python_styleguide/visitors/ast/{ => general}/wrong_import.py (100%) rename wemake_python_styleguide/visitors/ast/{ => general}/wrong_keyword.py (100%) rename wemake_python_styleguide/visitors/ast/{ => general}/wrong_name.py (100%) rename wemake_python_styleguide/visitors/ast/{ => general}/wrong_string.py (100%) rename wemake_python_styleguide/visitors/ast/{wrong_contents.py => wrong_module.py} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ad20e3b1..4f7249da7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,25 @@ We follow Semantic Versions since the `0.1.0` release. We used to have incremental versioning before `0.1.0`. -## Version 0.0.13 aka The Jones Complexity +## Version 0.0.14 + +### Features + +- Adds `TooManyImportsViolation` +- Adds `--max-imports` options +- Adds `flake8`, `noqa`, and `type` comments check + -This release is the last feature release before `0.1.0`. -However, there might be some supporting releases. +### Misc + +- Changes how many errors are generated for limits violations +- Improves typing support +- Adds `cohesion` plugin +- Adds `flake8-print` plugin +- Removes `delegate` concept + + +## Version 0.0.13 aka The Jones Complexity ### Features diff --git a/pyproject.lock b/pyproject.lock index 1e0356089..88f0e4c32 100644 --- a/pyproject.lock +++ b/pyproject.lock @@ -19,6 +19,15 @@ platform = "*" python-versions = "*" version = "0.7.11" +[[package]] +category = "dev" +description = "apipkg: namespace control and lazy-import mechanism" +name = "apipkg" +optional = false +platform = "unix" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "1.5" + [[package]] category = "dev" description = "Atomic file writes." @@ -67,6 +76,15 @@ platform = "*" python-versions = "*" version = "3.0.4" +[[package]] +category = "main" +description = "A tool for measuring Python class cohesion." +name = "cohesion" +optional = false +platform = "*" +python-versions = "*" +version = "0.8.0" + [[package]] category = "dev" description = "Cross-platform colored terminal text." @@ -113,6 +131,27 @@ platform = "OS-independent" python-versions = "*" version = "0.14" +[[package]] +category = "dev" +description = "Removes commented-out code." +name = "eradicate" +optional = false +platform = "*" +python-versions = "*" +version = "0.2.1" + +[[package]] +category = "dev" +description = "execnet: rapid multi-Python deployment" +name = "execnet" +optional = false +platform = "unix" +python-versions = "*" +version = "1.5.0" + +[package.dependencies] +apipkg = ">=1.4" + [[package]] category = "main" description = "the modular source code checker: pep8, pyflakes and co" @@ -262,6 +301,20 @@ version = "1.0.2" [package.dependencies] flake8 = "*" +[[package]] +category = "dev" +description = "print statement checker plugin for flake8" +name = "flake8-print" +optional = false +platform = "*" +python-versions = "*" +version = "3.1.0" + +[package.dependencies] +flake8 = ">=1.5" +pycodestyle = "*" +six = "*" + [[package]] category = "dev" description = "pytest assert checker plugin for flake8" @@ -408,11 +461,21 @@ name = "mypy" optional = false platform = "*" python-versions = "*" -version = "0.610" +version = "0.630" [package.dependencies] +mypy-extensions = ">=0.4.0,<0.5.0" typed-ast = ">=1.1.0,<1.2.0" +[[package]] +category = "dev" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +name = "mypy-extensions" +optional = false +platform = "*" +python-versions = "*" +version = "0.4.1" + [[package]] category = "dev" description = "Core utilities for Python packages" @@ -463,7 +526,7 @@ name = "pockets" optional = false platform = "any" python-versions = "*" -version = "0.6.2" +version = "0.6.4" [package.dependencies] six = ">=1.5.2" @@ -522,9 +585,9 @@ category = "dev" description = "Python parsing module" name = "pyparsing" optional = false -platform = "UNKNOWN" -python-versions = "*" -version = "2.2.0" +platform = "*" +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +version = "2.2.1" [[package]] category = "dev" @@ -548,6 +611,19 @@ six = ">=1.10.0" platform = "win32" version = "*" +[[package]] +category = "dev" +description = "pytest plugin with mechanisms for caching across test runs" +name = "pytest-cache" +optional = false +platform = "UNKNOWN" +python-versions = "*" +version = "1.0" + +[package.dependencies] +execnet = ">=1.1.dev1" +pytest = ">=2.2" + [[package]] category = "dev" description = "Pytest plugin for measuring coverage." @@ -561,6 +637,20 @@ version = "2.6.0" coverage = ">=4.4" pytest = ">=2.9" +[[package]] +category = "dev" +description = "pytest plugin to check for commented out code" +name = "pytest-eradicate" +optional = false +platform = "UNKNOWN" +python-versions = "*" +version = "0.0.3" + +[package.dependencies] +eradicate = "*" +pytest = ">=2.4.2" +pytest-cache = "*" + [[package]] category = "dev" description = "pytest plugin to check FLAKE8 requirements" @@ -785,22 +875,26 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" version = "1.23" [metadata] -content-hash = "e00b93239a7e2149bee5a8ed276c3f7972c68bb16426bbb9d25f3e613a89cf5d" +content-hash = "5e27d7f4144be420812ef86196796d6d99f3c2f9512ea90936e7685dd2a94aeb" platform = "*" python-versions = "^3.6 || ^3.7" [metadata.hashes] added-value = ["84f1dced8e72941718545e39ce141bafa95e7fda816a3b92113c45d27245acc6", "c10a1ca36daf0bddc7f6d5d52edc793b95dd5596f3443defc6dd52d26aa6d3d8"] alabaster = ["674bb3bab080f598371f4443c5008cbfeb1a5e622dd312395d2d82af2c54c456", "b63b1f4dc77c074d386752ec4a8a7517600f6c0db8cd42980cae17ab7b3275d7"] +apipkg = ["37228cda29411948b422fae072f57e31d3396d2ee1c9783775980ee9c9990af6", "58587dd4dc3daefad0487f6d9ae32b4542b185e1c36db6993290e7c41ca2b47c"] atomicwrites = ["0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0", "ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"] attrs = ["10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", "ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"] babel = ["6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669", "8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23"] certifi = ["376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638", "456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"] chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] +cohesion = ["ea3d5fb098c94281bc42727007fdf9e67b462e1cadaaa365b394f0a6723e7b32"] colorama = ["463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda", "48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"] coverage = ["03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba", "0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed", "104ab3934abaf5be871a583541e8829d6c19ce7bde2923b2751e0d3ca44db60a", "15b111b6a0f46ee1a485414a52a7ad1d703bdf984e9ed3c288a4414d3871dcbd", "198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640", "1c383d2ef13ade2acc636556fd544dba6e14fa30755f26812f54300e401f98f2", "28b2191e7283f4f3568962e373b47ef7f0392993bb6660d079c62bd50fe9d162", "2eb564bbf7816a9d68dd3369a510be3327f1c618d2357fa6b1216994c2e3d508", "337ded681dd2ef9ca04ef5d93cfc87e52e09db2594c296b4a0a3662cb1b41249", "3a2184c6d797a125dca8367878d3b9a178b6fdd05fdc2d35d758c3006a1cd694", "3c79a6f7b95751cdebcd9037e4d06f8d5a9b60e4ed0cd231342aa8ad7124882a", "3d72c20bd105022d29b14a7d628462ebdc61de2f303322c0212a054352f3b287", "3eb42bf89a6be7deb64116dd1cc4b08171734d721e7a7e57ad64cc4ef29ed2f1", "4635a184d0bbe537aa185a34193898eee409332a8ccb27eea36f262566585000", "56e448f051a201c5ebbaa86a5efd0ca90d327204d8b059ab25ad0f35fbfd79f1", "5a13ea7911ff5e1796b6d5e4fbbf6952381a611209b736d48e675c2756f3f74e", "69bf008a06b76619d3c3f3b1983f5145c75a305a0fea513aca094cae5c40a8f5", "6bc583dc18d5979dc0f6cec26a8603129de0304d5ae1f17e57a12834e7235062", "701cd6093d63e6b8ad7009d8a92425428bc4d6e7ab8d75efbb665c806c1d79ba", "7608a3dd5d73cb06c531b8925e0ef8d3de31fed2544a7de6c63960a1e73ea4bc", "76ecd006d1d8f739430ec50cc872889af1f9c1b6b8f48e29941814b09b0fd3cc", "7aa36d2b844a3e4a4b356708d79fd2c260281a7390d678a10b91ca595ddc9e99", "7d3f553904b0c5c016d1dad058a7554c7ac4c91a789fca496e7d8347ad040653", "7e1fe19bd6dce69d9fd159d8e4a80a8f52101380d5d3a4d374b6d3eae0e5de9c", "8c3cb8c35ec4d9506979b4cf90ee9918bc2e49f84189d9bf5c36c0c1119c6558", "9d6dd10d49e01571bf6e147d3b505141ffc093a06756c60b053a859cb2128b1f", "9e112fcbe0148a6fa4f0a02e8d58e94470fc6cb82a5481618fea901699bf34c4", "ac4fef68da01116a5c117eba4dd46f2e06847a497de5ed1d64bb99a5fda1ef91", "b8815995e050764c8610dbc82641807d196927c3dbed207f0a079833ffcf588d", "be6cfcd8053d13f5f5eeb284aa8a814220c3da1b0078fa859011c7fffd86dab9", "c1bb572fab8208c400adaf06a8133ac0712179a334c09224fb11393e920abcdd", "de4418dadaa1c01d497e539210cb6baa015965526ff5afc078c57ca69160108d", "e05cb4d9aad6233d67e0541caa7e511fa4047ed7750ec2510d466e806e0255d6", "e4d96c07229f58cb686120f168276e434660e4358cc9cf3b0464210b04913e77", "f3f501f345f24383c0000395b26b726e46758b71393267aeae0bd36f8b3ade80", "f8a923a85cb099422ad5a2e345fe877bbc89a8a8b23235824a93488150e45f6e"] doc8 = ["2df89f9c1a5abfb98ab55d0175fed633cae0cf45025b8b1e0ee5ea772be28543", "d12f08aa77a4a65eb28752f4bc78f41f611f9412c4155e2b03f1f5d4a45efe04"] docutils = ["02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", "51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", "7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"] +eradicate = ["f9af01c544ccd8f71bc2f7f3fa39dc363d842cfcb9c730a83676a59026ab5f24"] +execnet = ["a7a84d5fa07a089186a329528f127c9d73b9de57f1a1131b82bb5320ee651f6a", "fc155a6b553c66c838d1a22dba1dc9f5f505c43285a878c6f74a79c024750b83"] flake8 = ["7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0", "c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37"] flake8-bugbear = ["07b6e769d7f4e168d590f7088eae40f6ddd9fa4952bed31602def65842682c83", "0ccf56975f4db1d69dc1cf3598c99d768ebf95d0cad27d76087954aa399b515a"] flake8-builtins = ["8d806360767947c0035feada4ddef3ede32f0a586ef457e62d811b8456ad9a51", "cd7b1b7fec4905386a3643b59f9ca8e305768da14a49a7efb31fe9364f33cd04"] @@ -813,6 +907,7 @@ flake8-isort = ["298d7904ac3a46274edf4ce66fd7e272c2a60c34c3cc999dea000608d64e5e6 flake8-module-name = ["bc0a43cce6fc95215de39a0f18e06fdca160daaf63eae6926fabbb6d9458f3d2", "d155957f08c6dabd44d59ca229ca67375a34b14ee44c79097b66838dd919e5b6"] flake8-pep3101 = ["493821d6bdd083794eb0691ebe5b68e5c520b622b269d60e54308fb97440e21a", "b661ab718df42b87743dde266ef5de4f9e900b56c67dbccd45d24cf527545553"] flake8-polyfill = ["12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9", "e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"] +flake8-print = ["5010e6c138b63b62400da4b06afa33becc5e08bd1fcce9af3752445cf3342f54"] flake8-pytest = ["61686128a79e1513db575b2bcac351081d5a293811ddce2d5dfc25e8c762d33e", "b4d6703f7d7b646af1e2660809e795886dd349df11843613dbe6515efa82c0f3"] flake8-quotes = ["fd9127ad8bbcf3b546fa7871a5266fd8623ce765ebe3d5aa5eabb80c01212b26"] flake8-string-format = ["68ea72a1a5b75e7018cae44d14f32473c798cf73d75cbaed86c6a9a907b770b2", "774d56103d9242ed968897455ef49b7d6de272000cfa83de5814273a868832f1"] @@ -826,20 +921,23 @@ markupsafe = ["a6be69091dac236ea9c6bc7d012beab42010fa914c459791d627dad4910eb665" mccabe = ["ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", "dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"] mistune = ["b4c512ce2fc99e5a62eb95a4aba4b73e5f90264115c40b70a21e1f7d4e0eac91", "bc10c33bfdcaa4e749b779f62f60d6e12f8215c46a292d05e486b869ae306619"] more-itertools = ["c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092", "c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e", "fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d"] -mypy = ["1b899802a89b67bb68f30d788bba49b61b1f28779436f06b75c03495f9d6ea5c", "f472645347430282d62d1f97d12ccb8741f19f1572b7cf30b58280e4e0818739"] +mypy = ["00b95bfdc0d5b9aa53c906e56fb91937743f2121d66684db5f947ec5d75f565d", "6704586b4c2bf7dfa5e87a422be9ca57db622bab65008245759f3d4baeb219dd"] +mypy-extensions = ["37e0e956f41369209a3d5f34580150bcacfabaa57b33a15c0b25f4b5725e0812", "b16cabe759f55e3409a7d231ebd2841378fb0c27a5d1994719e340e4f429ac3e"] packaging = ["e9215d2d2535d3ae866c3d6efc77d5b24a0192cce0ff20e42896cc0664f889c0", "f019b770dd64e585a99714f1fd5e01c7a8f11b45635aa953fd41c689a657375b"] pbr = ["1b8be50d938c9bb75d0eaf7eda111eec1bf6dc88a62a6412e33bf077457e0f45", "b486975c0cafb6beeb50ca0e17ba047647f229087bd74e37f4a7e2cac17d2caa"] pep8-naming = ["360308d2c5d2fff8031c1b284820fbdb27a63274c0c1a8ce884d631836da4bdd", "624258e0dd06ef32a9daf3c36cc925ff7314da7233209c5b01f7e5cdd3c34826"] pluggy = ["6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1", "95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1"] -pockets = ["2f0828f9373a4beeb12a93ce4fad0cb5665a61b6506a836a89b5adcd5108680a", "40ac0936cde62b0e3ba02946ad4407648c95e4e3edae3659b21f6c9f7a2c9463"] +pockets = ["63dcd88203504d06f7393e9b505bc45888657cdf70c0ed6ed8dbb44fa06d1aaf", "7b2e0486cd54a187446300e485cb182b2995d07175de5aaad866d11aad9291c9"] py = ["06a30435d058473046be836d3fc4f27167fd84c45b99704f2fb5509ef61f9af1", "50402e9d1c9005d759426988a492e0edaadb7f4e68bcddfea586bc7432d009c6"] pycodestyle = ["682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766", "6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9"] pydocstyle = ["08a870edc94508264ed90510db466c6357c7192e0e866561d740624a8fc7d90c", "4d5bcde961107873bae621f3d580c3e35a426d3687ffc6f8fb356f6628da5a97", "af9fcccb303899b83bec82dc9a1d56c60fc369973223a5e80c3dfa9bdf984405"] pyflakes = ["08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f", "8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805"] pygments = ["78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d", "dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc"] -pyparsing = ["0832bcf47acd283788593e7a0f542407bd9550a55a8a8435214a1960e04bcb04", "281683241b25fe9b80ec9d66017485f6deff1af5cde372469134b56ca8447a07", "8f1e18d3fd36c6795bb7e02a39fd05c611ffc2596c1e0d995d34d67630426c18", "9e8143a3e15c13713506886badd96ca4b579a87fbdf49e550dbfc057d6cb218e", "b8b3117ed9bdf45e14dcc89345ce638ec7e0e29b2b579fa1ecf32ce45ebac8a5", "e4d45427c6e20a59bf4f88c639dcc03ce30d193112047f94012102f235853a58", "fee43f17a9c4087e7ed1605bd6df994c6173c1e977d7ade7b651292fab2bd010"] +pyparsing = ["905d8090c335314568b5faee0025b1829f27bb974604a5762a6cdef3a7dfc3b7", "f493ee323be1e94929416b3585eefcc04943115cecbaaa35a8c86d1a2368af19"] pytest = ["453cbbbe5ce6db38717d282b758b917de84802af4288910c12442984bde7b823", "a8a07f84e680482eb51e244370aaf2caa6301ef265f37c2bdefb3dd3b663f99d"] +pytest-cache = ["be7468edd4d3d83f1e844959fd6e3fd28e77a481440a7118d430130ea31b07a9"] pytest-cov = ["513c425e931a0344944f84ea47f3956be0e416d95acbd897a44970c8d926d5d7", "e360f048b7dae3f2f2a9a4d067b2dd6b6a015d384d1577c994a43f3f7cbad762"] +pytest-eradicate = ["1dcb3fa109e697817c2421e3767e2c3d031c4d36a2180034c119b9bd30fe17a2", "7ac2e48ef1dce414a3e93b30fc7ca56ff1b00df115b3c80a7dd83238f962d551"] pytest-flake8 = ["4f30f5be3efb89755f38f11bdb2a5e22d19a6f5faa73428f703a3292a9572cd3", "c740ad6aa19e3958947d2118f70bed218caf1d2097039fb7318573a2a72f89a1"] pytest-isort = ["2221c0914dfca41914625a646f0d2d1d4c676861b9a7b26746a7fdd40aa2c59b", "c70d0f900f4647bb714f0843dd82d7f7b759904006de31254efdb72ce88e0c0e"] pytest-randomly = ["6db5e03d72b54052b9b379dc3cfa4749c19bfe4de161cf3eb24762049f4ce9be", "92ec6745d3ebdd690ecb598648748c9601f16f5afacf83ccef2b50d23e6edb7f"] diff --git a/pyproject.toml b/pyproject.toml index b32d435d5..7041df29c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wemake-python-styleguide" -version = "0.0.13" +version = "0.0.14" description = "The most opinionated linter ever, used by wemake.services" license = "MIT" @@ -20,6 +20,7 @@ keywords = [ "linting", "wemake.services", "styleguide", + "code quality" ] classifiers = [ @@ -56,14 +57,18 @@ flake8-super-call = "^1.0" flake8-debugger = "^3.1" flake8-isort = "^2.5" pep8-naming = "^0.7" +cohesion = "^0.8" [tool.poetry.dev-dependencies] pytest-cov = "^2.6" pytest-flake8 = "^1.0" pytest-randomly = "^1.2" +pytest-isort = "^0.2" +pytest-eradicate = "^0.0.3" pytest = "^3.8" flake8-pytest = "^1.3" -mypy = "^0.610.0" +flake8-print = "^3.1" +mypy = "^0.630" sphinx = "^1.8" sphinx-autodoc-typehints = "^1.3" sphinxcontrib-napoleon = "^0.6" @@ -72,4 +77,3 @@ m2r = "^0.2" sphinx_readable_theme = "^1.3" typing_extensions = "^3.6" added-value = "^0.8.0" -pytest-isort = "^0.2.1" diff --git a/setup.cfg b/setup.cfg index 50904e550..8b7d651e0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,6 +27,8 @@ flake8-ignore = wemake_python_styleguide/visitors/ast/*.py N802 # These modules should contain a lot of classes: wemake_python_styleguide/errors/*.py Z208 + # We can not find an easier way to define a checker for now: + wemake_python_styleguide/checker.py Z212 # Disable filename checks for regression testing: tests/test_checkers/test_regression/*.py N999 # Disable some pydocstyle checks: diff --git a/tests/fixtures/config/wrong_arguments.py b/tests/fixtures/config/wrong_arguments.py index 04bad9b92..9b0a3d2c0 100644 --- a/tests/fixtures/config/wrong_arguments.py +++ b/tests/fixtures/config/wrong_arguments.py @@ -34,4 +34,3 @@ def static_normal_method(one, two, *three, four=4, five=5): @staticmethod def static_error_method(one, two, three, four, five, six): pass - diff --git a/tests/fixtures/noqa.py b/tests/fixtures/noqa.py index f6dbf0163..d63a4f79c 100644 --- a/tests/fixtures/noqa.py +++ b/tests/fixtures/noqa.py @@ -5,7 +5,10 @@ """ from __future__ import print_function # noqa: Z102 + from .version import get_version # noqa: Z100 + + def some(): from my_module import some_function # noqa: Z101 diff --git a/tests/test_visitors/conftest.py b/tests/test_visitors/conftest.py index 9047c190c..9e6503eee 100644 --- a/tests/test_visitors/conftest.py +++ b/tests/test_visitors/conftest.py @@ -62,7 +62,7 @@ def options(): """Returns the options builder.""" default_values = { _to_dest_option(option.long_option_name): option.default - for option in Configuration.all_options() + for option in Configuration.options } Options = namedtuple('options', default_values.keys()) diff --git a/tests/test_visitors/test_ast/test_complexity/test_counts/test_imports_counts.py b/tests/test_visitors/test_ast/test_complexity/test_counts/test_imports_counts.py new file mode 100644 index 000000000..eb2506ae4 --- /dev/null +++ b/tests/test_visitors/test_ast/test_complexity/test_counts/test_imports_counts.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +import pytest + +from wemake_python_styleguide.visitors.ast.complexity.counts import ( + ImportMembersVisitor, + TooManyImportsViolation, +) + +module_import = '' +module_with_regular_imports = """ +import sys +import os +""" + +module_with_from_imports = """ +from os import path +from sys import version +""" + + +@pytest.mark.parametrize('code', [ + module_import, + module_with_regular_imports, + module_with_from_imports, +]) +def test_module_import_counts_normal( + assert_errors, parse_ast_tree, code, default_options, +): + """Testing that imports in a module work well.""" + tree = parse_ast_tree(code) + + visitor = ImportMembersVisitor(default_options, tree=tree) + visitor.run() + + assert_errors(visitor, []) + + +@pytest.mark.parametrize('code', [ + module_with_regular_imports, + module_with_from_imports, +]) +def test_module_import_counts_violation( + assert_errors, parse_ast_tree, code, options, +): + """Testing that violations are raised when reaching max value.""" + tree = parse_ast_tree(code) + + option_values = options(max_imports=1) + visitor = ImportMembersVisitor(option_values, tree=tree) + visitor.run() + + assert_errors(visitor, [TooManyImportsViolation]) diff --git a/tests/test_visitors/test_ast/test_complexity/test_counts/test_module_counts.py b/tests/test_visitors/test_ast/test_complexity/test_counts/test_module_counts.py index 6c6849225..26f265c76 100644 --- a/tests/test_visitors/test_ast/test_complexity/test_counts/test_module_counts.py +++ b/tests/test_visitors/test_ast/test_complexity/test_counts/test_module_counts.py @@ -32,8 +32,8 @@ def test_module_counts_normal( """Testing that classes and functions in a module work well.""" tree = parse_ast_tree(code) - visitor = ModuleMembersVisitor(default_options, None) - visitor.visit(tree) + visitor = ModuleMembersVisitor(default_options, tree=tree) + visitor.run() assert_errors(visitor, []) @@ -49,7 +49,7 @@ def test_module_counts_violation( tree = parse_ast_tree(code) option_values = options(max_module_members=1) - visitor = ModuleMembersVisitor(option_values, None) - visitor.visit(tree) + visitor = ModuleMembersVisitor(option_values, tree=tree) + visitor.run() assert_errors(visitor, [TooManyModuleMembersViolation]) diff --git a/tests/test_visitors/test_ast/test_wrong_function_call/test_wrong_function_calls.py b/tests/test_visitors/test_ast/test_general/test_wrong_function_call/test_wrong_function_calls.py similarity index 95% rename from tests/test_visitors/test_ast/test_wrong_function_call/test_wrong_function_calls.py rename to tests/test_visitors/test_ast/test_general/test_wrong_function_call/test_wrong_function_calls.py index a531030e5..d840c63a9 100644 --- a/tests/test_visitors/test_ast/test_wrong_function_call/test_wrong_function_calls.py +++ b/tests/test_visitors/test_ast/test_general/test_wrong_function_call/test_wrong_function_calls.py @@ -2,7 +2,7 @@ import pytest -from wemake_python_styleguide.visitors.ast.wrong_function_call import ( +from wemake_python_styleguide.visitors.ast.general.wrong_function_call import ( BAD_FUNCTIONS, WrongFunctionCallViolation, WrongFunctionCallVisitor, diff --git a/tests/test_visitors/test_ast/test_wrong_import/test_dotted_raw_import.py b/tests/test_visitors/test_ast/test_general/test_wrong_import/test_dotted_raw_import.py similarity index 96% rename from tests/test_visitors/test_ast/test_wrong_import/test_dotted_raw_import.py rename to tests/test_visitors/test_ast/test_general/test_wrong_import/test_dotted_raw_import.py index f706ee41b..af0a81997 100644 --- a/tests/test_visitors/test_ast/test_wrong_import/test_dotted_raw_import.py +++ b/tests/test_visitors/test_ast/test_general/test_wrong_import/test_dotted_raw_import.py @@ -2,7 +2,7 @@ import pytest -from wemake_python_styleguide.visitors.ast.wrong_import import ( +from wemake_python_styleguide.visitors.ast.general.wrong_import import ( DottedRawImportViolation, WrongImportVisitor, ) diff --git a/tests/test_visitors/test_ast/test_wrong_import/test_future_imports.py b/tests/test_visitors/test_ast/test_general/test_wrong_import/test_future_imports.py similarity index 95% rename from tests/test_visitors/test_ast/test_wrong_import/test_future_imports.py rename to tests/test_visitors/test_ast/test_general/test_wrong_import/test_future_imports.py index c3f735820..41c4dcd98 100644 --- a/tests/test_visitors/test_ast/test_wrong_import/test_future_imports.py +++ b/tests/test_visitors/test_ast/test_general/test_wrong_import/test_future_imports.py @@ -3,7 +3,7 @@ import pytest from wemake_python_styleguide.constants import FUTURE_IMPORTS_WHITELIST -from wemake_python_styleguide.visitors.ast.wrong_import import ( +from wemake_python_styleguide.visitors.ast.general.wrong_import import ( FutureImportViolation, WrongImportVisitor, ) diff --git a/tests/test_visitors/test_ast/test_wrong_import/test_nested_imports.py b/tests/test_visitors/test_ast/test_general/test_wrong_import/test_nested_imports.py similarity index 95% rename from tests/test_visitors/test_ast/test_wrong_import/test_nested_imports.py rename to tests/test_visitors/test_ast/test_general/test_wrong_import/test_nested_imports.py index 7bf4a5ef6..1723d9670 100644 --- a/tests/test_visitors/test_ast/test_wrong_import/test_nested_imports.py +++ b/tests/test_visitors/test_ast/test_general/test_wrong_import/test_nested_imports.py @@ -2,7 +2,7 @@ import pytest -from wemake_python_styleguide.visitors.ast.wrong_import import ( +from wemake_python_styleguide.visitors.ast.general.wrong_import import ( NestedImportViolation, WrongImportVisitor, ) diff --git a/tests/test_visitors/test_ast/test_wrong_import/test_relative_imports.py b/tests/test_visitors/test_ast/test_general/test_wrong_import/test_relative_imports.py similarity index 94% rename from tests/test_visitors/test_ast/test_wrong_import/test_relative_imports.py rename to tests/test_visitors/test_ast/test_general/test_wrong_import/test_relative_imports.py index 5102fe5db..3af02d112 100644 --- a/tests/test_visitors/test_ast/test_wrong_import/test_relative_imports.py +++ b/tests/test_visitors/test_ast/test_general/test_wrong_import/test_relative_imports.py @@ -2,7 +2,7 @@ import pytest -from wemake_python_styleguide.visitors.ast.wrong_import import ( +from wemake_python_styleguide.visitors.ast.general.wrong_import import ( LocalFolderImportViolation, WrongImportVisitor, ) diff --git a/tests/test_visitors/test_ast/test_wrong_import/test_same_alias_import.py b/tests/test_visitors/test_ast/test_general/test_wrong_import/test_same_alias_import.py similarity index 93% rename from tests/test_visitors/test_ast/test_wrong_import/test_same_alias_import.py rename to tests/test_visitors/test_ast/test_general/test_wrong_import/test_same_alias_import.py index 63ce28579..41f9befc6 100644 --- a/tests/test_visitors/test_ast/test_wrong_import/test_same_alias_import.py +++ b/tests/test_visitors/test_ast/test_general/test_wrong_import/test_same_alias_import.py @@ -2,7 +2,7 @@ import pytest -from wemake_python_styleguide.visitors.ast.wrong_import import ( +from wemake_python_styleguide.visitors.ast.general.wrong_import import ( SameAliasImportViolation, WrongImportVisitor, ) diff --git a/tests/test_visitors/test_ast/test_wrong_keyword/test_del.py b/tests/test_visitors/test_ast/test_general/test_wrong_keyword/test_del.py similarity index 87% rename from tests/test_visitors/test_ast/test_wrong_keyword/test_del.py rename to tests/test_visitors/test_ast/test_general/test_wrong_keyword/test_del.py index 02ece2448..67d44a7dc 100644 --- a/tests/test_visitors/test_ast/test_wrong_keyword/test_del.py +++ b/tests/test_visitors/test_ast/test_general/test_wrong_keyword/test_del.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from wemake_python_styleguide.visitors.ast.wrong_keyword import ( +from wemake_python_styleguide.visitors.ast.general.wrong_keyword import ( WrongKeywordViolation, WrongKeywordVisitor, ) diff --git a/tests/test_visitors/test_ast/test_wrong_keyword/test_global.py b/tests/test_visitors/test_ast/test_general/test_wrong_keyword/test_global.py similarity index 90% rename from tests/test_visitors/test_ast/test_wrong_keyword/test_global.py rename to tests/test_visitors/test_ast/test_general/test_wrong_keyword/test_global.py index a3f826e9c..ebc0016ed 100644 --- a/tests/test_visitors/test_ast/test_wrong_keyword/test_global.py +++ b/tests/test_visitors/test_ast/test_general/test_wrong_keyword/test_global.py @@ -2,7 +2,7 @@ import pytest -from wemake_python_styleguide.visitors.ast.wrong_keyword import ( +from wemake_python_styleguide.visitors.ast.general.wrong_keyword import ( WrongKeywordViolation, WrongKeywordVisitor, ) diff --git a/tests/test_visitors/test_ast/test_wrong_keyword/test_pass.py b/tests/test_visitors/test_ast/test_general/test_wrong_keyword/test_pass.py similarity index 91% rename from tests/test_visitors/test_ast/test_wrong_keyword/test_pass.py rename to tests/test_visitors/test_ast/test_general/test_wrong_keyword/test_pass.py index f05c41138..2255c9f0a 100644 --- a/tests/test_visitors/test_ast/test_wrong_keyword/test_pass.py +++ b/tests/test_visitors/test_ast/test_general/test_wrong_keyword/test_pass.py @@ -2,7 +2,7 @@ import pytest -from wemake_python_styleguide.visitors.ast.wrong_keyword import ( +from wemake_python_styleguide.visitors.ast.general.wrong_keyword import ( WrongKeywordViolation, WrongKeywordVisitor, ) diff --git a/tests/test_visitors/test_ast/test_wrong_keyword/test_raise_notimplemented.py b/tests/test_visitors/test_ast/test_general/test_wrong_keyword/test_raise_notimplemented.py similarity index 95% rename from tests/test_visitors/test_ast/test_wrong_keyword/test_raise_notimplemented.py rename to tests/test_visitors/test_ast/test_general/test_wrong_keyword/test_raise_notimplemented.py index 4c9f72d42..e43c9cf58 100644 --- a/tests/test_visitors/test_ast/test_wrong_keyword/test_raise_notimplemented.py +++ b/tests/test_visitors/test_ast/test_general/test_wrong_keyword/test_raise_notimplemented.py @@ -2,7 +2,7 @@ import pytest -from wemake_python_styleguide.visitors.ast.wrong_keyword import ( +from wemake_python_styleguide.visitors.ast.general.wrong_keyword import ( RaiseNotImplementedViolation, WrongRaiseVisitor, ) diff --git a/tests/test_visitors/test_ast/test_wrong_name/test_class_attributes.py b/tests/test_visitors/test_ast/test_general/test_wrong_name/test_class_attributes.py similarity index 97% rename from tests/test_visitors/test_ast/test_wrong_name/test_class_attributes.py rename to tests/test_visitors/test_ast/test_general/test_wrong_name/test_class_attributes.py index 619d430ce..f8f60c573 100644 --- a/tests/test_visitors/test_ast/test_wrong_name/test_class_attributes.py +++ b/tests/test_visitors/test_ast/test_general/test_wrong_name/test_class_attributes.py @@ -4,7 +4,7 @@ import pytest -from wemake_python_styleguide.visitors.ast.wrong_name import ( +from wemake_python_styleguide.visitors.ast.general.wrong_name import ( BAD_VARIABLE_NAMES, PrivateNameViolation, TooShortVariableNameViolation, diff --git a/tests/test_visitors/test_ast/test_wrong_name/test_function_args_names.py b/tests/test_visitors/test_ast/test_general/test_wrong_name/test_function_args_names.py similarity index 97% rename from tests/test_visitors/test_ast/test_wrong_name/test_function_args_names.py rename to tests/test_visitors/test_ast/test_general/test_wrong_name/test_function_args_names.py index 7f5ef5253..c2b1751c2 100644 --- a/tests/test_visitors/test_ast/test_wrong_name/test_function_args_names.py +++ b/tests/test_visitors/test_ast/test_general/test_wrong_name/test_function_args_names.py @@ -4,7 +4,7 @@ import pytest -from wemake_python_styleguide.visitors.ast.wrong_name import ( +from wemake_python_styleguide.visitors.ast.general.wrong_name import ( BAD_VARIABLE_NAMES, PrivateNameViolation, TooShortVariableNameViolation, diff --git a/tests/test_visitors/test_ast/test_wrong_name/test_function_names.py b/tests/test_visitors/test_ast/test_general/test_wrong_name/test_function_names.py similarity index 96% rename from tests/test_visitors/test_ast/test_wrong_name/test_function_names.py rename to tests/test_visitors/test_ast/test_general/test_wrong_name/test_function_names.py index b3513a400..19d79bb5e 100644 --- a/tests/test_visitors/test_ast/test_wrong_name/test_function_names.py +++ b/tests/test_visitors/test_ast/test_general/test_wrong_name/test_function_names.py @@ -4,7 +4,7 @@ import pytest -from wemake_python_styleguide.visitors.ast.wrong_name import ( +from wemake_python_styleguide.visitors.ast.general.wrong_name import ( BAD_VARIABLE_NAMES, PrivateNameViolation, TooShortVariableNameViolation, diff --git a/tests/test_visitors/test_ast/test_wrong_name/test_import_alias.py b/tests/test_visitors/test_ast/test_general/test_wrong_name/test_import_alias.py similarity index 96% rename from tests/test_visitors/test_ast/test_wrong_name/test_import_alias.py rename to tests/test_visitors/test_ast/test_general/test_wrong_name/test_import_alias.py index d93665198..8e99b4721 100644 --- a/tests/test_visitors/test_ast/test_wrong_name/test_import_alias.py +++ b/tests/test_visitors/test_ast/test_general/test_wrong_name/test_import_alias.py @@ -4,7 +4,7 @@ import pytest -from wemake_python_styleguide.visitors.ast.wrong_name import ( +from wemake_python_styleguide.visitors.ast.general.wrong_name import ( BAD_VARIABLE_NAMES, PrivateNameViolation, TooShortVariableNameViolation, diff --git a/tests/test_visitors/test_ast/test_wrong_name/test_module_metadata.py b/tests/test_visitors/test_ast/test_general/test_wrong_name/test_module_metadata.py similarity index 95% rename from tests/test_visitors/test_ast/test_wrong_name/test_module_metadata.py rename to tests/test_visitors/test_ast/test_general/test_wrong_name/test_module_metadata.py index 550e4be75..7b8bd168a 100644 --- a/tests/test_visitors/test_ast/test_wrong_name/test_module_metadata.py +++ b/tests/test_visitors/test_ast/test_general/test_wrong_name/test_module_metadata.py @@ -2,7 +2,7 @@ import pytest -from wemake_python_styleguide.visitors.ast.wrong_name import ( +from wemake_python_styleguide.visitors.ast.general.wrong_name import ( BAD_MODULE_METADATA_VARIABLES, WrongModuleMetadataViolation, WrongModuleMetadataVisitor, diff --git a/tests/test_visitors/test_ast/test_wrong_name/test_variable_names.py b/tests/test_visitors/test_ast/test_general/test_wrong_name/test_variable_names.py similarity index 97% rename from tests/test_visitors/test_ast/test_wrong_name/test_variable_names.py rename to tests/test_visitors/test_ast/test_general/test_wrong_name/test_variable_names.py index 276afe8da..aa3901d99 100644 --- a/tests/test_visitors/test_ast/test_wrong_name/test_variable_names.py +++ b/tests/test_visitors/test_ast/test_general/test_wrong_name/test_variable_names.py @@ -4,7 +4,7 @@ import pytest -from wemake_python_styleguide.visitors.ast.wrong_name import ( +from wemake_python_styleguide.visitors.ast.general.wrong_name import ( BAD_VARIABLE_NAMES, PrivateNameViolation, TooShortVariableNameViolation, diff --git a/tests/test_visitors/test_ast/test_wrong_string.py b/tests/test_visitors/test_ast/test_general/test_wrong_string.py similarity index 94% rename from tests/test_visitors/test_ast/test_wrong_string.py rename to tests/test_visitors/test_ast/test_general/test_wrong_string.py index 6d063be61..c8c49a606 100644 --- a/tests/test_visitors/test_ast/test_wrong_string.py +++ b/tests/test_visitors/test_ast/test_general/test_wrong_string.py @@ -2,7 +2,7 @@ import pytest -from wemake_python_styleguide.visitors.ast.wrong_string import ( +from wemake_python_styleguide.visitors.ast.general.wrong_string import ( FormattedStringViolation, WrongStringVisitor, ) diff --git a/tests/test_visitors/test_ast/test_wrong_contents/test_empty_init.py b/tests/test_visitors/test_ast/test_wrong_module/test_empty_init.py similarity index 95% rename from tests/test_visitors/test_ast/test_wrong_contents/test_empty_init.py rename to tests/test_visitors/test_ast/test_wrong_module/test_empty_init.py index 623d31a98..8322307be 100644 --- a/tests/test_visitors/test_ast/test_wrong_contents/test_empty_init.py +++ b/tests/test_visitors/test_ast/test_wrong_module/test_empty_init.py @@ -2,7 +2,7 @@ import pytest -from wemake_python_styleguide.visitors.ast.wrong_contents import ( +from wemake_python_styleguide.visitors.ast.wrong_module import ( InitModuleHasLogicViolation, WrongContentsVisitor, ) diff --git a/tests/test_visitors/test_ast/test_wrong_contents/test_empty_modules.py b/tests/test_visitors/test_ast/test_wrong_module/test_empty_modules.py similarity index 89% rename from tests/test_visitors/test_ast/test_wrong_contents/test_empty_modules.py rename to tests/test_visitors/test_ast/test_wrong_module/test_empty_modules.py index 490b2e578..0aa06cc3c 100644 --- a/tests/test_visitors/test_ast/test_wrong_contents/test_empty_modules.py +++ b/tests/test_visitors/test_ast/test_wrong_module/test_empty_modules.py @@ -2,7 +2,7 @@ import pytest -from wemake_python_styleguide.visitors.ast.wrong_contents import ( +from wemake_python_styleguide.visitors.ast.wrong_module import ( EmptyModuleViolation, WrongContentsVisitor, ) diff --git a/tests/test_visitors/test_base.py b/tests/test_visitors/test_base.py index 1bb9b3cb5..f82d65b97 100644 --- a/tests/test_visitors/test_base.py +++ b/tests/test_visitors/test_base.py @@ -6,22 +6,15 @@ from wemake_python_styleguide import constants from wemake_python_styleguide.visitors.base import ( - BaseChecker, BaseFilenameVisitor, - BaseNodeVisitor, + BaseVisitor, ) -def test_raises_value_error_without_tree(default_options): - """Ensures that ValueError is raised when visitor does not have a tree.""" - with pytest.raises(ValueError): - BaseNodeVisitor(default_options).run() - - def test_checker_raises_not_implemented(default_options): """Ensures that `BaseChecker` raises `NotImplementedError`.""" with pytest.raises(NotImplementedError): - BaseChecker(default_options).run() + BaseVisitor(default_options).run() def test_base_filename_raises_not_implemented(default_options): diff --git a/wemake_python_styleguide/checker.py b/wemake_python_styleguide/checker.py index bcb2a79d0..92cd43f7c 100644 --- a/wemake_python_styleguide/checker.py +++ b/wemake_python_styleguide/checker.py @@ -1,19 +1,23 @@ # -*- coding: utf-8 -*- -from ast import Module -from typing import Generator +import ast +from tokenize import TokenInfo +from typing import Generator, Sequence from flake8.options.manager import OptionManager from wemake_python_styleguide import constants from wemake_python_styleguide.options.config import Configuration from wemake_python_styleguide.types import ( - CheckerSequence, CheckResult, ConfigurationOptions, + TokenVisitorSequence, + TreeVisitorSequence, + VisitorSequence, ) from wemake_python_styleguide.version import version from wemake_python_styleguide.visitors.ast.complexity.counts import ( + ImportMembersVisitor, MethodMembersVisitor, ModuleMembersVisitor, ) @@ -29,63 +33,42 @@ from wemake_python_styleguide.visitors.ast.complexity.offset import ( OffsetVisitor, ) -from wemake_python_styleguide.visitors.ast.wrong_class import WrongClassVisitor -from wemake_python_styleguide.visitors.ast.wrong_contents import ( - WrongContentsVisitor, -) -from wemake_python_styleguide.visitors.ast.wrong_function_call import ( +from wemake_python_styleguide.visitors.ast.general.wrong_function_call import ( WrongFunctionCallVisitor, ) -from wemake_python_styleguide.visitors.ast.wrong_import import ( +from wemake_python_styleguide.visitors.ast.general.wrong_import import ( WrongImportVisitor, ) -from wemake_python_styleguide.visitors.ast.wrong_keyword import ( +from wemake_python_styleguide.visitors.ast.general.wrong_keyword import ( WrongKeywordVisitor, WrongRaiseVisitor, ) -from wemake_python_styleguide.visitors.ast.wrong_name import ( +from wemake_python_styleguide.visitors.ast.general.wrong_name import ( WrongModuleMetadataVisitor, WrongNameVisitor, ) -from wemake_python_styleguide.visitors.ast.wrong_string import ( +from wemake_python_styleguide.visitors.ast.general.wrong_string import ( WrongStringVisitor, ) +from wemake_python_styleguide.visitors.ast.wrong_class import WrongClassVisitor +from wemake_python_styleguide.visitors.ast.wrong_module import ( + WrongContentsVisitor, +) from wemake_python_styleguide.visitors.filenames.wrong_module_name import ( WrongModuleNameVisitor, ) -#: Visitors that should be working by default: -ENABLED_VISITORS: CheckerSequence = [ - # Styling and correctness: - WrongRaiseVisitor, - WrongFunctionCallVisitor, - WrongImportVisitor, - WrongKeywordVisitor, - WrongNameVisitor, - WrongModuleMetadataVisitor, - WrongClassVisitor, - WrongStringVisitor, - WrongContentsVisitor, - - # Complexity: - FunctionComplexityVisitor, - NestedComplexityVisitor, - OffsetVisitor, - ModuleMembersVisitor, - MethodMembersVisitor, - JonesComplexityVisitor, - - # Modules: - WrongModuleNameVisitor, -] - class Checker(object): """ Main checker class. Runs all checks that are bundled with this package. - If you want to add new checks they should be added to ``ENABLED_VISITORS``. + If you want to add new checks they should be added to either: + + - ``ast_visitors`` if it is an ``ast`` based visitor + - ``token_visitors`` if it is a ``token`` based visitor + """ name = 'wemake-python-styleguide' @@ -94,11 +77,46 @@ class Checker(object): config = Configuration() options: ConfigurationOptions - # Receive `logic_line` as the first argument to make this plugin logical - def __init__(self, tree: Module, filename: str = constants.STDIN) -> None: + #: Visitors that should be working by default: + ast_visitors: TreeVisitorSequence = ( + # General: + WrongRaiseVisitor, + WrongFunctionCallVisitor, + WrongImportVisitor, + WrongKeywordVisitor, + WrongNameVisitor, + WrongModuleMetadataVisitor, + WrongStringVisitor, + WrongContentsVisitor, + + # Complexity: + FunctionComplexityVisitor, + NestedComplexityVisitor, + OffsetVisitor, + ImportMembersVisitor, + ModuleMembersVisitor, + MethodMembersVisitor, + JonesComplexityVisitor, + + # Classes: + WrongClassVisitor, + + # Modules: + WrongModuleNameVisitor, + ) + + token_visitors: TokenVisitorSequence = () + + def __init__( + self, + tree: ast.Module, + file_tokens: Sequence[TokenInfo], + filename: str = constants.STDIN, + ) -> None: """Creates new checker instance.""" self.tree = tree self.filename = filename + self.file_tokens = file_tokens @classmethod def add_options(cls, parser: OptionManager) -> None: @@ -110,20 +128,24 @@ def parse_options(cls, options: ConfigurationOptions) -> None: """Parses registered options for providing to the visitor.""" cls.options = options + def _run_checks( + self, + visitors: VisitorSequence, + ) -> Generator[CheckResult, None, None]: + """Runs all ``ast`` based visitors one by one.""" + for visitor_class in visitors: + visitor = visitor_class.from_checker(self) + visitor.run() + + for error in visitor.errors: + yield (*error.node_items(), type(self)) + def run(self) -> Generator[CheckResult, None, None]: """ Runs the checker. - This method is used by `flake8` API. + This method is used by ``flake8`` API. After all configuration is parsed and passed. """ - for visitor_class in ENABLED_VISITORS: - visitor = visitor_class( - self.options, - tree=self.tree, - filename=self.filename, - ) - visitor.run() - - for error in visitor.errors: - yield (*error.node_items(), type(self)) + yield from self._run_checks(self.ast_visitors) + yield from self._run_checks(self.token_visitors) diff --git a/wemake_python_styleguide/errors/complexity.py b/wemake_python_styleguide/errors/complexity.py index a97670112..eaaaf8d62 100644 --- a/wemake_python_styleguide/errors/complexity.py +++ b/wemake_python_styleguide/errors/complexity.py @@ -9,6 +9,9 @@ That's how many objects we can keep in our memory at a time. We try hard not to exceed the limit. +You can also find interesting reading about "Cognitive complexity": +https://www.sonarsource.com/docs/CognitiveComplexity.pdf + What we call "design flaws": 1. Complex code (there are a lof of complexity checks!) @@ -19,6 +22,7 @@ Simple is better than complex. Complex is better than complicated. Flat is better than nested. + Namespaces are one honking great idea -- let's do more of those! """ @@ -197,7 +201,8 @@ class TooManyElifsViolation(ASTStyleViolation): """ - error_template = '{0} Found too many "{1}" branches' + should_use_text = False + error_template = '{0} Found too many `elif` branches' code = 'Z204' @@ -230,7 +235,7 @@ class TooManyExpressionsViolation(ASTStyleViolation): Reasoning: When there are too many expression it means that this specific - function does too many things at once. It has too many logics. + function does too many things at once. It has too many logic. Solution: Split function into several functions, refactor your API. @@ -256,7 +261,7 @@ class TooDeepNestingViolation(ASTStyleViolation): suited to handle such construction. Solution: - We need to refactor our complex construction into simplier ones. + We need to refactor our complex construction into simpler ones. We can use new functions or different constructions. This rule is configurable with ``--max-offset-blocks``. @@ -270,7 +275,7 @@ class TooDeepNestingViolation(ASTStyleViolation): code = 'Z207' -class TooManyModuleMembersViolation(ASTStyleViolation): +class TooManyModuleMembersViolation(SimpleStyleViolation): """ This rule forbids to have many classes and functions in a single module. @@ -293,11 +298,12 @@ class TooManyModuleMembersViolation(ASTStyleViolation): """ - error_template = '{0} Found too many members "{1}"' + should_use_text = False + error_template = '{0} Found too many members' code = 'Z208' -class TooManyMethodsViolation(ASTStyleViolation): +class TooManyMethodsViolation(SimpleStyleViolation): """ This rule forbids to have many methods in a single class. @@ -336,7 +342,7 @@ class LineComplexityViolation(ASTStyleViolation): We are using Jones Complexity algorithm to count complexity. What is Jones Complexity? It is a simple yet power method to count the number of ``ast`` nodes per line. - If the complexity of a single line is higher than a tresshold, + If the complexity of a single line is higher than a threshold, then an error is raised. What nodes do we count? All except the following: @@ -375,13 +381,14 @@ class JonesScoreViolation(SimpleStyleViolation): """ This rule forbids to have modules with complex lines. - We are using Jones Complexity algorithm to count module score. + We are using Jones Complexity algorithm to count module's score. See :py:class:`~.LineComplexityViolation` for details of per-line-complexity. - How it is done: we count complexity per line + How it is done: we count complexity per line, then measuring the median + complexity across the lines in the whole module. Reasoning: - Having complex modules will decrease your code maintability. + Having complex modules will decrease your code maintainability. Solution: Refactor the module contents. @@ -399,3 +406,40 @@ class JonesScoreViolation(SimpleStyleViolation): should_use_text = False error_template = '{0} Found module with high Jones score' code = 'Z211' + + +class TooManyImportsViolation(SimpleStyleViolation): + """ + This rule forbids to have modules with too many imports. + + Namespaces are one honking great idea -- let's do more of those! + + Reasoning: + Having too many imports without prefixes is quite expensive. + You have to memorize all the source locations of the imports. + And sometimes it is hard to remember what kind of functions and classes + are already injected into your context. + + It is also a questionable design if a single module has a lot of + imports. Why a single module has so many dependencies? + So, it becomes too coupled. + + Solution: + Refactor the imports to import a common namespace. Something like + ``from package import module`` and then + use it like ``module.function()``. + + Or refactor your code and split the complex module into several ones. + + We do not make any differences between + ``import`` and ``from ... import ...``. + + This rule is configurable with ``--max-imports``. + + Note: + Returns Z212 as error code + + """ + + error_template = '{0} Found module with too many imports: {1}' + code = 'Z212' diff --git a/wemake_python_styleguide/logics/limits.py b/wemake_python_styleguide/logics/limits.py deleted file mode 100644 index d045412c9..000000000 --- a/wemake_python_styleguide/logics/limits.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- - - -def has_just_exceeded_limit(current_value: int, limit: int) -> bool: - """ - Check either value has just exceeded its limit or not. - - >>> has_just_exceeded_limit(1, 2) - False - - >>> has_just_exceeded_limit(1, 1) - False - - >>> has_just_exceeded_limit(2, 1) - True - - >>> has_just_exceeded_limit(3, 1) - False - - """ - return current_value == limit + 1 diff --git a/wemake_python_styleguide/options/config.py b/wemake_python_styleguide/options/config.py index 802ab7a5d..c04fd0af2 100644 --- a/wemake_python_styleguide/options/config.py +++ b/wemake_python_styleguide/options/config.py @@ -25,20 +25,36 @@ class Configuration(object): """ Provides configuration options for ``wemake-python-styleguide`` plugin. - You can adjust configuration via CLI option: + We do not like our linter to be configurable. + Since people may take the wrong path or make wrong decisions. + However, you can adjust some options via CLI option: Example:: flake8 --max-returns 7 - You can also provide configuration options in ``tox.ini`` or ``setup.cfg``: + Or you can provide options in ``tox.ini`` or ``setup.cfg``: Example:: [flake8] max-returns = 7 - We support the following options: + We use ``setup.cfg`` as a default way to provide configuration. + + Options for general checks: + + - ``min-variable-length`` - minimum number of chars to define a valid + variable name, defaults to + :str:`wemake_python_styleguide.options.defaults.MIN_VARIABLE_LENGTH` + + Options for module related checks: + + - ``min-module-name-length`` - minimum required module's name length, + defaults to + :str:`wemake_python_styleguide.options.defaults.MIN_MODULE_NAME_LENGTH` + + Options for complexity related checks: - ``max-returns`` - maximum allowed number of ``return`` statements in one function, defaults to @@ -52,9 +68,6 @@ class Configuration(object): - ``max-arguments`` - maximum allowed number of arguments in one function, defaults to :str:`wemake_python_styleguide.options.defaults.MAX_ARGUMENTS` - - ``min-variable-length`` - minimum number of chars to define a valid - variable name, defaults to - :str:`wemake_python_styleguide.options.defaults.MIN_VARIABLE_LENGTH` - ``max-offset-blocks`` - maximum number of block to nest expressions, defaults to :str:`wemake_python_styleguide.options.defaults.MAX_OFFSET_BLOCKS` @@ -66,9 +79,6 @@ class Configuration(object): - ``max-methods`` - maximum number of methods in a single class, defaults to :str:`wemake_python_styleguide.options.defaults.MAX_METHODS` - - ``min-module-name-length`` - minimum required module's name length, - defaults to - :str:`wemake_python_styleguide.options.defaults.MIN_MODULE_NAME_LENGTH` - ``max-line-complexity`` - maximum line complexity measured in number of ``ast`` nodes per line, defaults to :str:`wemake_python_styleguide.options.defaults.MAX_LINE_COMPLEXITY` @@ -78,84 +88,95 @@ class Configuration(object): """ - @classmethod - def all_options(cls) -> Sequence[_Option]: - """Returns a list of option values we use in this plugin.""" - return [ - _Option( - '--max-returns', - defaults.MAX_RETURNS, - 'Maximum allowed number of return statements in one function.', - ), - - _Option( - '--max-local-variables', - defaults.MAX_LOCAL_VARIABLES, - 'Maximum allowed number of local variables in one function.', - ), - - _Option( - '--max-expressions', - defaults.MAX_EXPRESSIONS, - 'Maximum allowed number of expressions in one function.', - ), - - _Option( - '--max-arguments', - defaults.MAX_ARGUMENTS, - 'Maximum allowed number of arguments in one function.', - ), - - _Option( - '--min-variable-length', - defaults.MIN_VARIABLE_LENGTH, - 'Minimum required length of the variable name.', - ), - - _Option( - '--max-offset-blocks', - defaults.MAX_OFFSET_BLOCKS, - 'Maximum number of blocks to nest different structures.', - ), - - _Option( - '--max-elifs', - defaults.MAX_ELIFS, - 'Maximum number of `elif` blocks.', - ), - - _Option( - '--max-module-members', - defaults.MAX_MODULE_MEMBERS, - 'Maximum number of classes and functions in a single module.', - ), - - _Option( - '--max-methods', - defaults.MAX_METHODS, - 'Maximum number of methods in a single class.', - ), - - _Option( - '--min-module-name-length', - defaults.MIN_MODULE_NAME_LENGTH, - "Minimum required module's name length", - ), - - _Option( - '--max-line-complexity', - defaults.MAX_LINE_COMPLEXITY, - 'Maximum line complexity, measured in `ast` nodes.', - ), - - _Option( - '--max-jones-score', - defaults.MAX_JONES_SCORE, - 'Maximum median module complexity, based on sum of lines.', - ), - ] + #: List of option values we use in this plugin: + options: Sequence[_Option] = [ + + # Complexity: + + _Option( + '--max-returns', + defaults.MAX_RETURNS, + 'Maximum allowed number of return statements in one function.', + ), + + _Option( + '--max-local-variables', + defaults.MAX_LOCAL_VARIABLES, + 'Maximum allowed number of local variables in one function.', + ), + + _Option( + '--max-expressions', + defaults.MAX_EXPRESSIONS, + 'Maximum allowed number of expressions in one function.', + ), + + _Option( + '--max-arguments', + defaults.MAX_ARGUMENTS, + 'Maximum allowed number of arguments in one function.', + ), + + _Option( + '--max-offset-blocks', + defaults.MAX_OFFSET_BLOCKS, + 'Maximum number of blocks to nest different structures.', + ), + + _Option( + '--max-elifs', + defaults.MAX_ELIFS, + 'Maximum number of `elif` blocks.', + ), + + _Option( + '--max-module-members', + defaults.MAX_MODULE_MEMBERS, + 'Maximum number of classes and functions in a single module.', + ), + + _Option( + '--max-methods', + defaults.MAX_METHODS, + 'Maximum number of methods in a single class.', + ), + + _Option( + '--max-line-complexity', + defaults.MAX_LINE_COMPLEXITY, + 'Maximum line complexity, measured in `ast` nodes.', + ), + + _Option( + '--max-jones-score', + defaults.MAX_JONES_SCORE, + 'Maximum median module complexity, based on sum of lines.', + ), + + _Option( + '--max-imports', + defaults.MAX_IMPORTS, + 'Maximum number of imports in a single module.', + ), + + # General: + + _Option( + '--min-variable-length', + defaults.MIN_VARIABLE_LENGTH, + 'Minimum required length of the variable name.', + ), + + # Modules: + + _Option( + '--min-module-name-length', + defaults.MIN_MODULE_NAME_LENGTH, + "Minimum required module's name length", + ), + ] def register_options(self, parser: OptionManager) -> None: """Registers options for our plugin.""" - for option in self.all_options(): + for option in self.options: parser.add_option(**attr.asdict(option)) diff --git a/wemake_python_styleguide/options/defaults.py b/wemake_python_styleguide/options/defaults.py index cf0fbe221..e4ebdca81 100644 --- a/wemake_python_styleguide/options/defaults.py +++ b/wemake_python_styleguide/options/defaults.py @@ -14,6 +14,14 @@ if you find them too strict or too permissive. """ +# General + +#: Minimum variable's name length: +MIN_VARIABLE_LENGTH = 2 + + +# Complexity + #: Maximum number of `return` statements allowed in a single function: MAX_RETURNS = 5 @@ -26,14 +34,11 @@ #: Maximum number of arguments for functions or method, `self` is not counted: MAX_ARGUMENTS = 5 -#: Minimum variable's name length: -MIN_VARIABLE_LENGTH = 2 - #: Maximum number of blocks to nest different structures: MAX_OFFSET_BLOCKS = 5 #: Maximum number of `elif` blocks in a single `if` condition: -MAX_ELIFS = 2 +MAX_ELIFS = 3 #: Maximum number of classes and functions in a single module: MAX_MODULE_MEMBERS = 7 @@ -47,6 +52,9 @@ #: Maximum median module Jones complexity: MAX_JONES_SCORE = 12 # this value was "guessed" based on existing source code +#: Maximum number of imports in a single module: +MAX_IMPORTS = 12 + # Modules diff --git a/wemake_python_styleguide/types.py b/wemake_python_styleguide/types.py index 4e4feb01d..b142f9fe5 100644 --- a/wemake_python_styleguide/types.py +++ b/wemake_python_styleguide/types.py @@ -18,8 +18,19 @@ # We do not need to do anything if typechecker is not working: Protocol = object -#: Checkers container, that has all enabled visitors' classes: -CheckerSequence = Sequence[Type['base.BaseChecker']] +#: Visitor container, that has all enabled visitors' classes: +VisitorSequence = Sequence[Type['base.BaseVisitor']] + +#: Tree specific visitors' classes: +TreeVisitorSequence = Sequence[ + Union[ + Type['base.BaseNodeVisitor'], + Type['base.BaseFilenameVisitor'], + ], +] + +#: Token specific visitors' classes: +TokenVisitorSequence = Sequence[Type['base.BaseVisitor']] #: In cases we need to work with both import types: AnyImport = Union[ast.Import, ast.ImportFrom] @@ -41,17 +52,21 @@ class or structure. See: https://mypy.readthedocs.io/en/latest/protocols.html """ + # General: + min_variable_length: int + + # Complexity: max_arguments: int max_local_variables: int max_returns: int max_expressions: int - min_variable_length: int max_offset_blocks: int max_elifs: int max_module_members: int max_methods: int max_line_complexity: int max_jones_score: int + max_imports: int # Modules: min_module_name_length: int diff --git a/wemake_python_styleguide/visitors/ast/complexity/counts.py b/wemake_python_styleguide/visitors/ast/complexity/counts.py index 89f526fae..d9136974b 100644 --- a/wemake_python_styleguide/visitors/ast/complexity/counts.py +++ b/wemake_python_styleguide/visitors/ast/complexity/counts.py @@ -5,12 +5,12 @@ from typing import DefaultDict from wemake_python_styleguide.errors.complexity import ( + TooManyImportsViolation, TooManyMethodsViolation, TooManyModuleMembersViolation, ) from wemake_python_styleguide.logics.functions import is_method -from wemake_python_styleguide.logics.limits import has_just_exceeded_limit -from wemake_python_styleguide.types import ModuleMembers +from wemake_python_styleguide.types import AnyImport, ModuleMembers from wemake_python_styleguide.visitors.base import BaseNodeVisitor @@ -29,11 +29,10 @@ def _check_members_count(self, node: ModuleMembers) -> None: if isinstance(parent, ast.Module) and not is_real_method: self._public_items_count += 1 - max_members = self.options.max_module_members - if has_just_exceeded_limit(self._public_items_count, max_members): - self.add_error( - TooManyModuleMembersViolation(node, text=self.filename), - ) + + def _post_visit(self) -> None: + if self._public_items_count > self.options.max_module_members: + self.add_error(TooManyModuleMembersViolation()) def visit_ClassDef(self, node: ast.ClassDef) -> None: """ @@ -58,6 +57,34 @@ def visit_FunctionDef(self, node: ast.FunctionDef) -> None: self.generic_visit(node) +class ImportMembersVisitor(BaseNodeVisitor): + """Counts imports in a module.""" + + def __init__(self, *args, **kwargs) -> None: + """Creates a counter for tracked metrics.""" + super().__init__(*args, **kwargs) + self._imports_count = 0 + + def _post_visit(self) -> None: + if self._imports_count > self.options.max_imports: + self.add_error( + TooManyImportsViolation(text=str(self._imports_count)), + ) + + def visit_Import(self, node: AnyImport) -> None: + """ + Counts the number of ``import`` and ``from ... import ...``. + + Raises: + TooManyImportsViolation + + """ + self._imports_count += 1 + self.generic_visit(node) + + visit_ImportFrom = visit_Import + + class MethodMembersVisitor(BaseNodeVisitor): """Counts methods in a single class.""" @@ -70,9 +97,11 @@ def _check_method(self, node: ast.FunctionDef) -> None: parent = getattr(node, 'parent', None) if isinstance(parent, ast.ClassDef): self._methods[parent] += 1 - max_methods = self.options.max_methods - if has_just_exceeded_limit(self._methods[parent], max_methods): - self.add_error(TooManyMethodsViolation(node, text=parent.name)) + + def _post_visit(self) -> None: + for node, count in self._methods.items(): + if count > self.options.max_methods: + self.add_error(TooManyMethodsViolation(text=node.name)) def visit_FunctionDef(self, node: ast.FunctionDef) -> None: """ diff --git a/wemake_python_styleguide/visitors/ast/complexity/function.py b/wemake_python_styleguide/visitors/ast/complexity/function.py index 25d57fcb3..faf11b108 100644 --- a/wemake_python_styleguide/visitors/ast/complexity/function.py +++ b/wemake_python_styleguide/visitors/ast/complexity/function.py @@ -2,9 +2,8 @@ import ast from collections import defaultdict -from typing import DefaultDict, List, Type +from typing import DefaultDict, List -from wemake_python_styleguide.errors.base import BaseStyleViolation from wemake_python_styleguide.errors.complexity import ( TooManyArgumentsViolation, TooManyElifsViolation, @@ -13,19 +12,22 @@ TooManyReturnsViolation, ) from wemake_python_styleguide.logics.functions import is_method -from wemake_python_styleguide.logics.limits import has_just_exceeded_limit from wemake_python_styleguide.visitors.base import BaseNodeVisitor +FunctionCounter = DefaultDict[ast.FunctionDef, int] + class _ComplexityCounter(object): """Helper class to encapsulate logic from the visitor.""" - def __init__(self, delegate: 'FunctionComplexityVisitor') -> None: - self.delegate = delegate - - self.expressions: DefaultDict[str, int] = defaultdict(int) - self.variables: DefaultDict[str, List[str]] = defaultdict(list) - self.returns: DefaultDict[str, int] = defaultdict(int) + def __init__(self) -> None: + self.arguments: FunctionCounter = defaultdict(int) + self.elifs: FunctionCounter = defaultdict(int) + self.returns: FunctionCounter = defaultdict(int) + self.expressions: FunctionCounter = defaultdict(int) + self.variables: DefaultDict[ + ast.FunctionDef, List[str], + ] = defaultdict(list) def _update_variables( self, @@ -38,62 +40,30 @@ def _update_variables( What is treated as a local variable? Check ``TooManyLocalsViolation`` documentation. """ - function_variables = self.variables[function.name] + function_variables = self.variables[function] if variable_name not in function_variables and variable_name != '_': function_variables.append(variable_name) - limit_exceeded = has_just_exceeded_limit( - len(function_variables), - self.delegate.options.max_local_variables, - ) - if limit_exceeded: - self.delegate.add_error( - TooManyLocalsViolation(function, text=function.name), - ) - - def _update_counter( - self, - function: ast.FunctionDef, - counter: DefaultDict[str, int], - max_value: int, - exception: Type[BaseStyleViolation], - ) -> None: - counter[function.name] += 1 - limit_exceeded = has_just_exceeded_limit( - counter[function.name], max_value, + def _update_elifs(self, node: ast.FunctionDef, sub_node: ast.If) -> None: + has_elif = any( + isinstance(if_node, ast.If) for if_node in sub_node.orelse ) - if limit_exceeded: - self.delegate.add_error(exception(function, text=function.name)) - def _update_elifs(self, node: ast.If, count: int = 0) -> None: - if node.orelse and isinstance(node.orelse[0], ast.If): - self._update_elifs(node.orelse[0], count=count + 1) - else: - if count > self.delegate.options.max_elifs: - self.delegate.add_error(TooManyElifsViolation(node)) + if has_elif: + self.elifs[node] += 1 def _check_sub_node(self, node: ast.FunctionDef, sub_node) -> None: is_variable = isinstance(sub_node, ast.Name) context = getattr(sub_node, 'ctx', None) if is_variable and isinstance(context, ast.Store): - self._update_variables(node, getattr(sub_node, 'id')) - if isinstance(sub_node, ast.Return): - self._update_counter( - node, - self.returns, - self.delegate.options.max_returns, - TooManyReturnsViolation, - ) - if isinstance(sub_node, ast.Expr): - self._update_counter( - node, - self.expressions, - self.delegate.options.max_expressions, - TooManyExpressionsViolation, - ) - if isinstance(sub_node, ast.If): - self._update_elifs(sub_node) + self._update_variables(node, sub_node.id) + elif isinstance(sub_node, ast.Return): + self.returns[node] += 1 + elif isinstance(sub_node, ast.Expr): + self.expressions[node] += 1 + elif isinstance(sub_node, ast.If): + self._update_elifs(node, sub_node) def check_arguments_count(self, node: ast.FunctionDef) -> None: """Checks the number of the arguments in a function.""" @@ -103,17 +73,12 @@ def check_arguments_count(self, node: ast.FunctionDef) -> None: has_extra_arg = 1 counter += len(node.args.args) + len(node.args.kwonlyargs) - if node.args.vararg: counter += 1 - if node.args.kwarg: counter += 1 - if counter > self.delegate.options.max_arguments + has_extra_arg: - self.delegate.add_error( - TooManyArgumentsViolation(node, text=node.name), - ) + self.arguments[node] = counter - has_extra_arg def check_function_complexity(self, node: ast.FunctionDef) -> None: """ @@ -143,7 +108,41 @@ class FunctionComplexityVisitor(BaseNodeVisitor): def __init__(self, *args, **kwargs) -> None: """Creates a counter for tracked metrics.""" super().__init__(*args, **kwargs) - self._counter = _ComplexityCounter(self) + self._counter = _ComplexityCounter() + + def _check_possible_switch(self) -> None: + for node, elifs in self._counter.elifs.items(): + if elifs > self.options.max_elifs: + self.add_error(TooManyElifsViolation(node)) + + def _check_function_internals(self) -> None: + for node, variables in self._counter.variables.items(): + if len(variables) > self.options.max_local_variables: + self.add_error( + TooManyLocalsViolation(node, text=node.name), + ) + + for node, expressions in self._counter.expressions.items(): + if expressions > self.options.max_expressions: + self.add_error( + TooManyExpressionsViolation(node, text=node.name), + ) + + def _check_function_signature(self) -> None: + for node, arguments in self._counter.arguments.items(): + if arguments > self.options.max_arguments: + self.add_error( + TooManyArgumentsViolation(node, text=str(arguments)), + ) + + for node, returns in self._counter.returns.items(): + if returns > self.options.max_returns: + self.add_error(TooManyReturnsViolation(node, text=node.name)) + + def _post_visit(self) -> None: + self._check_function_signature() + self._check_function_internals() + self._check_possible_switch() def visit_FunctionDef(self, node: ast.FunctionDef) -> None: """ diff --git a/wemake_python_styleguide/visitors/ast/complexity/jones.py b/wemake_python_styleguide/visitors/ast/complexity/jones.py index 6c38151d7..0e18fd8e0 100644 --- a/wemake_python_styleguide/visitors/ast/complexity/jones.py +++ b/wemake_python_styleguide/visitors/ast/complexity/jones.py @@ -66,7 +66,7 @@ def _post_visit(self) -> None: self.add_error(JonesScoreViolation()) def _maybe_ignore_child(self, node: ast.AST) -> bool: - if isinstance(node, ast.AnnAssign): # type: ignore + if isinstance(node, ast.AnnAssign): self._to_ignore.append(node.annotation) return node in self._to_ignore diff --git a/wemake_python_styleguide/visitors/ast/general/__init__.py b/wemake_python_styleguide/visitors/ast/general/__init__.py new file mode 100644 index 000000000..40a96afc6 --- /dev/null +++ b/wemake_python_styleguide/visitors/ast/general/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/wemake_python_styleguide/visitors/ast/wrong_function_call.py b/wemake_python_styleguide/visitors/ast/general/wrong_function_call.py similarity index 100% rename from wemake_python_styleguide/visitors/ast/wrong_function_call.py rename to wemake_python_styleguide/visitors/ast/general/wrong_function_call.py diff --git a/wemake_python_styleguide/visitors/ast/wrong_import.py b/wemake_python_styleguide/visitors/ast/general/wrong_import.py similarity index 100% rename from wemake_python_styleguide/visitors/ast/wrong_import.py rename to wemake_python_styleguide/visitors/ast/general/wrong_import.py diff --git a/wemake_python_styleguide/visitors/ast/wrong_keyword.py b/wemake_python_styleguide/visitors/ast/general/wrong_keyword.py similarity index 100% rename from wemake_python_styleguide/visitors/ast/wrong_keyword.py rename to wemake_python_styleguide/visitors/ast/general/wrong_keyword.py diff --git a/wemake_python_styleguide/visitors/ast/wrong_name.py b/wemake_python_styleguide/visitors/ast/general/wrong_name.py similarity index 100% rename from wemake_python_styleguide/visitors/ast/wrong_name.py rename to wemake_python_styleguide/visitors/ast/general/wrong_name.py diff --git a/wemake_python_styleguide/visitors/ast/wrong_string.py b/wemake_python_styleguide/visitors/ast/general/wrong_string.py similarity index 100% rename from wemake_python_styleguide/visitors/ast/wrong_string.py rename to wemake_python_styleguide/visitors/ast/general/wrong_string.py diff --git a/wemake_python_styleguide/visitors/ast/wrong_contents.py b/wemake_python_styleguide/visitors/ast/wrong_module.py similarity index 100% rename from wemake_python_styleguide/visitors/ast/wrong_contents.py rename to wemake_python_styleguide/visitors/ast/wrong_module.py diff --git a/wemake_python_styleguide/visitors/base.py b/wemake_python_styleguide/visitors/base.py index a1cfc0cee..9223dcb50 100644 --- a/wemake_python_styleguide/visitors/base.py +++ b/wemake_python_styleguide/visitors/base.py @@ -1,38 +1,39 @@ # -*- coding: utf-8 -*- import ast -from typing import List +from typing import List, Type from wemake_python_styleguide import constants from wemake_python_styleguide.errors.base import BaseStyleViolation from wemake_python_styleguide.types import ConfigurationOptions -class BaseChecker(object): +class BaseVisitor(object): """ - Base class for different type of checkers. + Base class for different types of visitors. Attributes: - tree: AST tree to be checked if any. options: contains the options objects passed and parsed by ``flake8``. filename: filename passed by ``flake8``. - errors: list of errors for the specific checker. + errors: list of errors for the specific visitor. """ def __init__( self, options: ConfigurationOptions, - tree: ast.AST = None, - filename: str = 'stdin', + filename: str = constants.STDIN, ) -> None: - """Creates new instance.""" - super().__init__() + """Create base visitor instance.""" self.options = options - self.tree = tree self.filename = filename self.errors: List[BaseStyleViolation] = [] + @classmethod + def from_checker(cls: Type['BaseVisitor'], checker) -> 'BaseVisitor': + """Constructs visitor instance from the checker.""" + return cls(options=checker.options, filename=checker.filename) + def add_error(self, error: BaseStyleViolation) -> None: """Adds error to the visitor.""" self.errors.append(error) @@ -42,34 +43,58 @@ def run(self) -> None: raise NotImplementedError('Should be defined in a subclass') -class BaseNodeVisitor(ast.NodeVisitor, BaseChecker): +class BaseNodeVisitor(ast.NodeVisitor, BaseVisitor): """ - This class allows to store errors while traversing node tree. + Allows to store errors while traversing node tree. This class should be used as a base class for all ``ast`` based checkers. Method ``visit()`` is defined in ``NodeVisitor`` class. + + Attributes: + tree: ``ast`` tree to be checked. + """ + def __init__( + self, + options: ConfigurationOptions, + tree: ast.AST, + **kwargs, + ) -> None: + """Creates new ``ast`` based instance.""" + super().__init__(options, **kwargs) + self.tree = tree + + @classmethod + def from_checker( + cls: Type['BaseNodeVisitor'], + checker, + ) -> 'BaseNodeVisitor': + """Constructs visitor instance from the checker.""" + return cls( + options=checker.options, + filename=checker.filename, + tree=checker.tree, + ) + def _post_visit(self) -> None: """ - This method is executed after all nodes have been visited. + Executed after all nodes have been visited. - By default, does nothing. + By default does nothing. """ def run(self) -> None: """Runs the checking process.""" - if self.tree is None: - raise ValueError('Parsing without a defined tree') self.visit(self.tree) self._post_visit() -class BaseFilenameVisitor(BaseChecker): +class BaseFilenameVisitor(BaseVisitor): """ - This class allows to check module file names. + Allows to check module file names. - Method `visit()` is used only for API compatibility. + Has ``visit_filename()`` method that should be redefined in subclasses. """ def visit_filename(self) -> None: @@ -77,11 +102,6 @@ def visit_filename(self) -> None: raise NotImplementedError('Should be defined in a subclass') def run(self) -> None: - """ - Checks module's filename. - - If filename equals to ``STDIN`` constant then this check is ignored. - Otherwise, runs ``visit_filename()`` method. - """ + """Checks module's filename.""" if self.filename != constants.STDIN: self.visit_filename() From 12402405a181ff551ed76872cdb083f1bd4e815d Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 21 Sep 2018 17:21:16 +0300 Subject: [PATCH 2/7] Adds new filename checkers --- CHANGELOG.md | 14 ++- pyproject.lock | 84 ++++------------ pyproject.toml | 12 +-- setup.cfg | 4 - tests/test_errors.py | 4 +- tests/test_version.py | 6 +- .../test_wrong_module/test_empty_init.py | 2 +- .../test_wrong_module/test_empty_modules.py | 2 +- .../test_module_magic_name.py | 2 +- .../test_module_name_length.py | 6 +- .../test_module_pattern.py | 37 +++++++ .../test_module_underscores.py | 34 +++++++ wemake_python_styleguide/checker.py | 97 +++---------------- wemake_python_styleguide/constants.py | 6 +- wemake_python_styleguide/errors/base.py | 14 ++- wemake_python_styleguide/errors/classes.py | 14 +-- wemake_python_styleguide/errors/complexity.py | 52 +++++----- wemake_python_styleguide/errors/general.py | 56 ++++++----- wemake_python_styleguide/errors/imports.py | 20 ++-- wemake_python_styleguide/errors/modules.py | 80 +++++++++++++-- wemake_python_styleguide/logics/filenames.py | 31 +++++- wemake_python_styleguide/options/config.py | 2 +- wemake_python_styleguide/version.py | 6 +- .../ast/general/wrong_function_call.py | 6 +- .../visitors/ast/general/wrong_import.py | 6 +- .../visitors/ast/general/wrong_keyword.py | 14 ++- .../ast/{ => general}/wrong_module.py | 0 .../visitors/ast/general/wrong_name.py | 4 +- .../visitors/filenames/wrong_module_name.py | 42 +++++--- .../visitors/presets/__init__.py | 1 + .../visitors/presets/complexity.py | 30 ++++++ .../visitors/presets/general.py | 45 +++++++++ 32 files changed, 441 insertions(+), 292 deletions(-) create mode 100644 tests/test_visitors/test_filenames/test_wrong_module_name/test_module_pattern.py create mode 100644 tests/test_visitors/test_filenames/test_wrong_module_name/test_module_underscores.py rename wemake_python_styleguide/visitors/ast/{ => general}/wrong_module.py (100%) create mode 100644 wemake_python_styleguide/visitors/presets/__init__.py create mode 100644 wemake_python_styleguide/visitors/presets/complexity.py create mode 100644 wemake_python_styleguide/visitors/presets/general.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f7249da7..7fb99c66a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,18 +8,22 @@ We used to have incremental versioning before `0.1.0`. ### Features -- Adds `TooManyImportsViolation` -- Adds `--max-imports` options +- Adds `WrongModuleNamePatternViolation` + and `WrongModuleNameUnderscoresViolation` +- Adds `TooManyImportsViolation` error and `--max-imports` option +- Adds `--i-control-code` option to ignore `InitModuleHasLogicViolation` +- Adds check for underscored numbers - Adds `flake8`, `noqa`, and `type` comments check - ### Misc - Changes how many errors are generated for limits violations +- Refactors how visitors are injected into the checker, now using presets - Improves typing support - Adds `cohesion` plugin -- Adds `flake8-print` plugin -- Removes `delegate` concept +- Adds `flake8-eradicate` plugin +- Adds `flake8-print` plugin for development +- Removes `delegate` concept from the codebase ## Version 0.0.13 aka The Jones Complexity diff --git a/pyproject.lock b/pyproject.lock index 88f0e4c32..50ca730e8 100644 --- a/pyproject.lock +++ b/pyproject.lock @@ -19,15 +19,6 @@ platform = "*" python-versions = "*" version = "0.7.11" -[[package]] -category = "dev" -description = "apipkg: namespace control and lazy-import mechanism" -name = "apipkg" -optional = false -platform = "unix" -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -version = "1.5" - [[package]] category = "dev" description = "Atomic file writes." @@ -83,7 +74,7 @@ name = "cohesion" optional = false platform = "*" python-versions = "*" -version = "0.8.0" +version = "0.9.1" [[package]] category = "dev" @@ -132,7 +123,7 @@ python-versions = "*" version = "0.14" [[package]] -category = "dev" +category = "main" description = "Removes commented-out code." name = "eradicate" optional = false @@ -140,18 +131,6 @@ platform = "*" python-versions = "*" version = "0.2.1" -[[package]] -category = "dev" -description = "execnet: rapid multi-Python deployment" -name = "execnet" -optional = false -platform = "unix" -python-versions = "*" -version = "1.5.0" - -[package.dependencies] -apipkg = ">=1.4" - [[package]] category = "main" description = "the modular source code checker: pep8, pyflakes and co" @@ -254,6 +233,19 @@ flake8 = "*" flake8-polyfill = "*" pydocstyle = ">=2.1" +[[package]] +category = "main" +description = "Flake8 plugin to find commented out code" +name = "flake8-eradicate" +optional = false +platform = "*" +python-versions = ">=3.6,<4.0" +version = "0.1.0" + +[package.dependencies] +eradicate = ">=0.2.1,<0.3.0" +flake8 = ">=3.5,<4.0" + [[package]] category = "main" description = "flake8 plugin that integrates isort ." @@ -268,15 +260,6 @@ flake8 = ">=3.2.1" isort = ">=4.3.0" testfixtures = "*" -[[package]] -category = "main" -description = "A flake8 plugin for testing PEP-8 conform package and module names." -name = "flake8-module-name" -optional = false -platform = "*" -python-versions = "*" -version = "0.1.5" - [[package]] category = "main" description = "Checks for old string formatting." @@ -611,19 +594,6 @@ six = ">=1.10.0" platform = "win32" version = "*" -[[package]] -category = "dev" -description = "pytest plugin with mechanisms for caching across test runs" -name = "pytest-cache" -optional = false -platform = "UNKNOWN" -python-versions = "*" -version = "1.0" - -[package.dependencies] -execnet = ">=1.1.dev1" -pytest = ">=2.2" - [[package]] category = "dev" description = "Pytest plugin for measuring coverage." @@ -637,20 +607,6 @@ version = "2.6.0" coverage = ">=4.4" pytest = ">=2.9" -[[package]] -category = "dev" -description = "pytest plugin to check for commented out code" -name = "pytest-eradicate" -optional = false -platform = "UNKNOWN" -python-versions = "*" -version = "0.0.3" - -[package.dependencies] -eradicate = "*" -pytest = ">=2.4.2" -pytest-cache = "*" - [[package]] category = "dev" description = "pytest plugin to check FLAKE8 requirements" @@ -875,26 +831,24 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" version = "1.23" [metadata] -content-hash = "5e27d7f4144be420812ef86196796d6d99f3c2f9512ea90936e7685dd2a94aeb" +content-hash = "3f968141461a9121b7eb29ac78732b1cc97319947db2d52d9f25578a8c171847" platform = "*" python-versions = "^3.6 || ^3.7" [metadata.hashes] added-value = ["84f1dced8e72941718545e39ce141bafa95e7fda816a3b92113c45d27245acc6", "c10a1ca36daf0bddc7f6d5d52edc793b95dd5596f3443defc6dd52d26aa6d3d8"] alabaster = ["674bb3bab080f598371f4443c5008cbfeb1a5e622dd312395d2d82af2c54c456", "b63b1f4dc77c074d386752ec4a8a7517600f6c0db8cd42980cae17ab7b3275d7"] -apipkg = ["37228cda29411948b422fae072f57e31d3396d2ee1c9783775980ee9c9990af6", "58587dd4dc3daefad0487f6d9ae32b4542b185e1c36db6993290e7c41ca2b47c"] atomicwrites = ["0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0", "ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"] attrs = ["10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", "ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"] babel = ["6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669", "8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23"] certifi = ["376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638", "456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"] chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] -cohesion = ["ea3d5fb098c94281bc42727007fdf9e67b462e1cadaaa365b394f0a6723e7b32"] +cohesion = ["a20019bdf6c5345bb1ddf18124121b87b79db5a039f20b53547d9e664dab19ed"] colorama = ["463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda", "48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"] coverage = ["03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba", "0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed", "104ab3934abaf5be871a583541e8829d6c19ce7bde2923b2751e0d3ca44db60a", "15b111b6a0f46ee1a485414a52a7ad1d703bdf984e9ed3c288a4414d3871dcbd", "198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640", "1c383d2ef13ade2acc636556fd544dba6e14fa30755f26812f54300e401f98f2", "28b2191e7283f4f3568962e373b47ef7f0392993bb6660d079c62bd50fe9d162", "2eb564bbf7816a9d68dd3369a510be3327f1c618d2357fa6b1216994c2e3d508", "337ded681dd2ef9ca04ef5d93cfc87e52e09db2594c296b4a0a3662cb1b41249", "3a2184c6d797a125dca8367878d3b9a178b6fdd05fdc2d35d758c3006a1cd694", "3c79a6f7b95751cdebcd9037e4d06f8d5a9b60e4ed0cd231342aa8ad7124882a", "3d72c20bd105022d29b14a7d628462ebdc61de2f303322c0212a054352f3b287", "3eb42bf89a6be7deb64116dd1cc4b08171734d721e7a7e57ad64cc4ef29ed2f1", "4635a184d0bbe537aa185a34193898eee409332a8ccb27eea36f262566585000", "56e448f051a201c5ebbaa86a5efd0ca90d327204d8b059ab25ad0f35fbfd79f1", "5a13ea7911ff5e1796b6d5e4fbbf6952381a611209b736d48e675c2756f3f74e", "69bf008a06b76619d3c3f3b1983f5145c75a305a0fea513aca094cae5c40a8f5", "6bc583dc18d5979dc0f6cec26a8603129de0304d5ae1f17e57a12834e7235062", "701cd6093d63e6b8ad7009d8a92425428bc4d6e7ab8d75efbb665c806c1d79ba", "7608a3dd5d73cb06c531b8925e0ef8d3de31fed2544a7de6c63960a1e73ea4bc", "76ecd006d1d8f739430ec50cc872889af1f9c1b6b8f48e29941814b09b0fd3cc", "7aa36d2b844a3e4a4b356708d79fd2c260281a7390d678a10b91ca595ddc9e99", "7d3f553904b0c5c016d1dad058a7554c7ac4c91a789fca496e7d8347ad040653", "7e1fe19bd6dce69d9fd159d8e4a80a8f52101380d5d3a4d374b6d3eae0e5de9c", "8c3cb8c35ec4d9506979b4cf90ee9918bc2e49f84189d9bf5c36c0c1119c6558", "9d6dd10d49e01571bf6e147d3b505141ffc093a06756c60b053a859cb2128b1f", "9e112fcbe0148a6fa4f0a02e8d58e94470fc6cb82a5481618fea901699bf34c4", "ac4fef68da01116a5c117eba4dd46f2e06847a497de5ed1d64bb99a5fda1ef91", "b8815995e050764c8610dbc82641807d196927c3dbed207f0a079833ffcf588d", "be6cfcd8053d13f5f5eeb284aa8a814220c3da1b0078fa859011c7fffd86dab9", "c1bb572fab8208c400adaf06a8133ac0712179a334c09224fb11393e920abcdd", "de4418dadaa1c01d497e539210cb6baa015965526ff5afc078c57ca69160108d", "e05cb4d9aad6233d67e0541caa7e511fa4047ed7750ec2510d466e806e0255d6", "e4d96c07229f58cb686120f168276e434660e4358cc9cf3b0464210b04913e77", "f3f501f345f24383c0000395b26b726e46758b71393267aeae0bd36f8b3ade80", "f8a923a85cb099422ad5a2e345fe877bbc89a8a8b23235824a93488150e45f6e"] doc8 = ["2df89f9c1a5abfb98ab55d0175fed633cae0cf45025b8b1e0ee5ea772be28543", "d12f08aa77a4a65eb28752f4bc78f41f611f9412c4155e2b03f1f5d4a45efe04"] docutils = ["02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", "51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", "7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"] eradicate = ["f9af01c544ccd8f71bc2f7f3fa39dc363d842cfcb9c730a83676a59026ab5f24"] -execnet = ["a7a84d5fa07a089186a329528f127c9d73b9de57f1a1131b82bb5320ee651f6a", "fc155a6b553c66c838d1a22dba1dc9f5f505c43285a878c6f74a79c024750b83"] flake8 = ["7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0", "c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37"] flake8-bugbear = ["07b6e769d7f4e168d590f7088eae40f6ddd9fa4952bed31602def65842682c83", "0ccf56975f4db1d69dc1cf3598c99d768ebf95d0cad27d76087954aa399b515a"] flake8-builtins = ["8d806360767947c0035feada4ddef3ede32f0a586ef457e62d811b8456ad9a51", "cd7b1b7fec4905386a3643b59f9ca8e305768da14a49a7efb31fe9364f33cd04"] @@ -903,8 +857,8 @@ flake8-commas = ["d3005899466f51380387df7151fb59afec666a0f4f4a2c6a8995b975de0f44 flake8-comprehensions = ["b83891fec0e680b07aa1fd92e53eb6993be29a0f3673a09badbe8da307c445e0", "e4ccf1627f75f192eb7fde640f5edb81c98d04b1390df9d4145ffd7710bb1ef2"] flake8-debugger = ["be4fb88de3ee8f6dd5053a2d347e2c0a2b54bab6733a2280bb20ebd3c4ca1d97"] flake8-docstrings = ["4e0ce1476b64e6291520e5570cf12b05016dd4e8ae454b8a8a9a48bc5f84e1cd", "8436396b5ecad51a122a2c99ba26e5b4e623bf6e913b0fea0cb6c2c4050f91eb"] +flake8-eradicate = ["37d0daeb395584be5110253cc7260e3ecc947065f089d559a9b7219f5ddb66ff", "7a68d6e371b4602172b0f2310be427f571aa945a59c05a2abbc792e5de01c47f"] flake8-isort = ["298d7904ac3a46274edf4ce66fd7e272c2a60c34c3cc999dea000608d64e5e6e", "5992850626ce96547b1f1c7e8a7f0ef49ab2be44eca2177934566437b636fa3c"] -flake8-module-name = ["bc0a43cce6fc95215de39a0f18e06fdca160daaf63eae6926fabbb6d9458f3d2", "d155957f08c6dabd44d59ca229ca67375a34b14ee44c79097b66838dd919e5b6"] flake8-pep3101 = ["493821d6bdd083794eb0691ebe5b68e5c520b622b269d60e54308fb97440e21a", "b661ab718df42b87743dde266ef5de4f9e900b56c67dbccd45d24cf527545553"] flake8-polyfill = ["12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9", "e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"] flake8-print = ["5010e6c138b63b62400da4b06afa33becc5e08bd1fcce9af3752445cf3342f54"] @@ -935,9 +889,7 @@ pyflakes = ["08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f", pygments = ["78f3f434bcc5d6ee09020f92ba487f95ba50f1e3ef83ae96b9d5ffa1bab25c5d", "dbae1046def0efb574852fab9e90209b23f556367b5a320c0bcb871c77c3e8cc"] pyparsing = ["905d8090c335314568b5faee0025b1829f27bb974604a5762a6cdef3a7dfc3b7", "f493ee323be1e94929416b3585eefcc04943115cecbaaa35a8c86d1a2368af19"] pytest = ["453cbbbe5ce6db38717d282b758b917de84802af4288910c12442984bde7b823", "a8a07f84e680482eb51e244370aaf2caa6301ef265f37c2bdefb3dd3b663f99d"] -pytest-cache = ["be7468edd4d3d83f1e844959fd6e3fd28e77a481440a7118d430130ea31b07a9"] pytest-cov = ["513c425e931a0344944f84ea47f3956be0e416d95acbd897a44970c8d926d5d7", "e360f048b7dae3f2f2a9a4d067b2dd6b6a015d384d1577c994a43f3f7cbad762"] -pytest-eradicate = ["1dcb3fa109e697817c2421e3767e2c3d031c4d36a2180034c119b9bd30fe17a2", "7ac2e48ef1dce414a3e93b30fc7ca56ff1b00df115b3c80a7dd83238f962d551"] pytest-flake8 = ["4f30f5be3efb89755f38f11bdb2a5e22d19a6f5faa73428f703a3292a9572cd3", "c740ad6aa19e3958947d2118f70bed218caf1d2097039fb7318573a2a72f89a1"] pytest-isort = ["2221c0914dfca41914625a646f0d2d1d4c676861b9a7b26746a7fdd40aa2c59b", "c70d0f900f4647bb714f0843dd82d7f7b759904006de31254efdb72ce88e0c0e"] pytest-randomly = ["6db5e03d72b54052b9b379dc3cfa4749c19bfe4de161cf3eb24762049f4ce9be", "92ec6745d3ebdd690ecb598648748c9601f16f5afacf83ccef2b50d23e6edb7f"] diff --git a/pyproject.toml b/pyproject.toml index 7041df29c..676080014 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ authors = [ "Nikita Sobolev " ] -readme = "README.md" # Markdown files are supported +readme = "README.md" repository = "https://github.com/wemake-services/wemake-python-styleguide" homepage = "https://github.com/wemake-services/wemake-python-styleguide" @@ -24,7 +24,7 @@ keywords = [ ] classifiers = [ - "Development Status :: 3 - Alpha", + "Development Status :: 4 - Beta", "Environment :: Console", "Framework :: Flake8", "Intended Audience :: Developers", @@ -39,6 +39,7 @@ Z = "wemake_python_styleguide.checker:Checker" [tool.poetry.dependencies] python = "^3.6 || ^3.7" flake8 = "^3.5" +attrs = "^18.2" # This is a fix for issue-118 pycodestyle = "==2.3.1" @@ -50,21 +51,20 @@ flake8-comprehensions = "^1.4" flake8-docstrings = "^1.3" flake8-string-format = "^0.2" flake8-coding = "^1.3" -flake8-module-name = "^0.1" flake8-bugbear = "^18.2" flake8-pep3101 = "^1.2" flake8-super-call = "^1.0" flake8-debugger = "^3.1" flake8-isort = "^2.5" +flake8-eradicate = "^0.1" pep8-naming = "^0.7" -cohesion = "^0.8" +cohesion = "^0.9" [tool.poetry.dev-dependencies] pytest-cov = "^2.6" pytest-flake8 = "^1.0" pytest-randomly = "^1.2" pytest-isort = "^0.2" -pytest-eradicate = "^0.0.3" pytest = "^3.8" flake8-pytest = "^1.3" flake8-print = "^3.1" @@ -76,4 +76,4 @@ doc8 = "^0.8" m2r = "^0.2" sphinx_readable_theme = "^1.3" typing_extensions = "^3.6" -added-value = "^0.8.0" +added-value = "^0.8" diff --git a/setup.cfg b/setup.cfg index 8b7d651e0..ed0956879 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,10 +27,6 @@ flake8-ignore = wemake_python_styleguide/visitors/ast/*.py N802 # These modules should contain a lot of classes: wemake_python_styleguide/errors/*.py Z208 - # We can not find an easier way to define a checker for now: - wemake_python_styleguide/checker.py Z212 - # Disable filename checks for regression testing: - tests/test_checkers/test_regression/*.py N999 # Disable some pydocstyle checks: *.py D100 D104 D106 D401 diff --git a/tests/test_errors.py b/tests/test_errors.py index 1b0110354..c37b53354 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -5,7 +5,7 @@ def test_all_unique_error_codes(all_errors): """Ensures that all errors have unique error codes.""" codes = [] for error in all_errors: - codes.append(error.code) + codes.append(int(error.code)) assert len(set(codes)) == len(all_errors) @@ -13,4 +13,4 @@ def test_all_unique_error_codes(all_errors): def test_all_errors_have_description_with_code(all_errors): """Ensures that all errors have description with error code.""" for error in all_errors: - assert error.code in error.__doc__ + assert str(error.code) in error.__doc__ diff --git a/tests/test_version.py b/tests/test_version.py index ad8366a31..91fc27dd1 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -2,7 +2,7 @@ import subprocess -from wemake_python_styleguide.version import version +from wemake_python_styleguide.version import pkg_name, pkg_version def test_call_flake8_version(): @@ -13,5 +13,5 @@ def test_call_flake8_version(): ) output_text = output.decode('utf-8') - assert 'wemake-python-styleguide' in output_text - assert version in output_text + assert pkg_name in output_text + assert pkg_version in output_text diff --git a/tests/test_visitors/test_ast/test_wrong_module/test_empty_init.py b/tests/test_visitors/test_ast/test_wrong_module/test_empty_init.py index 8322307be..d9365624a 100644 --- a/tests/test_visitors/test_ast/test_wrong_module/test_empty_init.py +++ b/tests/test_visitors/test_ast/test_wrong_module/test_empty_init.py @@ -2,7 +2,7 @@ import pytest -from wemake_python_styleguide.visitors.ast.wrong_module import ( +from wemake_python_styleguide.visitors.ast.general.wrong_module import ( InitModuleHasLogicViolation, WrongContentsVisitor, ) diff --git a/tests/test_visitors/test_ast/test_wrong_module/test_empty_modules.py b/tests/test_visitors/test_ast/test_wrong_module/test_empty_modules.py index 0aa06cc3c..d63ce1124 100644 --- a/tests/test_visitors/test_ast/test_wrong_module/test_empty_modules.py +++ b/tests/test_visitors/test_ast/test_wrong_module/test_empty_modules.py @@ -2,7 +2,7 @@ import pytest -from wemake_python_styleguide.visitors.ast.wrong_module import ( +from wemake_python_styleguide.visitors.ast.general.wrong_module import ( EmptyModuleViolation, WrongContentsVisitor, ) diff --git a/tests/test_visitors/test_filenames/test_wrong_module_name/test_module_magic_name.py b/tests/test_visitors/test_filenames/test_wrong_module_name/test_module_magic_name.py index 82653448e..a5664588b 100644 --- a/tests/test_visitors/test_filenames/test_wrong_module_name/test_module_magic_name.py +++ b/tests/test_visitors/test_filenames/test_wrong_module_name/test_module_magic_name.py @@ -21,7 +21,7 @@ def test_correct_magic_filename(assert_errors, filename, default_options): @pytest.mark.parametrize('filename', [ '__version__.py', '__custom__.py', - '____.py', + '__some_extra__.py', ]) def test_simple_filename(assert_errors, filename, default_options): """Testing that some file names are restricted.""" diff --git a/tests/test_visitors/test_filenames/test_wrong_module_name/test_module_name_length.py b/tests/test_visitors/test_filenames/test_wrong_module_name/test_module_name_length.py index b028455d0..ad43264eb 100644 --- a/tests/test_visitors/test_filenames/test_wrong_module_name/test_module_name_length.py +++ b/tests/test_visitors/test_filenames/test_wrong_module_name/test_module_name_length.py @@ -4,6 +4,7 @@ from wemake_python_styleguide.visitors.filenames.wrong_module_name import ( TooShortModuleNameViolation, + WrongModuleNamePatternViolation, WrongModuleNameVisitor, ) @@ -19,7 +20,10 @@ def test_too_short_filename(assert_errors, filename, default_options): visitor = WrongModuleNameVisitor(default_options, filename=filename) visitor.run() - assert_errors(visitor, [TooShortModuleNameViolation]) + assert_errors(visitor, [ + TooShortModuleNameViolation, + WrongModuleNamePatternViolation, + ]) def test_length_option(assert_errors, options): diff --git a/tests/test_visitors/test_filenames/test_wrong_module_name/test_module_pattern.py b/tests/test_visitors/test_filenames/test_wrong_module_name/test_module_pattern.py new file mode 100644 index 000000000..62a44aa43 --- /dev/null +++ b/tests/test_visitors/test_filenames/test_wrong_module_name/test_module_pattern.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- + +import pytest + +from wemake_python_styleguide.visitors.filenames.wrong_module_name import ( + WrongModuleNamePatternViolation, + WrongModuleNameVisitor, +) + + +@pytest.mark.parametrize('filename', [ + 'my_module.py', + '_prefixed.py', + '_prefixed_with_number2.py', + 'regression_123.py', +]) +def test_simple_filename(assert_errors, filename, default_options): + """Testing that simple file names works well.""" + visitor = WrongModuleNameVisitor(default_options, filename=filename) + visitor.run() + + assert_errors(visitor, []) + + +@pytest.mark.parametrize('filename', [ + 'ending_.py', + 'MyModule.py', + '1python.py', + 'some_More.py', + 'wrong+char.py', +]) +def test_wrong_filename(assert_errors, filename, default_options): + """Testing that incorrect names are restricted.""" + visitor = WrongModuleNameVisitor(default_options, filename=filename) + visitor.run() + + assert_errors(visitor, [WrongModuleNamePatternViolation]) diff --git a/tests/test_visitors/test_filenames/test_wrong_module_name/test_module_underscores.py b/tests/test_visitors/test_filenames/test_wrong_module_name/test_module_underscores.py new file mode 100644 index 000000000..3d4c592ba --- /dev/null +++ b/tests/test_visitors/test_filenames/test_wrong_module_name/test_module_underscores.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- + +import pytest + +from wemake_python_styleguide.visitors.filenames.wrong_module_name import ( + WrongModuleNameUnderscoresViolation, + WrongModuleNameVisitor, +) + + +@pytest.mark.parametrize('filename', [ + 'some.py', + 'my_module.py', + '__init__.py', + '_compat.py', +]) +def test_correct_filename(assert_errors, filename, default_options): + """Testing that correct file names are allowed.""" + visitor = WrongModuleNameVisitor(default_options, filename=filename) + visitor.run() + + assert_errors(visitor, []) + + +@pytest.mark.parametrize('filename', [ + '__compat.py', + 'some__typo.py', +]) +def test_length_option(assert_errors, filename, default_options): + """Ensures incorrect underscores are caught.""" + visitor = WrongModuleNameVisitor(default_options, filename=filename) + visitor.run() + + assert_errors(visitor, [WrongModuleNameUnderscoresViolation]) diff --git a/wemake_python_styleguide/checker.py b/wemake_python_styleguide/checker.py index 92cd43f7c..b45f23ac4 100644 --- a/wemake_python_styleguide/checker.py +++ b/wemake_python_styleguide/checker.py @@ -6,57 +6,12 @@ from flake8.options.manager import OptionManager -from wemake_python_styleguide import constants +from wemake_python_styleguide import constants, types, version from wemake_python_styleguide.options.config import Configuration -from wemake_python_styleguide.types import ( - CheckResult, - ConfigurationOptions, - TokenVisitorSequence, - TreeVisitorSequence, - VisitorSequence, -) -from wemake_python_styleguide.version import version -from wemake_python_styleguide.visitors.ast.complexity.counts import ( - ImportMembersVisitor, - MethodMembersVisitor, - ModuleMembersVisitor, -) -from wemake_python_styleguide.visitors.ast.complexity.function import ( - FunctionComplexityVisitor, -) -from wemake_python_styleguide.visitors.ast.complexity.jones import ( - JonesComplexityVisitor, -) -from wemake_python_styleguide.visitors.ast.complexity.nested import ( - NestedComplexityVisitor, -) -from wemake_python_styleguide.visitors.ast.complexity.offset import ( - OffsetVisitor, -) -from wemake_python_styleguide.visitors.ast.general.wrong_function_call import ( - WrongFunctionCallVisitor, -) -from wemake_python_styleguide.visitors.ast.general.wrong_import import ( - WrongImportVisitor, -) -from wemake_python_styleguide.visitors.ast.general.wrong_keyword import ( - WrongKeywordVisitor, - WrongRaiseVisitor, -) -from wemake_python_styleguide.visitors.ast.general.wrong_name import ( - WrongModuleMetadataVisitor, - WrongNameVisitor, -) -from wemake_python_styleguide.visitors.ast.general.wrong_string import ( - WrongStringVisitor, -) -from wemake_python_styleguide.visitors.ast.wrong_class import WrongClassVisitor -from wemake_python_styleguide.visitors.ast.wrong_module import ( - WrongContentsVisitor, -) -from wemake_python_styleguide.visitors.filenames.wrong_module_name import ( - WrongModuleNameVisitor, +from wemake_python_styleguide.visitors.presets.complexity import ( + COMPLEXITY_PRESET, ) +from wemake_python_styleguide.visitors.presets.general import GENERAL_PRESET class Checker(object): @@ -71,41 +26,19 @@ class Checker(object): """ - name = 'wemake-python-styleguide' - version = version + name = version.pkg_name + version = version.pkg_version config = Configuration() - options: ConfigurationOptions + options: types.ConfigurationOptions #: Visitors that should be working by default: - ast_visitors: TreeVisitorSequence = ( - # General: - WrongRaiseVisitor, - WrongFunctionCallVisitor, - WrongImportVisitor, - WrongKeywordVisitor, - WrongNameVisitor, - WrongModuleMetadataVisitor, - WrongStringVisitor, - WrongContentsVisitor, - - # Complexity: - FunctionComplexityVisitor, - NestedComplexityVisitor, - OffsetVisitor, - ImportMembersVisitor, - ModuleMembersVisitor, - MethodMembersVisitor, - JonesComplexityVisitor, - - # Classes: - WrongClassVisitor, - - # Modules: - WrongModuleNameVisitor, + ast_visitors: types.TreeVisitorSequence = ( + *GENERAL_PRESET, + *COMPLEXITY_PRESET, ) - token_visitors: TokenVisitorSequence = () + token_visitors: types.TokenVisitorSequence = () def __init__( self, @@ -124,14 +57,14 @@ def add_options(cls, parser: OptionManager) -> None: cls.config.register_options(parser) @classmethod - def parse_options(cls, options: ConfigurationOptions) -> None: + def parse_options(cls, options: types.ConfigurationOptions) -> None: """Parses registered options for providing to the visitor.""" cls.options = options def _run_checks( self, - visitors: VisitorSequence, - ) -> Generator[CheckResult, None, None]: + visitors: types.VisitorSequence, + ) -> Generator[types.CheckResult, None, None]: """Runs all ``ast`` based visitors one by one.""" for visitor_class in visitors: visitor = visitor_class.from_checker(self) @@ -140,7 +73,7 @@ def _run_checks( for error in visitor.errors: yield (*error.node_items(), type(self)) - def run(self) -> Generator[CheckResult, None, None]: + def run(self) -> Generator[types.CheckResult, None, None]: """ Runs the checker. diff --git a/wemake_python_styleguide/constants.py b/wemake_python_styleguide/constants.py index 425b194d3..b9986040d 100644 --- a/wemake_python_styleguide/constants.py +++ b/wemake_python_styleguide/constants.py @@ -7,6 +7,7 @@ It also contains some exceptions that we allow to use in our codebase. """ +import re import sys from typing import Tuple @@ -128,9 +129,12 @@ '__main__', )) +#: Regex pattern to name modules: +MODULE_NAME_PATTERN = re.compile(r'^_?_?[a-z][a-z\d_]+[a-z\d](__)?$') + # Internal variables -# They are not publicly documented since they are only used internally +# They are not publicly documented since they are not used by the end user. # This variable is used as a default filename, when it is not passed by flake8: STDIN = 'stdin' diff --git a/wemake_python_styleguide/errors/base.py b/wemake_python_styleguide/errors/base.py index 1d06eefaf..f7d8713ea 100644 --- a/wemake_python_styleguide/errors/base.py +++ b/wemake_python_styleguide/errors/base.py @@ -6,7 +6,7 @@ class BaseStyleViolation(object): """ - This is a base class for all style errors. + Base class for all style errors. It basically just defines how to create any error and how to format this error later on. @@ -15,7 +15,7 @@ class BaseStyleViolation(object): """ error_template: str - code: str + code: int should_use_text: bool = True def __init__(self, node: Optional[ast.AST], text: str = None) -> None: @@ -27,14 +27,18 @@ def __init__(self, node: Optional[ast.AST], text: str = None) -> None: else: self._text = text + def _full_code(self) -> str: + """Returns fully formated code.""" + return 'Z' + str(self.code) + def message(self) -> str: """Returns error's formatted message.""" if self.should_use_text: - return self.error_template.format(self.code, self._text) - return self.error_template.format(self.code) + return self.error_template.format(self._full_code(), self._text) + return self.error_template.format(self._full_code()) def node_items(self) -> Tuple[int, int, str]: - """Returns `Tuple` to match `flake8` API format.""" + """Returns tuple to match ``flake8`` API format.""" line_number = getattr(self._node, 'lineno', 0) column_offset = getattr(self._node, 'col_offset', 0) return line_number, column_offset, self.message() diff --git a/wemake_python_styleguide/errors/classes.py b/wemake_python_styleguide/errors/classes.py index 39daeb043..925f405e6 100644 --- a/wemake_python_styleguide/errors/classes.py +++ b/wemake_python_styleguide/errors/classes.py @@ -17,14 +17,14 @@ class StaticMethodViolation(ASTStyleViolation): """ - This rule forbids to use ``@staticmethod`` decorator. + Forbids to use ``@staticmethod`` decorator. Reasoning: Static methods are not required to be inside the class. Because it even does not an access to the current instance. Solution: - Use regular methods, ``classmethods`` or raw functions instead. + Use instance methods, ``@classmethod``, or functions instead. Note: Returns Z300 as error code @@ -33,12 +33,12 @@ class StaticMethodViolation(ASTStyleViolation): should_use_text = False error_template = '{0} Found using `@staticmethod`' - code = 'Z300' + code = 300 class BadMagicMethodViolation(ASTStyleViolation): """ - This rule forbids to use some magic methods. + Forbids to use some magic methods. Reasoning: We forbid to use magic methods related to the forbidden language parts. @@ -55,12 +55,12 @@ class BadMagicMethodViolation(ASTStyleViolation): """ error_template = '{0} Found using restricted magic method "{1}"' - code = 'Z301' + code = 301 class RequiredBaseClassViolation(ASTStyleViolation): """ - This rule forbids to write classes without base classes. + Forbids to write classes without base classes. Reasoning: We just need to decide how to do it. @@ -81,4 +81,4 @@ class Some: ... """ error_template = '{0} Found class without a base class "{1}"' - code = 'Z302' + code = 302 diff --git a/wemake_python_styleguide/errors/complexity.py b/wemake_python_styleguide/errors/complexity.py index eaaaf8d62..e31e03c31 100644 --- a/wemake_python_styleguide/errors/complexity.py +++ b/wemake_python_styleguide/errors/complexity.py @@ -34,7 +34,7 @@ class NestedFunctionViolation(ASTStyleViolation): """ - This rule forbids to have nested functions. + Forbids to have nested functions. Reasoning: Nesting functions is a bad practice. @@ -69,12 +69,12 @@ def inner(): """ error_template = '{0} Found nested function "{1}"' - code = 'Z200' + code = 200 class NestedClassViolation(ASTStyleViolation): """ - This rule forbids to have nested classes. + Forbids to use nested classes. Reasoning: Nested classes are really hard to manage. @@ -107,12 +107,12 @@ class Inner(object): """ error_template = '{0} Found nested class "{1}"' - code = 'Z201' + code = 201 class TooManyLocalsViolation(ASTStyleViolation): """ - This rule forbids to have too many local variables in the unit of code. + Forbids to have too many local variables in the unit of code. Reasoning: Having too many variables in a single function is bad thing. @@ -153,12 +153,12 @@ def second_function(argument): """ error_template = '{0} Found too many local variables "{1}"' - code = 'Z202' + code = 202 class TooManyArgumentsViolation(ASTStyleViolation): """ - This rule forbids to have too many arguments for a function or method. + Forbids to have too many arguments for a function or method. Reasoning: This is an indicator of a bad design. When function requires many @@ -177,12 +177,12 @@ class TooManyArgumentsViolation(ASTStyleViolation): """ error_template = '{0} Found too many arguments "{1}"' - code = 'Z203' + code = 203 class TooManyElifsViolation(ASTStyleViolation): """ - This rule forbids to use many ``elif`` branches. + Forbids to use many ``elif`` branches. Reasoning: This rule is specifically important, because many ``elif`` @@ -203,12 +203,12 @@ class TooManyElifsViolation(ASTStyleViolation): should_use_text = False error_template = '{0} Found too many `elif` branches' - code = 'Z204' + code = 204 class TooManyReturnsViolation(ASTStyleViolation): """ - This rule forbids placing too many ``return`` statements into the function. + Forbids placing too many ``return`` statements into the function. Reasoning: When there are too many ``return`` keywords, @@ -226,12 +226,12 @@ class TooManyReturnsViolation(ASTStyleViolation): """ error_template = '{0} Found too many return statements "{1}"' - code = 'Z205' + code = 205 class TooManyExpressionsViolation(ASTStyleViolation): """ - This rule forbids putting to many expression is a unit of code. + Forbids putting to many expression is a unit of code. Reasoning: When there are too many expression it means that this specific @@ -248,12 +248,12 @@ class TooManyExpressionsViolation(ASTStyleViolation): """ error_template = '{0} Found too many expressions "{1}"' - code = 'Z206' + code = 206 class TooDeepNestingViolation(ASTStyleViolation): """ - This rule forbids nesting blocks too deep. + Forbids nesting blocks too deep. Reasoning: If nesting is too deep that indicates of a complex logic @@ -272,12 +272,12 @@ class TooDeepNestingViolation(ASTStyleViolation): """ error_template = '{0} Found too deep nesting "{1}"' - code = 'Z207' + code = 207 class TooManyModuleMembersViolation(SimpleStyleViolation): """ - This rule forbids to have many classes and functions in a single module. + Forbids to have many classes and functions in a single module. Reasoning: Having many classes and functions in a single module is a bad thing. @@ -300,12 +300,12 @@ class TooManyModuleMembersViolation(SimpleStyleViolation): should_use_text = False error_template = '{0} Found too many members' - code = 'Z208' + code = 208 class TooManyMethodsViolation(SimpleStyleViolation): """ - This rule forbids to have many methods in a single class. + Forbids to have many methods in a single class. Reasoning: Having too many methods might lead to the "God object". @@ -332,12 +332,12 @@ class TooManyMethodsViolation(SimpleStyleViolation): """ error_template = '{0} Found too many methods "{1}"' - code = 'Z209' + code = 209 class LineComplexityViolation(ASTStyleViolation): """ - This rule forbids to have complex lines. + Forbids to have complex lines. We are using Jones Complexity algorithm to count complexity. What is Jones Complexity? It is a simple yet power method to count @@ -374,12 +374,12 @@ class LineComplexityViolation(ASTStyleViolation): """ error_template = '{0} Found too complex line: {1}' - code = 'Z210' + code = 210 class JonesScoreViolation(SimpleStyleViolation): """ - This rule forbids to have modules with complex lines. + Forbids to have modules with complex lines. We are using Jones Complexity algorithm to count module's score. See @@ -405,12 +405,12 @@ class JonesScoreViolation(SimpleStyleViolation): should_use_text = False error_template = '{0} Found module with high Jones score' - code = 'Z211' + code = 211 class TooManyImportsViolation(SimpleStyleViolation): """ - This rule forbids to have modules with too many imports. + Forbids to have modules with too many imports. Namespaces are one honking great idea -- let's do more of those! @@ -442,4 +442,4 @@ class TooManyImportsViolation(SimpleStyleViolation): """ error_template = '{0} Found module with too many imports: {1}' - code = 'Z212' + code = 212 diff --git a/wemake_python_styleguide/errors/general.py b/wemake_python_styleguide/errors/general.py index 6ea0b23ac..9a2617a63 100644 --- a/wemake_python_styleguide/errors/general.py +++ b/wemake_python_styleguide/errors/general.py @@ -24,7 +24,7 @@ class WrongKeywordViolation(ASTStyleViolation): """ - This rule forbids to use some keywords from ``python``. + Forbids to use some keywords from ``python``. Reasoning: We believe, tha some keywords are anti-patterns. @@ -51,12 +51,12 @@ class WrongKeywordViolation(ASTStyleViolation): """ error_template = '{0} Found wrong keyword "{1}"' - code = 'Z110' + code = 110 class RaiseNotImplementedViolation(ASTStyleViolation): """ - This rule forbids to use ``NotImplemented`` error. + Forbids to use ``NotImplemented`` error. Reasoning: These two errors look so similar. @@ -80,13 +80,14 @@ class RaiseNotImplementedViolation(ASTStyleViolation): """ - error_template = '{0} Found raise NotImplemented "{1}"' - code = 'Z111' + should_use_text = False + error_template = '{0} Found raise NotImplemented' + code = 111 class WrongFunctionCallViolation(ASTStyleViolation): """ - This rule forbids to call some built-in functions. + Forbids to call some built-in functions. Reasoning: Some functions are only suitable @@ -103,15 +104,16 @@ class WrongFunctionCallViolation(ASTStyleViolation): """ error_template = '{0} Found wrong function call "{1}"' - code = 'Z112' + code = 112 class WrongVariableNameViolation(ASTStyleViolation): """ - This rule forbids to have blacklisted variable names. + Forbids to have blacklisted variable names. Reasoning: - Naming is hard. We have found names that are not expressive enough. + We have found some names that are not expressive enough. + However, they appear in the code more than offten. All names from ``BAD_VARIABLE_NAMES`` could be improved. Solution: @@ -136,16 +138,16 @@ class WrongVariableNameViolation(ASTStyleViolation): """ error_template = '{0} Found wrong variable name "{1}"' - code = 'Z113' + code = 113 class TooShortVariableNameViolation(ASTStyleViolation): """ - This rule forbids to have too short variable names. + Forbids to have too short variable names. Reasoning: Naming is hard. - It is hard to understand what this variable means, + It is hard to understand what the variable means and why it is used, if it's name is too short. This rule is configurable with ``--min-variable-length``. @@ -164,18 +166,19 @@ class TooShortVariableNameViolation(ASTStyleViolation): """ error_template = '{0} Found too short name "{1}"' - code = 'Z114' + code = 114 class PrivateNameViolation(ASTStyleViolation): """ - This rule forbids to have private name pattern. + Forbids to have private name pattern. Reasoning: - Naming is hard. - Private is not private in ``python``. So, why should we pretend it is? + Private is not private in ``python``. + So, why should we pretend it is? + This might lead to some serious design flaws. - This rule includes: variables, attributes, functions, and methods. + This rule checks: variables, attributes, functions, and methods. Example:: @@ -191,12 +194,12 @@ def __collect_coverage(self): ... """ error_template = '{0} Found private name pattern "{1}"' - code = 'Z115' + code = 115 class WrongModuleMetadataViolation(ASTStyleViolation): """ - This rule forbids to have some module level variables. + Forbids to have some module level variables. Reasoning: We discourage using module variables like ``__author__``, @@ -204,6 +207,7 @@ class WrongModuleMetadataViolation(ASTStyleViolation): Solution: Use proper docstrings and packaging classifiers. + Use ``pkg_resources`` if you need to import this data into your app. See :py:data:`~wemake_python_styleguide.constants.BAD_MODULE_METADATA_VARIABLES` @@ -221,12 +225,12 @@ class WrongModuleMetadataViolation(ASTStyleViolation): """ error_template = '{0} Found wrong metadata variable {1}' - code = 'Z116' + code = 116 class FormattedStringViolation(ASTStyleViolation): """ - This rule forbids to use ``f`` strings. + Forbids to use ``f`` strings. Reasoning: ``f`` strings looses context too often and they are hard to lint. @@ -254,12 +258,12 @@ class FormattedStringViolation(ASTStyleViolation): should_use_text = False error_template = '{0} Found `f` string' - code = 'Z117' + code = 117 class EmptyModuleViolation(ASTStyleViolation): """ - This rule forbids to have empty modules. + Forbids to have empty modules. Reasoning: Why is it even there? @@ -277,12 +281,12 @@ class EmptyModuleViolation(ASTStyleViolation): should_use_text = False error_template = '{0} Found empty module' - code = 'Z118' + code = 118 class InitModuleHasLogicViolation(ASTStyleViolation): """ - This rule forbids to have logic inside ``__init__`` module. + Forbids to have logic inside ``__init__`` module. Reasoning: If you have logic inside the ``__init__`` module @@ -308,4 +312,4 @@ class InitModuleHasLogicViolation(ASTStyleViolation): should_use_text = False error_template = '{0} Found `__init__` module with logic' - code = 'Z119' + code = 119 diff --git a/wemake_python_styleguide/errors/imports.py b/wemake_python_styleguide/errors/imports.py index 03d77e23a..b468a93c2 100644 --- a/wemake_python_styleguide/errors/imports.py +++ b/wemake_python_styleguide/errors/imports.py @@ -17,7 +17,7 @@ class LocalFolderImportViolation(ASTStyleViolation): """ - This rule forbids to have imports relative to the current folder. + Forbids to have imports relative to the current folder. Reasoning: We should pick one style and stick to it. @@ -38,12 +38,12 @@ class LocalFolderImportViolation(ASTStyleViolation): """ error_template = '{0} Found local folder import "{1}"' - code = 'Z100' + code = 100 class NestedImportViolation(ASTStyleViolation): """ - This rule forbids to have nested imports in functions. + Forbids to have nested imports in functions. Reasoning: Usually nested imports are used to fix the import cycle. @@ -69,12 +69,12 @@ def some(): """ error_template = '{0} Found nested import "{1}"' - code = 'Z101' + code = 101 class FutureImportViolation(ASTStyleViolation): """ - This rule forbids to use ``__future__`` imports. + Forbids to use ``__future__`` imports. Reasoning: Almost all ``__future__`` imports are legacy ``python2`` compatibility @@ -102,12 +102,12 @@ class FutureImportViolation(ASTStyleViolation): """ error_template = '{0} Found future import "{1}"' - code = 'Z102' + code = 102 class DottedRawImportViolation(ASTStyleViolation): """ - This rule forbids to use imports like ``import os.path``. + Forbids to use imports like ``import os.path``. Reasoning: We should pick one style and stick to it. @@ -127,12 +127,12 @@ class DottedRawImportViolation(ASTStyleViolation): """ error_template = '{0} Found dotted raw import "{1}"' - code = 'Z103' + code = 103 class SameAliasImportViolation(ASTStyleViolation): """ - This rule forbids to use the same alias as the original name in imports. + Forbids to use the same alias as the original name in imports. Reasoning: Why would you even do this in the first place? @@ -151,4 +151,4 @@ class SameAliasImportViolation(ASTStyleViolation): """ error_template = '{0} Found same alias import "{1}"' - code = 'Z104' + code = 104 diff --git a/wemake_python_styleguide/errors/modules.py b/wemake_python_styleguide/errors/modules.py index 90e49fd6d..703912edf 100644 --- a/wemake_python_styleguide/errors/modules.py +++ b/wemake_python_styleguide/errors/modules.py @@ -17,10 +17,9 @@ class WrongModuleNameViolation(SimpleStyleViolation): """ - This rule forbids to use blacklisted module names. + Forbids to use blacklisted module names. Reasoning: - Naming is hard. Some module names are not expressive enough. It is hard to tell what you can find inside the ``utils.py`` module. @@ -48,15 +47,14 @@ class WrongModuleNameViolation(SimpleStyleViolation): should_use_text = False error_template = '{0} Found wrong module name' - code = 'Z400' + code = 400 class WrongModuleMagicNameViolation(SimpleStyleViolation): """ - This rule forbids to use any magic names except whitelisted ones. + Forbids to use any magic names except whitelisted ones. Reasoning: - Naming is hard. Do not fall in love with magic. There's no good reason to use magic names, when you can use regular names. @@ -80,15 +78,14 @@ class WrongModuleMagicNameViolation(SimpleStyleViolation): should_use_text = False error_template = '{0} Found wrong module magic name' - code = 'Z401' + code = 401 class TooShortModuleNameViolation(SimpleStyleViolation): """ - This rule forbids to use module name shorter than some breakpoint. + Forbids to use module name shorter than some breakpoint. Reasoning: - Naming is hard. Too short module names are not expressive enough. We will have to open the code to find out what is going on there. @@ -104,4 +101,69 @@ class TooShortModuleNameViolation(SimpleStyleViolation): should_use_text = False error_template = '{0} Found too short module name' - code = 'Z402' + code = 402 + + +class WrongModuleNameUnderscoresViolation(SimpleStyleViolation): + """ + Forbids to use multiple underscores in a row in a module name. + + Reasoning: + It is hard to tell how many underscores are there: two or three? + + Solution: + Keep just one underscore in a module name. + + Example:: + # Correct: + __init__.py + some_module_name.py + test.py + + # Wrong: + some__wrong__name.py + my__module.py + __fake__magic__.py + + Note: + Returns Z403 as error code + + """ + + should_use_text = False + error_template = '{0} Found repeating underscores in a module name' + code = 403 + + +class WrongModuleNamePatternViolation(SimpleStyleViolation): + """ + Forbids to use module names that do not match our pattern. + + Reasoning: + Just like the variable names - module names should be consistent. + Ideally, they should follow the same rules. + For ``python`` world it is common to use `snake_case` notation. + + We use + :py:data:`~wemake_python_styleguide.constants.MODULE_NAME_PATTERN` + to validate the module names. + + Example:: + # Correct: + __init__.py + some_module_name.py + test12.py + + # Wrong: + _some.py + MyModule.py + 0001_migration.py + + Note: + Returns Z404 as error code + + """ + + should_use_text = False + error_template = '{0} Found incorrect module name pattern' + code = 404 diff --git a/wemake_python_styleguide/logics/filenames.py b/wemake_python_styleguide/logics/filenames.py index e1993f40b..410d4eddf 100644 --- a/wemake_python_styleguide/logics/filenames.py +++ b/wemake_python_styleguide/logics/filenames.py @@ -2,8 +2,10 @@ from pathlib import PurePath from typing import Iterable +from typing.re import Pattern -from wemake_python_styleguide.options.defaults import MIN_MODULE_NAME_LENGTH +from wemake_python_styleguide import constants +from wemake_python_styleguide.options import defaults def _get_stem(file_path: str) -> str: @@ -56,7 +58,7 @@ def is_magic(file_path: str) -> bool: def is_too_short_stem( file_path: str, - min_length: int = MIN_MODULE_NAME_LENGTH, + min_length: int = defaults.MIN_MODULE_NAME_LENGTH, ) -> bool: """ Checks either the file's stem fits into the minimum length. @@ -76,3 +78,28 @@ def is_too_short_stem( """ stem = _get_stem(file_path) return len(stem) < min_length + + +def is_matching_pattern( + file_path: str, + pattern: Pattern = constants.MODULE_NAME_PATTERN, +) -> bool: + r""" + Checks either the file's stem matches the given pattern or not. + + >>> is_matching_pattern('some.py') + True + + >>> is_matching_pattern('__init__.py') + True + + >>> is_matching_pattern('MyModule.py') + False + + >>> import re + >>> is_matching_pattern('123.py', pattern=re.compile(r'\d{3}')) + True + + """ + stem = _get_stem(file_path) + return pattern.match(stem) is not None diff --git a/wemake_python_styleguide/options/config.py b/wemake_python_styleguide/options/config.py index c04fd0af2..2978cc042 100644 --- a/wemake_python_styleguide/options/config.py +++ b/wemake_python_styleguide/options/config.py @@ -12,7 +12,7 @@ @attr.attrs(frozen=True, auto_attribs=True, slots=True) class _Option(object): - """This class represent `flake8` option object.""" + """Represents ``flake8`` option object.""" long_option_name: str default: int # noqa: E704 diff --git a/wemake_python_styleguide/version.py b/wemake_python_styleguide/version.py index ec4f990bb..c4f300c81 100644 --- a/wemake_python_styleguide/version.py +++ b/wemake_python_styleguide/version.py @@ -2,7 +2,7 @@ import pkg_resources +pkg_name = 'wemake-python-styleguide' + #: We store the version number inside the `pyproject.toml`: -version: str = pkg_resources.get_distribution( - 'wemake-python-styleguide', -).version +pkg_version: str = pkg_resources.get_distribution(pkg_name).version diff --git a/wemake_python_styleguide/visitors/ast/general/wrong_function_call.py b/wemake_python_styleguide/visitors/ast/general/wrong_function_call.py index 046eb83dc..3b1907a09 100644 --- a/wemake_python_styleguide/visitors/ast/general/wrong_function_call.py +++ b/wemake_python_styleguide/visitors/ast/general/wrong_function_call.py @@ -10,14 +10,14 @@ class WrongFunctionCallVisitor(BaseNodeVisitor): """ - This class is responsible for restricting some dangerous function calls. + Responsible for restricting some dangerous function calls. - All these functions are defined in `BAD_FUNCTIONS`. + All these functions are defined in ``BAD_FUNCTIONS``. """ def visit_Call(self, node: ast.Call) -> None: """ - Used to find `BAD_FUNCTIONS` calls. + Used to find ``BAD_FUNCTIONS`` calls. Raises: WrongFunctionCallViolation diff --git a/wemake_python_styleguide/visitors/ast/general/wrong_import.py b/wemake_python_styleguide/visitors/ast/general/wrong_import.py index 97a43c29a..46d12cd48 100644 --- a/wemake_python_styleguide/visitors/ast/general/wrong_import.py +++ b/wemake_python_styleguide/visitors/ast/general/wrong_import.py @@ -57,7 +57,7 @@ def check_alias(self, node: AnyImport) -> None: class WrongImportVisitor(BaseNodeVisitor): - """This class is responsible for finding wrong imports.""" + """Responsible for finding wrong imports.""" def __init__(self, *args, **kwargs) -> None: """Creates a checker for tracked violations.""" @@ -66,7 +66,7 @@ def __init__(self, *args, **kwargs) -> None: def visit_Import(self, node: ast.Import) -> None: """ - Used to find wrong `import` statements. + Used to find wrong ``import`` statements. Raises: SameAliasImportViolation @@ -81,7 +81,7 @@ def visit_Import(self, node: ast.Import) -> None: def visit_ImportFrom(self, node: ast.ImportFrom) -> None: """ - Used to find wrong `from import` statements. + Used to find wrong ``from ... import ...`` statements. Raises: SameAliasImportViolation diff --git a/wemake_python_styleguide/visitors/ast/general/wrong_keyword.py b/wemake_python_styleguide/visitors/ast/general/wrong_keyword.py index 239f9d251..c6504b877 100644 --- a/wemake_python_styleguide/visitors/ast/general/wrong_keyword.py +++ b/wemake_python_styleguide/visitors/ast/general/wrong_keyword.py @@ -23,13 +23,11 @@ def _check_exception_type(self, node: ast.Raise) -> None: exception_name = getattr(exception, 'id', None) if exception_name == 'NotImplemented': - self.add_error( - RaiseNotImplementedViolation(node, text=exception_name), - ) + self.add_error(RaiseNotImplementedViolation(node)) def visit_Raise(self, node: ast.Raise) -> None: """ - Checks how `raise` keyword is used. + Checks how ``raise`` keyword is used. Raises: RaiseNotImplementedViolation @@ -44,7 +42,7 @@ class WrongKeywordVisitor(BaseNodeVisitor): def visit_Global(self, node: ast.Global) -> None: """ - Used to find `global` keyword. + Used to find ``global`` keyword. Raises: WrongKeywordViolation @@ -55,7 +53,7 @@ def visit_Global(self, node: ast.Global) -> None: def visit_Nonlocal(self, node: ast.Nonlocal) -> None: """ - Used to find `nonlocal` keyword. + Used to find ``nonlocal`` keyword. Raises: WrongKeywordViolation @@ -66,7 +64,7 @@ def visit_Nonlocal(self, node: ast.Nonlocal) -> None: def visit_Delete(self, node: ast.Delete) -> None: """ - Used to find `del` keyword. + Used to find ``del`` keyword. Raises: WrongKeywordViolation @@ -77,7 +75,7 @@ def visit_Delete(self, node: ast.Delete) -> None: def visit_Pass(self, node: ast.Pass) -> None: """ - Used to find `pass` keyword. + Used to find ``pass`` keyword. Raises: WrongKeywordViolation diff --git a/wemake_python_styleguide/visitors/ast/wrong_module.py b/wemake_python_styleguide/visitors/ast/general/wrong_module.py similarity index 100% rename from wemake_python_styleguide/visitors/ast/wrong_module.py rename to wemake_python_styleguide/visitors/ast/general/wrong_module.py diff --git a/wemake_python_styleguide/visitors/ast/general/wrong_name.py b/wemake_python_styleguide/visitors/ast/general/wrong_name.py index f5a2e8b15..31d82d5ad 100644 --- a/wemake_python_styleguide/visitors/ast/general/wrong_name.py +++ b/wemake_python_styleguide/visitors/ast/general/wrong_name.py @@ -23,7 +23,7 @@ class WrongNameVisitor(BaseNodeVisitor): """ - This class performs checks based on variable names. + Performs checks based on variable names. It is responsible for finding short and blacklisted variables, functions, and arguments. @@ -130,7 +130,7 @@ def visit_Import(self, node: AnyImport) -> None: class WrongModuleMetadataVisitor(BaseNodeVisitor): - """This class finds wrong metadata information of a module.""" + """Finds wrong metadata information of a module.""" def _check_metadata(self, node: ast.Assign) -> None: node_parent = getattr(node, 'parent', None) diff --git a/wemake_python_styleguide/visitors/filenames/wrong_module_name.py b/wemake_python_styleguide/visitors/filenames/wrong_module_name.py index d815ca887..944092b71 100644 --- a/wemake_python_styleguide/visitors/filenames/wrong_module_name.py +++ b/wemake_python_styleguide/visitors/filenames/wrong_module_name.py @@ -1,19 +1,14 @@ # -*- coding: utf-8 -*- -from wemake_python_styleguide.constants import ( - BAD_MODULE_NAMES, - MAGIC_MODULE_NAMES_WHITELIST, -) +from wemake_python_styleguide import constants from wemake_python_styleguide.errors.modules import ( TooShortModuleNameViolation, WrongModuleMagicNameViolation, + WrongModuleNamePatternViolation, + WrongModuleNameUnderscoresViolation, WrongModuleNameViolation, ) -from wemake_python_styleguide.logics.filenames import ( - is_magic, - is_stem_in_list, - is_too_short_stem, -) +from wemake_python_styleguide.logics import filenames from wemake_python_styleguide.visitors.base import BaseFilenameVisitor @@ -21,26 +16,41 @@ class WrongModuleNameVisitor(BaseFilenameVisitor): """Checks that modules have correct names.""" def _check_module_name(self) -> None: - if is_stem_in_list(self.filename, BAD_MODULE_NAMES): + is_wrong_name = filenames.is_stem_in_list( + self.filename, + constants.BAD_MODULE_NAMES, + ) + if is_wrong_name: self.add_error(WrongModuleNameViolation()) def _check_magic_name(self) -> None: - if is_magic(self.filename): - good_magic = is_stem_in_list( + if filenames.is_magic(self.filename): + good_magic = filenames.is_stem_in_list( self.filename, - MAGIC_MODULE_NAMES_WHITELIST, + constants.MAGIC_MODULE_NAMES_WHITELIST, ) if not good_magic: self.add_error(WrongModuleMagicNameViolation()) def _check_module_name_length(self) -> None: - is_short = is_too_short_stem( + is_short = filenames.is_too_short_stem( self.filename, min_length=self.options.min_module_name_length, ) if is_short: self.add_error(TooShortModuleNameViolation()) + def _check_module_name_pattern(self) -> None: + if not filenames.is_matching_pattern(self.filename): + self.add_error(WrongModuleNamePatternViolation()) + + def _check_underscores(self) -> None: + repeating_underscores = self.filename.count('__') + if filenames.is_magic(self.filename): + repeating_underscores -= 2 + if repeating_underscores > 0: + self.add_error(WrongModuleNameUnderscoresViolation()) + def visit_filename(self) -> None: """ Checks a single module's filename. @@ -49,8 +59,12 @@ def visit_filename(self) -> None: TooShortModuleNameViolation WrongModuleMagicNameViolation WrongModuleNameViolation + WrongModuleNamePatternViolation + WrongModuleNameUnderscoresViolation """ self._check_module_name() self._check_magic_name() self._check_module_name_length() + self._check_module_name_pattern() + self._check_underscores() diff --git a/wemake_python_styleguide/visitors/presets/__init__.py b/wemake_python_styleguide/visitors/presets/__init__.py new file mode 100644 index 000000000..40a96afc6 --- /dev/null +++ b/wemake_python_styleguide/visitors/presets/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/wemake_python_styleguide/visitors/presets/complexity.py b/wemake_python_styleguide/visitors/presets/complexity.py new file mode 100644 index 000000000..b67f5acb2 --- /dev/null +++ b/wemake_python_styleguide/visitors/presets/complexity.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- + +from wemake_python_styleguide.visitors.ast.complexity.counts import ( + ImportMembersVisitor, + MethodMembersVisitor, + ModuleMembersVisitor, +) +from wemake_python_styleguide.visitors.ast.complexity.function import ( + FunctionComplexityVisitor, +) +from wemake_python_styleguide.visitors.ast.complexity.jones import ( + JonesComplexityVisitor, +) +from wemake_python_styleguide.visitors.ast.complexity.nested import ( + NestedComplexityVisitor, +) +from wemake_python_styleguide.visitors.ast.complexity.offset import ( + OffsetVisitor, +) + +#: Used to store all complexity related visitors to be later passed to checker: +COMPLEXITY_PRESET = ( + FunctionComplexityVisitor, + NestedComplexityVisitor, + OffsetVisitor, + ImportMembersVisitor, + ModuleMembersVisitor, + MethodMembersVisitor, + JonesComplexityVisitor, +) diff --git a/wemake_python_styleguide/visitors/presets/general.py b/wemake_python_styleguide/visitors/presets/general.py new file mode 100644 index 000000000..527709532 --- /dev/null +++ b/wemake_python_styleguide/visitors/presets/general.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +from wemake_python_styleguide.visitors.ast.general.wrong_function_call import ( + WrongFunctionCallVisitor, +) +from wemake_python_styleguide.visitors.ast.general.wrong_import import ( + WrongImportVisitor, +) +from wemake_python_styleguide.visitors.ast.general.wrong_keyword import ( + WrongKeywordVisitor, + WrongRaiseVisitor, +) +from wemake_python_styleguide.visitors.ast.general.wrong_module import ( + WrongContentsVisitor, +) +from wemake_python_styleguide.visitors.ast.general.wrong_name import ( + WrongModuleMetadataVisitor, + WrongNameVisitor, +) +from wemake_python_styleguide.visitors.ast.general.wrong_string import ( + WrongStringVisitor, +) +from wemake_python_styleguide.visitors.ast.wrong_class import WrongClassVisitor +from wemake_python_styleguide.visitors.filenames.wrong_module_name import ( + WrongModuleNameVisitor, +) + +#: Used to store all general visitors to be later passed to checker: +GENERAL_PRESET = ( + # General: + WrongRaiseVisitor, + WrongFunctionCallVisitor, + WrongImportVisitor, + WrongKeywordVisitor, + WrongNameVisitor, + WrongModuleMetadataVisitor, + WrongStringVisitor, + WrongContentsVisitor, + + # Classes: + WrongClassVisitor, + + # Modules: + WrongModuleNameVisitor, +) From b69437dbe0d068b44dedbe98871045106782f906 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 21 Sep 2018 18:04:34 +0300 Subject: [PATCH 3/7] Adds --i-control-code option --- CHANGELOG.md | 1 + .../test_wrong_module/test_empty_init.py | 22 +++++++++++++++++ .../test_wrong_module/test_empty_modules.py | 0 wemake_python_styleguide/options/config.py | 24 +++++++++++++++---- wemake_python_styleguide/options/defaults.py | 3 +++ wemake_python_styleguide/types.py | 5 ++-- .../visitors/ast/general/wrong_module.py | 12 ++++++---- 7 files changed, 56 insertions(+), 11 deletions(-) rename tests/test_visitors/test_ast/{ => test_general}/test_wrong_module/test_empty_init.py (74%) rename tests/test_visitors/test_ast/{ => test_general}/test_wrong_module/test_empty_modules.py (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fb99c66a..0bb75898d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ We used to have incremental versioning before `0.1.0`. - Adds `TooManyImportsViolation` error and `--max-imports` option - Adds `--i-control-code` option to ignore `InitModuleHasLogicViolation` - Adds check for underscored numbers +- Forbids `u''` strings - Adds `flake8`, `noqa`, and `type` comments check ### Misc diff --git a/tests/test_visitors/test_ast/test_wrong_module/test_empty_init.py b/tests/test_visitors/test_ast/test_general/test_wrong_module/test_empty_init.py similarity index 74% rename from tests/test_visitors/test_ast/test_wrong_module/test_empty_init.py rename to tests/test_visitors/test_ast/test_general/test_wrong_module/test_empty_init.py index d9365624a..3570df6a5 100644 --- a/tests/test_visitors/test_ast/test_wrong_module/test_empty_init.py +++ b/tests/test_visitors/test_ast/test_general/test_wrong_module/test_empty_init.py @@ -70,3 +70,25 @@ def test_init_with_logic( visitor.run() assert_errors(visitor, [InitModuleHasLogicViolation]) + + +@pytest.mark.parametrize('code', [ + module_with_imports, + module_with_one_import, + module_with_logic, +]) +def test_init_with_logic_without_control( + assert_errors, parse_ast_tree, code, options, +): + """Testing that `__init__` with logic is restricted.""" + tree = parse_ast_tree(code) + + option_values = options(i_control_code=False) + visitor = WrongContentsVisitor( + option_values, + tree=tree, + filename='__init__.py', + ) + visitor.run() + + assert_errors(visitor, []) diff --git a/tests/test_visitors/test_ast/test_wrong_module/test_empty_modules.py b/tests/test_visitors/test_ast/test_general/test_wrong_module/test_empty_modules.py similarity index 100% rename from tests/test_visitors/test_ast/test_wrong_module/test_empty_modules.py rename to tests/test_visitors/test_ast/test_general/test_wrong_module/test_empty_modules.py diff --git a/wemake_python_styleguide/options/config.py b/wemake_python_styleguide/options/config.py index 2978cc042..7c33e96f0 100644 --- a/wemake_python_styleguide/options/config.py +++ b/wemake_python_styleguide/options/config.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from typing import Dict, Sequence, Union +from typing import Dict, Optional, Sequence, Union import attr from flake8.options.manager import OptionManager @@ -17,16 +17,19 @@ class _Option(object): long_option_name: str default: int # noqa: E704 help: str - type: str = 'int' # noqa: A003 + type: Optional[str] = 'int' # noqa: A003 parse_from_config: bool = True + action: str = 'store' class Configuration(object): """ - Provides configuration options for ``wemake-python-styleguide`` plugin. + Provides configuration options for our plugin. We do not like our linter to be configurable. Since people may take the wrong path or make wrong decisions. + We try to make all defaults as reasonable as possible. + However, you can adjust some options via CLI option: Example:: @@ -47,8 +50,11 @@ class Configuration(object): - ``min-variable-length`` - minimum number of chars to define a valid variable name, defaults to :str:`wemake_python_styleguide.options.defaults.MIN_VARIABLE_LENGTH` + - ``i-control-code`` - either or not your control ones who use your code, + more rule are enforced when you do control it, defaults to + :str:`wemake_python_styleguide.options.defaults.I_CONTROL_CODE` - Options for module related checks: + Options for module names related checks: - ``min-module-name-length`` - minimum required module's name length, defaults to @@ -167,7 +173,15 @@ class Configuration(object): 'Minimum required length of the variable name.', ), - # Modules: + _Option( + '--i-control-code', + defaults.I_CONTROL_CODE, + 'Either or not you control ones who use your code.', + action='store_true', + type=None, + ), + + # File names: _Option( '--min-module-name-length', diff --git a/wemake_python_styleguide/options/defaults.py b/wemake_python_styleguide/options/defaults.py index e4ebdca81..cba2d90c3 100644 --- a/wemake_python_styleguide/options/defaults.py +++ b/wemake_python_styleguide/options/defaults.py @@ -19,6 +19,9 @@ #: Minimum variable's name length: MIN_VARIABLE_LENGTH = 2 +#: Either or not you control ones who use your code: +I_CONTROL_CODE = True + # Complexity diff --git a/wemake_python_styleguide/types.py b/wemake_python_styleguide/types.py index b142f9fe5..be280b06c 100644 --- a/wemake_python_styleguide/types.py +++ b/wemake_python_styleguide/types.py @@ -46,7 +46,7 @@ class ConfigurationOptions(Protocol): """ This class provides structure for the options we use in our checker. - It uses structural subtyping, and does not represent any kind of a real + It uses structural sub-typing, and does not represent any kind of a real class or structure. See: https://mypy.readthedocs.io/en/latest/protocols.html @@ -54,6 +54,7 @@ class or structure. # General: min_variable_length: int + i_control_code: bool # Complexity: max_arguments: int @@ -68,5 +69,5 @@ class or structure. max_jones_score: int max_imports: int - # Modules: + # File names: min_module_name_length: int diff --git a/wemake_python_styleguide/visitors/ast/general/wrong_module.py b/wemake_python_styleguide/visitors/ast/general/wrong_module.py index e5666265d..28b7ad012 100644 --- a/wemake_python_styleguide/visitors/ast/general/wrong_module.py +++ b/wemake_python_styleguide/visitors/ast/general/wrong_module.py @@ -17,7 +17,7 @@ class WrongContentsVisitor(BaseNodeVisitor): def _is_init(self) -> bool: return is_stem_in_list(self.filename, [INIT]) - def _is_doc_string(self, node: ast.stmt) -> bool: + def _is_doc_string(self, node: ast.stmt) -> bool: # TODO: move if not isinstance(node, ast.Expr): return False return isinstance(node.value, ast.Str) @@ -32,6 +32,9 @@ def _check_init_contents(self, node: ast.Module) -> None: if not self._is_init() or not node.body: return + if not self.options.i_control_code: + return + if len(node.body) > 1: self.add_error(InitModuleHasLogicViolation(node)) return @@ -43,9 +46,10 @@ def visit_Module(self, node: ast.Module) -> None: """ Checks that module has something other than module definition. - We have completely different rules for `__init__.py` and regular files. - Since, we believe that `__init__.py` must be empty. - But, other files must not be empty. + We have completely different rules + for ``__init__.py`` and regular files. + Since, we believe that ``__init__.py`` must be empty. + But, other files must have contents. Raises: EmptyModuleViolation From 062bdf939b71a042e16ecf9ec9bd0ed34f6b0d0f Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 21 Sep 2018 23:23:37 +0300 Subject: [PATCH 4/7] Adds tokenize visitors --- CHANGELOG.md | 1 + tests/fixtures/noqa.py | 4 ++ tests/test_errors.py | 7 +++ tests/test_visitors/conftest.py | 35 +---------- tests/test_visitors/test_ast/conftest.py | 35 +++++++++++ tests/test_visitors/test_tokenize/conftest.py | 16 +++++ .../test_underscored_numbers.py | 49 +++++++++++++++ .../test_unicode_prefix.py | 49 +++++++++++++++ wemake_python_styleguide/checker.py | 19 +++--- wemake_python_styleguide/errors/base.py | 42 +++++++++---- wemake_python_styleguide/errors/tokens.py | 62 +++++++++++++++++++ wemake_python_styleguide/types.py | 2 +- wemake_python_styleguide/visitors/base.py | 43 ++++++++++++- .../visitors/presets/tokens.py | 9 +++ .../visitors/tokenize/__init__.py | 1 + .../visitors/tokenize/wrong_primitives.py | 40 ++++++++++++ 16 files changed, 359 insertions(+), 55 deletions(-) create mode 100644 tests/test_visitors/test_ast/conftest.py create mode 100644 tests/test_visitors/test_tokenize/conftest.py create mode 100644 tests/test_visitors/test_tokenize/test_wrong_primitives/test_underscored_numbers.py create mode 100644 tests/test_visitors/test_tokenize/test_wrong_primitives/test_unicode_prefix.py create mode 100644 wemake_python_styleguide/errors/tokens.py create mode 100644 wemake_python_styleguide/visitors/presets/tokens.py create mode 100644 wemake_python_styleguide/visitors/tokenize/__init__.py create mode 100644 wemake_python_styleguide/visitors/tokenize/wrong_primitives.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bb75898d..2d148227c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ We used to have incremental versioning before `0.1.0`. - Changes how many errors are generated for limits violations - Refactors how visitors are injected into the checker, now using presets +- Creates new visitor type: `BaseTokenVisitor` for working with `tokenize` - Improves typing support - Adds `cohesion` plugin - Adds `flake8-eradicate` plugin diff --git a/tests/fixtures/noqa.py b/tests/fixtures/noqa.py index d63a4f79c..fd0136f07 100644 --- a/tests/fixtures/noqa.py +++ b/tests/fixtures/noqa.py @@ -9,6 +9,10 @@ from .version import get_version # noqa: Z100 +full_name = u'Nikita Sobolev' # noqa: Z001 +phone_number = 555_123_999 # noqa: Z002 + + def some(): from my_module import some_function # noqa: Z101 diff --git a/tests/test_errors.py b/tests/test_errors.py index c37b53354..9a5faf33a 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -1,5 +1,12 @@ # -*- coding: utf-8 -*- +from wemake_python_styleguide.errors.base import BaseStyleViolation + + +def test_checker_default_location(): + """Ensures that `BaseStyleViolation` returns correct location.""" + assert BaseStyleViolation(None)._location() == (0, 0) + def test_all_unique_error_codes(all_errors): """Ensures that all errors have unique error codes.""" diff --git a/tests/test_visitors/conftest.py b/tests/test_visitors/conftest.py index 9e6503eee..84adf0f7e 100644 --- a/tests/test_visitors/conftest.py +++ b/tests/test_visitors/conftest.py @@ -1,53 +1,22 @@ # -*- coding: utf-8 -*- -import ast from collections import namedtuple -from textwrap import dedent from typing import Sequence import pytest from wemake_python_styleguide.options.config import Configuration -from wemake_python_styleguide.visitors.base import BaseNodeVisitor - - -def _maybe_set_parent(tree: ast.AST) -> ast.AST: - """ - Sets parents for all nodes that do not have this prop. - - This step is required due to how `flake8` works. - It does not set the same properties as `ast` module. - - This function was the cause of `issue-112`. - - Version changed: 0.0.11 - - """ - for statement in ast.walk(tree): - for child in ast.iter_child_nodes(statement): - if not hasattr(child, 'parent'): # noqa: Z112 - setattr(child, 'parent', statement) - - return tree +from wemake_python_styleguide.visitors.base import BaseVisitor def _to_dest_option(long_option_name: str) -> str: return long_option_name[2:].replace('-', '_') -@pytest.fixture(scope='session') -def parse_ast_tree(): - """Helper function to convert code to ast.""" - def factory(code: str) -> ast.AST: - return _maybe_set_parent(ast.parse(dedent(code))) - - return factory - - @pytest.fixture(scope='session') def assert_errors(): """Helper function to assert visitor errors.""" - def factory(visitor: BaseNodeVisitor, errors: Sequence[str]): + def factory(visitor: BaseVisitor, errors: Sequence[str]): for index, error in enumerate(visitor.errors): assert len(errors) > index, visitor.errors assert error.code == errors[index].code diff --git a/tests/test_visitors/test_ast/conftest.py b/tests/test_visitors/test_ast/conftest.py new file mode 100644 index 000000000..76abdbb11 --- /dev/null +++ b/tests/test_visitors/test_ast/conftest.py @@ -0,0 +1,35 @@ +# -*- coding: utf-8 -*- + +import ast +from textwrap import dedent + +import pytest + + +def _maybe_set_parent(tree: ast.AST) -> ast.AST: + """ + Sets parents for all nodes that do not have this prop. + + This step is required due to how `flake8` works. + It does not set the same properties as `ast` module. + + This function was the cause of `issue-112`. + + .. versionchanged:: 0.0.11 + + """ + for statement in ast.walk(tree): + for child in ast.iter_child_nodes(statement): + if not hasattr(child, 'parent'): # noqa: Z112 + setattr(child, 'parent', statement) + + return tree + + +@pytest.fixture(scope='session') +def parse_ast_tree(): + """Helper function to convert code to ast.""" + def factory(code: str) -> ast.AST: + return _maybe_set_parent(ast.parse(dedent(code))) + + return factory diff --git a/tests/test_visitors/test_tokenize/conftest.py b/tests/test_visitors/test_tokenize/conftest.py new file mode 100644 index 000000000..f2fcccf26 --- /dev/null +++ b/tests/test_visitors/test_tokenize/conftest.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +import tokenize +from io import BytesIO +from textwrap import dedent + +import pytest + + +@pytest.fixture(scope='session') +def parse_tokens(): + """Parses tokens from a string.""" + def factory(code: str): + buffer = BytesIO(dedent(code).encode('utf-8')) + return list(tokenize.tokenize(buffer.readline)) + return factory diff --git a/tests/test_visitors/test_tokenize/test_wrong_primitives/test_underscored_numbers.py b/tests/test_visitors/test_tokenize/test_wrong_primitives/test_underscored_numbers.py new file mode 100644 index 000000000..0b1de81c0 --- /dev/null +++ b/tests/test_visitors/test_tokenize/test_wrong_primitives/test_underscored_numbers.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- + +import pytest + +from wemake_python_styleguide.visitors.tokenize.wrong_primitives import ( + UnderscoredNumberViolation, + WrongPrimitivesVisitor, +) + + +@pytest.mark.parametrize('code', [ + 'x = 10_00', + 'print(333_555)', + '3_3 + 55', +]) +def test_underscored_number( + parse_tokens, + assert_errors, + default_options, + code, +): + """Ensures that underscored numbers raise a warning.""" + file_tokens = parse_tokens(code) + + visitor = WrongPrimitivesVisitor(default_options, file_tokens=file_tokens) + visitor.run() + + assert_errors(visitor, [UnderscoredNumberViolation]) + + +@pytest.mark.parametrize('code', [ + 'x = 1000', + 'print(333555)', + '33 + 55', + 'print("10_00")', +]) +def test_correct_number( + parse_tokens, + assert_errors, + default_options, + code, +): + """Ensures that correct numbers are fine.""" + file_tokens = parse_tokens(code) + + visitor = WrongPrimitivesVisitor(default_options, file_tokens=file_tokens) + visitor.run() + + assert_errors(visitor, []) diff --git a/tests/test_visitors/test_tokenize/test_wrong_primitives/test_unicode_prefix.py b/tests/test_visitors/test_tokenize/test_wrong_primitives/test_unicode_prefix.py new file mode 100644 index 000000000..f8ab4b9ff --- /dev/null +++ b/tests/test_visitors/test_tokenize/test_wrong_primitives/test_unicode_prefix.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- + +import pytest + +from wemake_python_styleguide.visitors.tokenize.wrong_primitives import ( + UnicodeStringViolation, + WrongPrimitivesVisitor, +) + + +@pytest.mark.parametrize('code', [ + 'x = u"text"', + "print(u'unicode')", + '"3_3" + u"5_5"', +]) +def test_unicode_prefix( + parse_tokens, + assert_errors, + default_options, + code, +): + """Ensures that unicode prefixes raise a warning.""" + file_tokens = parse_tokens(code) + + visitor = WrongPrimitivesVisitor(default_options, file_tokens=file_tokens) + visitor.run() + + assert_errors(visitor, [UnicodeStringViolation]) + + +@pytest.mark.parametrize('code', [ + 'x = "name"', + 'x = r"text"', + "print(b'unicode')", + '"u" + "12"', +]) +def test_correct_strings( + parse_tokens, + assert_errors, + default_options, + code, +): + """Ensures that correct strings are fine.""" + file_tokens = parse_tokens(code) + + visitor = WrongPrimitivesVisitor(default_options, file_tokens=file_tokens) + visitor.run() + + assert_errors(visitor, []) diff --git a/wemake_python_styleguide/checker.py b/wemake_python_styleguide/checker.py index b45f23ac4..96d08e0c4 100644 --- a/wemake_python_styleguide/checker.py +++ b/wemake_python_styleguide/checker.py @@ -1,17 +1,18 @@ # -*- coding: utf-8 -*- import ast -from tokenize import TokenInfo +import tokenize from typing import Generator, Sequence from flake8.options.manager import OptionManager from wemake_python_styleguide import constants, types, version from wemake_python_styleguide.options.config import Configuration -from wemake_python_styleguide.visitors.presets.complexity import ( - COMPLEXITY_PRESET, +from wemake_python_styleguide.visitors.presets import ( + complexity, + general, + tokens, ) -from wemake_python_styleguide.visitors.presets.general import GENERAL_PRESET class Checker(object): @@ -34,16 +35,18 @@ class Checker(object): #: Visitors that should be working by default: ast_visitors: types.TreeVisitorSequence = ( - *GENERAL_PRESET, - *COMPLEXITY_PRESET, + *general.GENERAL_PRESET, + *complexity.COMPLEXITY_PRESET, ) - token_visitors: types.TokenVisitorSequence = () + token_visitors: types.TokenVisitorSequence = ( + *tokens.TOKENS_PRESET, + ) def __init__( self, tree: ast.Module, - file_tokens: Sequence[TokenInfo], + file_tokens: Sequence[tokenize.TokenInfo], filename: str = constants.STDIN, ) -> None: """Creates new checker instance.""" diff --git a/wemake_python_styleguide/errors/base.py b/wemake_python_styleguide/errors/base.py index f7d8713ea..e6df0673d 100644 --- a/wemake_python_styleguide/errors/base.py +++ b/wemake_python_styleguide/errors/base.py @@ -1,7 +1,14 @@ # -*- coding: utf-8 -*- import ast -from typing import Optional, Tuple +import tokenize +from typing import Tuple, Union + +ErrorNode = Union[ + ast.AST, + tokenize.TokenInfo, + None, +] class BaseStyleViolation(object): @@ -18,8 +25,8 @@ class BaseStyleViolation(object): code: int should_use_text: bool = True - def __init__(self, node: Optional[ast.AST], text: str = None) -> None: - """Creates new instance of AST style violation.""" + def __init__(self, node: ErrorNode, text: str = None) -> None: + """Creates new instance of style violation.""" self._node = node if text is None: @@ -28,8 +35,11 @@ def __init__(self, node: Optional[ast.AST], text: str = None) -> None: self._text = text def _full_code(self) -> str: - """Returns fully formated code.""" - return 'Z' + str(self.code) + """Returns fully formatted code.""" + return 'Z' + str(self.code).zfill(3) + + def _location(self) -> Tuple[int, int]: + return 0, 0 def message(self) -> str: """Returns error's formatted message.""" @@ -39,22 +49,32 @@ def message(self) -> str: def node_items(self) -> Tuple[int, int, str]: """Returns tuple to match ``flake8`` API format.""" - line_number = getattr(self._node, 'lineno', 0) - column_offset = getattr(self._node, 'col_offset', 0) - return line_number, column_offset, self.message() + return (*self._location(), self.message()) class ASTStyleViolation(BaseStyleViolation): """AST based style violations.""" - def __init__(self, node: ast.AST, text: str = None) -> None: - """Creates new instance of AST style violation.""" - super().__init__(node, text=text) + _node: ast.AST + + def _location(self) -> Tuple[int, int]: + return self._node.lineno, self._node.col_offset class SimpleStyleViolation(BaseStyleViolation): """Style violation for cases where there's no AST nodes.""" + _node: None + def __init__(self, node=None, text: str = None) -> None: """Creates new instance of simple style violation.""" super().__init__(node, text=text) + + +class TokenStyleViolation(BaseStyleViolation): + """Style violation for ``tokenize`` errors.""" + + _node: tokenize.TokenInfo + + def _location(self) -> Tuple[int, int]: + return self._node.start diff --git a/wemake_python_styleguide/errors/tokens.py b/wemake_python_styleguide/errors/tokens.py new file mode 100644 index 000000000..010a187e0 --- /dev/null +++ b/wemake_python_styleguide/errors/tokens.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- + +from wemake_python_styleguide.errors.base import TokenStyleViolation + + +class UnicodeStringViolation(TokenStyleViolation): + """ + Forbids to use ``u`` string prefix. + + Reasoning: + We do not need this prefix since ``python2``. + But, it is still possible to find it inside the codebase. + + Solution: + Remove this prefix. + + Example:: + # Correct: + nickname = 'sobolevn' + file_contents = b'aabbcc' + + # Wrong: + nickname = u'sobolevn' + + Note: + Returns Z001 as error code + + """ + + code = 1 + error_template = '{0} Found unicode string prefix: {1}' + + +class UnderscoredNumberViolation(TokenStyleViolation): + """ + Forbids to use ``_`` in numbers. + + Reasoning: + It is possible to write ``1000`` in three different ways: + ``1_000``, ``10_00``, and ``100_0``. It all depends on cultural + habits of the author. + + Solution: + Numbers should be written as numbers: ``1000``. + If you have a very big number with a lot of zeros, use multiplication. + + Example:: + # Correct: + phone = 88313443 + million = 1000000 + + # Wrong: + phone = 883_134_43 + million = 100_00_00 + + Note: + Returns Z002 as error code + + """ + + code = 2 + error_template = '{0} Found underscored number: {1}' diff --git a/wemake_python_styleguide/types.py b/wemake_python_styleguide/types.py index be280b06c..70c762459 100644 --- a/wemake_python_styleguide/types.py +++ b/wemake_python_styleguide/types.py @@ -30,7 +30,7 @@ ] #: Token specific visitors' classes: -TokenVisitorSequence = Sequence[Type['base.BaseVisitor']] +TokenVisitorSequence = Sequence[Type['base.BaseTokenVisitor']] #: In cases we need to work with both import types: AnyImport = Union[ast.Import, ast.ImportFrom] diff --git a/wemake_python_styleguide/visitors/base.py b/wemake_python_styleguide/visitors/base.py index 9223dcb50..da98744d6 100644 --- a/wemake_python_styleguide/visitors/base.py +++ b/wemake_python_styleguide/visitors/base.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- import ast -from typing import List, Type +import tokenize +from typing import List, Sequence, Type from wemake_python_styleguide import constants from wemake_python_styleguide.errors.base import BaseStyleViolation @@ -85,7 +86,7 @@ def _post_visit(self) -> None: """ def run(self) -> None: - """Runs the checking process.""" + """Recursively visits all ``ast`` nodes. Then executes post hook.""" self.visit(self.tree) self._post_visit() @@ -105,3 +106,41 @@ def run(self) -> None: """Checks module's filename.""" if self.filename != constants.STDIN: self.visit_filename() + + +class BaseTokenVisitor(BaseVisitor): + """Allows to check ``tokenize`` sequences.""" + + def __init__( + self, + options: ConfigurationOptions, + file_tokens: Sequence[tokenize.TokenInfo], + **kwargs, + ) -> None: + """Creates new ``tokenize`` based instance.""" + super().__init__(options, **kwargs) + self.file_tokens = file_tokens + + @classmethod + def from_checker( + cls: Type['BaseTokenVisitor'], + checker, + ) -> 'BaseTokenVisitor': + """Constructs visitor instance from the checker.""" + return cls( + options=checker.options, + filename=checker.filename, + file_tokens=checker.file_tokens, + ) + + def visit(self, token: tokenize.TokenInfo) -> None: + """Runs custom defined for each specific token type.""" + token_type = tokenize.tok_name[token.type].lower() + method = getattr(self, 'visit_' + token_type, None) + if method is not None: + method(token) + + def run(self) -> None: + """Visits all token types.""" + for token in self.file_tokens: + self.visit(token) diff --git a/wemake_python_styleguide/visitors/presets/tokens.py b/wemake_python_styleguide/visitors/presets/tokens.py new file mode 100644 index 000000000..6bc1a7d0f --- /dev/null +++ b/wemake_python_styleguide/visitors/presets/tokens.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- + +from wemake_python_styleguide.visitors.tokenize.wrong_primitives import ( + WrongPrimitivesVisitor, +) + +TOKENS_PRESET = ( + WrongPrimitivesVisitor, +) diff --git a/wemake_python_styleguide/visitors/tokenize/__init__.py b/wemake_python_styleguide/visitors/tokenize/__init__.py new file mode 100644 index 000000000..40a96afc6 --- /dev/null +++ b/wemake_python_styleguide/visitors/tokenize/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/wemake_python_styleguide/visitors/tokenize/wrong_primitives.py b/wemake_python_styleguide/visitors/tokenize/wrong_primitives.py new file mode 100644 index 000000000..8d8139d8a --- /dev/null +++ b/wemake_python_styleguide/visitors/tokenize/wrong_primitives.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- + +import tokenize + +from wemake_python_styleguide.errors.tokens import ( + UnderscoredNumberViolation, + UnicodeStringViolation, +) +from wemake_python_styleguide.visitors.base import BaseTokenVisitor + + +class WrongPrimitivesVisitor(BaseTokenVisitor): + """Visits primitive types to find incorrect usages.""" + + def visit_string(self, token: tokenize.TokenInfo) -> None: + """ + Checks string declarations. + + ``u`` can only be the only prefix. + You can not combine it with ``r``, ``b``, or ``f``. + + Raises: + UnicodeStringViolation + + """ + if token.string.startswith('u'): + self.add_error(UnicodeStringViolation(token, text=token.string)) + + def visit_number(self, token: tokenize.TokenInfo) -> None: + """ + Checks number declarations. + + Raises: + UnderscoredNumberViolation + + """ + if '_' in token.string: + self.add_error( + UnderscoredNumberViolation(token, text=token.string), + ) From da1a9e8f6887f8e125fc9614c0a06aac2ea4ae10 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sat, 22 Sep 2018 14:15:08 +0300 Subject: [PATCH 5/7] Adds flake8-bandit --- CHANGELOG.md | 2 +- pyproject.lock | 93 ++++++++++++++++--- pyproject.toml | 2 +- setup.cfg | 2 + tests/test_errors.py | 15 ++- tests/test_visitors/test_base.py | 2 +- wemake_python_styleguide/errors/base.py | 4 +- wemake_python_styleguide/errors/tokens.py | 27 ++++++ .../visitors/presets/tokens.py | 4 + .../visitors/tokenize/wrong_comments.py | 26 ++++++ 10 files changed, 159 insertions(+), 18 deletions(-) create mode 100644 wemake_python_styleguide/visitors/tokenize/wrong_comments.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d148227c..1d4e873c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ We used to have incremental versioning before `0.1.0`. - Refactors how visitors are injected into the checker, now using presets - Creates new visitor type: `BaseTokenVisitor` for working with `tokenize` - Improves typing support -- Adds `cohesion` plugin +- Adds `flake8-bandit` plugin - Adds `flake8-eradicate` plugin - Adds `flake8-print` plugin for development - Removes `delegate` concept from the codebase diff --git a/pyproject.lock b/pyproject.lock index 50ca730e8..bca8c3ab9 100644 --- a/pyproject.lock +++ b/pyproject.lock @@ -49,6 +49,21 @@ version = "2.6.0" [package.dependencies] pytz = ">=0a" +[[package]] +category = "main" +description = "Security oriented static analyser for python code." +name = "bandit" +optional = false +platform = "*" +python-versions = "*" +version = "1.5.1" + +[package.dependencies] +GitPython = ">=1.0.1" +PyYAML = ">=3.12" +six = ">=1.10.0" +stevedore = ">=1.20.0" + [[package]] category = "dev" description = "Python package for providing Mozilla's CA Bundle." @@ -67,15 +82,6 @@ platform = "*" python-versions = "*" version = "3.0.4" -[[package]] -category = "main" -description = "A tool for measuring Python class cohesion." -name = "cohesion" -optional = false -platform = "*" -python-versions = "*" -version = "0.9.1" - [[package]] category = "dev" description = "Cross-platform colored terminal text." @@ -145,6 +151,20 @@ mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.0.0,<2.4.0" pyflakes = ">=1.5.0,<1.7.0" +[[package]] +category = "main" +description = "Automated security testing with bandit and flake8." +name = "flake8-bandit" +optional = false +platform = "*" +python-versions = "*" +version = "1.0.2" + +[package.dependencies] +bandit = "*" +flake8 = "*" +flake8-polyfill = "*" + [[package]] category = "main" description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." @@ -346,6 +366,30 @@ version = "1.0.0" [package.dependencies] flake8 = ">=3.0.0" +[[package]] +category = "main" +description = "Git Object Database" +name = "gitdb2" +optional = false +platform = "*" +python-versions = "*" +version = "2.0.4" + +[package.dependencies] +smmap2 = ">=2.0.0" + +[[package]] +category = "main" +description = "Python Git Library" +name = "gitpython" +optional = false +platform = "*" +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +version = "2.1.11" + +[package.dependencies] +gitdb2 = ">=2.0.0" + [[package]] category = "dev" description = "Internationalized Domain Names in Applications (IDNA)" @@ -473,7 +517,7 @@ pyparsing = ">=2.0.2" six = "*" [[package]] -category = "dev" +category = "main" description = "Python Build Reasonableness" name = "pbr" optional = false @@ -654,6 +698,15 @@ platform = "Independent" python-versions = "*" version = "2018.5" +[[package]] +category = "main" +description = "YAML parser and emitter for Python" +name = "pyyaml" +optional = false +platform = "Any" +python-versions = "*" +version = "3.13" + [[package]] category = "dev" description = "Python HTTP for Humans." @@ -690,6 +743,15 @@ platform = "*" python-versions = "*" version = "1.11.0" +[[package]] +category = "main" +description = "A pure python implementation of a sliding window memory map manager" +name = "smmap2" +optional = false +platform = "any" +python-versions = "*" +version = "2.0.4" + [[package]] category = "main" description = "This package provides 16 stemmer algorithms (15 + Poerter English stemmer) generated from Snowball algorithms." @@ -770,7 +832,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" version = "1.1.0" [[package]] -category = "dev" +category = "main" description = "Manage dynamic plugins for Python applications" name = "stevedore" optional = false @@ -831,7 +893,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" version = "1.23" [metadata] -content-hash = "3f968141461a9121b7eb29ac78732b1cc97319947db2d52d9f25578a8c171847" +content-hash = "d8ff4d5e6666b0963c59e1e10b97a0bd0cdc71818c7d5c7f1c6d01558cc9cf98" platform = "*" python-versions = "^3.6 || ^3.7" @@ -841,15 +903,16 @@ alabaster = ["674bb3bab080f598371f4443c5008cbfeb1a5e622dd312395d2d82af2c54c456", atomicwrites = ["0312ad34fcad8fac3704d441f7b317e50af620823353ec657a53e981f92920c0", "ec9ae8adaae229e4f8446952d204a3e4b5fdd2d099f9be3aaf556120135fb3ee"] attrs = ["10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69", "ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"] babel = ["6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669", "8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23"] +bandit = ["6102b5d6afd9d966df5054e0bdfc2e73a24d0fea400ec25f2e54c134412158d7", "9413facfe9de1e1bd291d525c784e1beb1a55c9916b51dae12979af63a69ba4c"] certifi = ["376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638", "456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"] chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] -cohesion = ["a20019bdf6c5345bb1ddf18124121b87b79db5a039f20b53547d9e664dab19ed"] colorama = ["463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda", "48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"] coverage = ["03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba", "0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed", "104ab3934abaf5be871a583541e8829d6c19ce7bde2923b2751e0d3ca44db60a", "15b111b6a0f46ee1a485414a52a7ad1d703bdf984e9ed3c288a4414d3871dcbd", "198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640", "1c383d2ef13ade2acc636556fd544dba6e14fa30755f26812f54300e401f98f2", "28b2191e7283f4f3568962e373b47ef7f0392993bb6660d079c62bd50fe9d162", "2eb564bbf7816a9d68dd3369a510be3327f1c618d2357fa6b1216994c2e3d508", "337ded681dd2ef9ca04ef5d93cfc87e52e09db2594c296b4a0a3662cb1b41249", "3a2184c6d797a125dca8367878d3b9a178b6fdd05fdc2d35d758c3006a1cd694", "3c79a6f7b95751cdebcd9037e4d06f8d5a9b60e4ed0cd231342aa8ad7124882a", "3d72c20bd105022d29b14a7d628462ebdc61de2f303322c0212a054352f3b287", "3eb42bf89a6be7deb64116dd1cc4b08171734d721e7a7e57ad64cc4ef29ed2f1", "4635a184d0bbe537aa185a34193898eee409332a8ccb27eea36f262566585000", "56e448f051a201c5ebbaa86a5efd0ca90d327204d8b059ab25ad0f35fbfd79f1", "5a13ea7911ff5e1796b6d5e4fbbf6952381a611209b736d48e675c2756f3f74e", "69bf008a06b76619d3c3f3b1983f5145c75a305a0fea513aca094cae5c40a8f5", "6bc583dc18d5979dc0f6cec26a8603129de0304d5ae1f17e57a12834e7235062", "701cd6093d63e6b8ad7009d8a92425428bc4d6e7ab8d75efbb665c806c1d79ba", "7608a3dd5d73cb06c531b8925e0ef8d3de31fed2544a7de6c63960a1e73ea4bc", "76ecd006d1d8f739430ec50cc872889af1f9c1b6b8f48e29941814b09b0fd3cc", "7aa36d2b844a3e4a4b356708d79fd2c260281a7390d678a10b91ca595ddc9e99", "7d3f553904b0c5c016d1dad058a7554c7ac4c91a789fca496e7d8347ad040653", "7e1fe19bd6dce69d9fd159d8e4a80a8f52101380d5d3a4d374b6d3eae0e5de9c", "8c3cb8c35ec4d9506979b4cf90ee9918bc2e49f84189d9bf5c36c0c1119c6558", "9d6dd10d49e01571bf6e147d3b505141ffc093a06756c60b053a859cb2128b1f", "9e112fcbe0148a6fa4f0a02e8d58e94470fc6cb82a5481618fea901699bf34c4", "ac4fef68da01116a5c117eba4dd46f2e06847a497de5ed1d64bb99a5fda1ef91", "b8815995e050764c8610dbc82641807d196927c3dbed207f0a079833ffcf588d", "be6cfcd8053d13f5f5eeb284aa8a814220c3da1b0078fa859011c7fffd86dab9", "c1bb572fab8208c400adaf06a8133ac0712179a334c09224fb11393e920abcdd", "de4418dadaa1c01d497e539210cb6baa015965526ff5afc078c57ca69160108d", "e05cb4d9aad6233d67e0541caa7e511fa4047ed7750ec2510d466e806e0255d6", "e4d96c07229f58cb686120f168276e434660e4358cc9cf3b0464210b04913e77", "f3f501f345f24383c0000395b26b726e46758b71393267aeae0bd36f8b3ade80", "f8a923a85cb099422ad5a2e345fe877bbc89a8a8b23235824a93488150e45f6e"] doc8 = ["2df89f9c1a5abfb98ab55d0175fed633cae0cf45025b8b1e0ee5ea772be28543", "d12f08aa77a4a65eb28752f4bc78f41f611f9412c4155e2b03f1f5d4a45efe04"] docutils = ["02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", "51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", "7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"] eradicate = ["f9af01c544ccd8f71bc2f7f3fa39dc363d842cfcb9c730a83676a59026ab5f24"] flake8 = ["7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0", "c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37"] +flake8-bandit = ["a66c7b42af9530d5e988851ccee02958a51a85d46f1f4609ecc3546948f809b8", "f7c3421fd9aebc63689c0693511e16dcad678fd4a0ce624b78ca91ae713eacdc"] flake8-bugbear = ["07b6e769d7f4e168d590f7088eae40f6ddd9fa4952bed31602def65842682c83", "0ccf56975f4db1d69dc1cf3598c99d768ebf95d0cad27d76087954aa399b515a"] flake8-builtins = ["8d806360767947c0035feada4ddef3ede32f0a586ef457e62d811b8456ad9a51", "cd7b1b7fec4905386a3643b59f9ca8e305768da14a49a7efb31fe9364f33cd04"] flake8-coding = ["549c2b22c08711feda11795fb49f147a626305b602c547837bab405e7981f844", "f2ee7c3c8ae47f2d278111a2090655bcf170789c24ccfea519d93be2ede7571c"] @@ -866,6 +929,8 @@ flake8-pytest = ["61686128a79e1513db575b2bcac351081d5a293811ddce2d5dfc25e8c762d3 flake8-quotes = ["fd9127ad8bbcf3b546fa7871a5266fd8623ce765ebe3d5aa5eabb80c01212b26"] flake8-string-format = ["68ea72a1a5b75e7018cae44d14f32473c798cf73d75cbaed86c6a9a907b770b2", "774d56103d9242ed968897455ef49b7d6de272000cfa83de5814273a868832f1"] flake8-super-call = ["0486ebfb24e3bf104b1c1d7f823a51ebcefb3a26108b25cb3129a9e8a57ef145", "61cfcd0c8ad78e1cf1f4d1225cb7513666ab9c5a9900a238b2c4abee8ad9a7de"] +gitdb2 = ["87783b7f4a8f6b71c7fe81d32179b3c8781c1a7d6fa0c69bff2f315b00aff4f8", "bb4c85b8a58531c51373c89f92163b92f30f81369605a67cd52d1fc21246c044"] +gitpython = ["563221e5a44369c6b79172f455584c9ebbb122a13368cc82cb4b5addff788f82", "8237dc5bfd6f1366abeee5624111b9d6879393d84745a507de0fda86043b65a8"] idna = ["156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", "684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"] imagesize = ["3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8", "f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"] isort = ["1153601da39a25b14ddc54955dbbacbb6b2d19135386699e2ad58517953b34af", "b9c40e9750f3d77e6e4d441d8b0266cf555e7cdabdcff33c4fd06366ca761ef8", "ec9ef8f4a9bc6f71eec99e1806bfa2de401650d996c59330782b89a5555c1497"] @@ -894,9 +959,11 @@ pytest-flake8 = ["4f30f5be3efb89755f38f11bdb2a5e22d19a6f5faa73428f703a3292a9572c pytest-isort = ["2221c0914dfca41914625a646f0d2d1d4c676861b9a7b26746a7fdd40aa2c59b", "c70d0f900f4647bb714f0843dd82d7f7b759904006de31254efdb72ce88e0c0e"] pytest-randomly = ["6db5e03d72b54052b9b379dc3cfa4749c19bfe4de161cf3eb24762049f4ce9be", "92ec6745d3ebdd690ecb598648748c9601f16f5afacf83ccef2b50d23e6edb7f"] pytz = ["a061aa0a9e06881eb8b3b2b43f05b9439d6583c206d0a6c340ff72a7b6669053", "ffb9ef1de172603304d9d2819af6f5ece76f2e85ec10692a524dd876e72bf277"] +pyyaml = ["3d7da3009c0f3e783b2c873687652d83b1bbfd5c88e9813fb7e5b03c0dd3108b", "3ef3092145e9b70e3ddd2c7ad59bdd0252a94dfe3949721633e41344de00a6bf", "40c71b8e076d0550b2e6380bada1f1cd1017b882f7e16f09a65be98e017f211a", "558dd60b890ba8fd982e05941927a3911dc409a63dcb8b634feaa0cda69330d3", "a7c28b45d9f99102fa092bb213aa12e0aaf9a6a1f5e395d36166639c1f96c3a1", "aa7dd4a6a427aed7df6fb7f08a580d68d9b118d90310374716ae90b710280af1", "bc558586e6045763782014934bfaf39d48b8ae85a2713117d16c39864085c613", "d46d7982b62e0729ad0175a9bc7e10a566fc07b224d2c79fafb5e032727eaa04", "d5eef459e30b09f5a098b9cea68bebfeb268697f78d647bd255a085371ac7f3f", "e01d3203230e1786cd91ccfdc8f8454c8069c91bee3962ad93b87a4b2860f537", "e170a9e6fcfd19021dd29845af83bb79236068bf5fd4df3327c1be18182b2531"] requests = ["63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1", "ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a"] restructuredtext-lint = ["c48ca9a84c312b262809f041fe47dcfaedc9ee4879b3e1f9532f745c182b4037"] six = ["70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", "832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"] +smmap2 = ["0dd53d991af487f9b22774fa89451358da3607c02b9b886a54736c6a313ece0b", "dc216005e529d57007ace27048eb336dcecb7fc413cfb3b2f402bb25972b69c6"] snowballstemmer = ["919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128", "9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89"] sphinx = ["95acd6648902333647a0e0564abdb28a74b0a76d2333148aa35e5ed1f56d3c4b", "c091dbdd5cc5aac6eb95d591a819fd18bccec90ffb048ec465b165a48b839b45"] sphinx-autodoc-typehints = ["1a9df6cb3ba72453ea4bfbe96ea887abc0d796b2ce9508c2189217a1bb69b366", "46cc9e985ee6d8bbbd07fffd95b815c39a72df6afb600f59671f85f7340e7d0d"] diff --git a/pyproject.toml b/pyproject.toml index 676080014..4bab6db34 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,7 +58,7 @@ flake8-debugger = "^3.1" flake8-isort = "^2.5" flake8-eradicate = "^0.1" pep8-naming = "^0.7" -cohesion = "^0.9" +flake8-bandit = "^1.0" [tool.poetry.dev-dependencies] pytest-cov = "^2.6" diff --git a/setup.cfg b/setup.cfg index ed0956879..7fa82c379 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,6 +27,8 @@ flake8-ignore = wemake_python_styleguide/visitors/ast/*.py N802 # These modules should contain a lot of classes: wemake_python_styleguide/errors/*.py Z208 + # There are `assert`s and subprocesses in tests: + tests/*.py S # Disable some pydocstyle checks: *.py D100 D104 D106 D401 diff --git a/tests/test_errors.py b/tests/test_errors.py index 9a5faf33a..09cd9e298 100644 --- a/tests/test_errors.py +++ b/tests/test_errors.py @@ -1,6 +1,19 @@ # -*- coding: utf-8 -*- -from wemake_python_styleguide.errors.base import BaseStyleViolation +import ast + +from wemake_python_styleguide.errors.base import ( + ASTStyleViolation, + BaseStyleViolation, +) + + +def test_visitor_returns_location(): + """Ensures that `BaseNodeVisitor` return correct error message.""" + visitor = ASTStyleViolation(node=ast.parse(''), text='error') + visitor.error_template = '{0} {1}' + visitor.code = 1 + assert visitor.node_items() == (0, 0, 'Z001 error') def test_checker_default_location(): diff --git a/tests/test_visitors/test_base.py b/tests/test_visitors/test_base.py index f82d65b97..8707165b9 100644 --- a/tests/test_visitors/test_base.py +++ b/tests/test_visitors/test_base.py @@ -11,7 +11,7 @@ ) -def test_checker_raises_not_implemented(default_options): +def test_visitor_raises_not_implemented(default_options): """Ensures that `BaseChecker` raises `NotImplementedError`.""" with pytest.raises(NotImplementedError): BaseVisitor(default_options).run() diff --git a/wemake_python_styleguide/errors/base.py b/wemake_python_styleguide/errors/base.py index e6df0673d..8cc389321 100644 --- a/wemake_python_styleguide/errors/base.py +++ b/wemake_python_styleguide/errors/base.py @@ -58,7 +58,9 @@ class ASTStyleViolation(BaseStyleViolation): _node: ast.AST def _location(self) -> Tuple[int, int]: - return self._node.lineno, self._node.col_offset + line_number = getattr(self._node, 'lineno', 0) + column_offset = getattr(self._node, 'col_offset', 0) + return line_number, column_offset class SimpleStyleViolation(BaseStyleViolation): diff --git a/wemake_python_styleguide/errors/tokens.py b/wemake_python_styleguide/errors/tokens.py index 010a187e0..ea43da5fe 100644 --- a/wemake_python_styleguide/errors/tokens.py +++ b/wemake_python_styleguide/errors/tokens.py @@ -60,3 +60,30 @@ class UnderscoredNumberViolation(TokenStyleViolation): code = 2 error_template = '{0} Found underscored number: {1}' + + +class WrongMagicCommentViolation(TokenStyleViolation): + """ + Restricts to use several control (or magic) comments. + + We do not allow to use: + + 1. ``# noqa`` comment without specified errors + 2. ``# flake8: noqa`` comment to disable the whole file + 3. ``type: some_type`` comments to specify a type for ``typed_ast`` + + Reasoning: + We cover several different use-cases in a single rule. + ``# noqa`` comment is restricted because it can hide other errors. + ``# flake8: noqa`` is restricted because it is hard to tell + what files are ignored with this comment. + ``type: int`` comment is restricted because + we can already use type annotations instead. + + Note: + Returns Z003 as error code + + """ + + code = 3 + error_template = '{0} Found wrong magic comment: {1}' diff --git a/wemake_python_styleguide/visitors/presets/tokens.py b/wemake_python_styleguide/visitors/presets/tokens.py index 6bc1a7d0f..4795632f6 100644 --- a/wemake_python_styleguide/visitors/presets/tokens.py +++ b/wemake_python_styleguide/visitors/presets/tokens.py @@ -1,9 +1,13 @@ # -*- coding: utf-8 -*- +from wemake_python_styleguide.visitors.tokenize.wrong_comments import ( + WrongCommentVisitor, +) from wemake_python_styleguide.visitors.tokenize.wrong_primitives import ( WrongPrimitivesVisitor, ) TOKENS_PRESET = ( + WrongCommentVisitor, WrongPrimitivesVisitor, ) diff --git a/wemake_python_styleguide/visitors/tokenize/wrong_comments.py b/wemake_python_styleguide/visitors/tokenize/wrong_comments.py new file mode 100644 index 000000000..9619d7413 --- /dev/null +++ b/wemake_python_styleguide/visitors/tokenize/wrong_comments.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +r""" +Disallows to use incorrect magic comments. + +That's how a basic ``comment`` type token looks like: + +TokenInfo( + type=57 (COMMENT), + string='# noqa: Z100', + start=(1, 4), + end=(1, 16), + line="u'' # noqa: Z100\n", +) +""" + +import tokenize + +from wemake_python_styleguide.visitors.base import BaseTokenVisitor + + +class WrongCommentVisitor(BaseTokenVisitor): + """Checks comment tokens.""" + + def visit_comment(self, token: tokenize.TokenInfo) -> None: + """Performs comment checks.""" From 3897fed200daa2dee4047be8eaa48121f7f81675 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sun, 23 Sep 2018 14:43:17 +0300 Subject: [PATCH 6/7] Adds noqa comments analyzer --- tests/fixtures/noqa.py | 1 - .../test_wrong_comments/test_noqa_comment.py | 58 +++++++++++++++++++ wemake_python_styleguide/errors/tokens.py | 7 ++- .../visitors/tokenize/wrong_comments.py | 18 ++++++ 4 files changed, 81 insertions(+), 3 deletions(-) create mode 100644 tests/test_visitors/test_tokenize/test_wrong_comments/test_noqa_comment.py diff --git a/tests/fixtures/noqa.py b/tests/fixtures/noqa.py index fd0136f07..9507e24a9 100644 --- a/tests/fixtures/noqa.py +++ b/tests/fixtures/noqa.py @@ -8,7 +8,6 @@ from .version import get_version # noqa: Z100 - full_name = u'Nikita Sobolev' # noqa: Z001 phone_number = 555_123_999 # noqa: Z002 diff --git a/tests/test_visitors/test_tokenize/test_wrong_comments/test_noqa_comment.py b/tests/test_visitors/test_tokenize/test_wrong_comments/test_noqa_comment.py new file mode 100644 index 000000000..3845b117a --- /dev/null +++ b/tests/test_visitors/test_tokenize/test_wrong_comments/test_noqa_comment.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- + +import pytest + +from wemake_python_styleguide.visitors.tokenize.wrong_comments import ( + WrongCommentVisitor, + WrongMagicCommentViolation, +) + + +@pytest.mark.parametrize('code', [ + 'x = 10_00 # noqa: Z002,Z114', + 'x = 10_00 # noqa:Z002, Z114', + 'x = 10_00 # noqa: Z002, Z114', + 'wallet = 10_00 # noqa: Z002', + 'x = 1000 # noqa: Z002', + 'print(12 + 3) # regular comment', + 'print(12 + 3)', + '', +]) +def test_correct_comments( + parse_tokens, + assert_errors, + default_options, + code, +): + """Ensures that correct comments do not raise a warning.""" + file_tokens = parse_tokens(code) + + visitor = WrongCommentVisitor(default_options, file_tokens=file_tokens) + visitor.run() + + assert_errors(visitor, []) + + +@pytest.mark.parametrize('code', [ + 'x = 10_00 # noqa', + 'x = 10_00 #noqa', + 'x = 10_00#noqa', + 'wallet = 10_00 # noqa: some comments', + 'x = 1000 # noqa:', + 'x = 10_00 # noqa: -', + 'x = 10_00 # noqa: *', + '# noqa', +]) +def test_incorrect_noqa_comment( + parse_tokens, + assert_errors, + default_options, + code, +): + """Ensures that incorrect `noqa` comments raise a warning.""" + file_tokens = parse_tokens(code) + + visitor = WrongCommentVisitor(default_options, file_tokens=file_tokens) + visitor.run() + + assert_errors(visitor, [WrongMagicCommentViolation]) diff --git a/wemake_python_styleguide/errors/tokens.py b/wemake_python_styleguide/errors/tokens.py index ea43da5fe..e3d76fa3a 100644 --- a/wemake_python_styleguide/errors/tokens.py +++ b/wemake_python_styleguide/errors/tokens.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- -from wemake_python_styleguide.errors.base import TokenStyleViolation +from wemake_python_styleguide.errors.base import ( + SimpleStyleViolation, + TokenStyleViolation, +) class UnicodeStringViolation(TokenStyleViolation): @@ -62,7 +65,7 @@ class UnderscoredNumberViolation(TokenStyleViolation): error_template = '{0} Found underscored number: {1}' -class WrongMagicCommentViolation(TokenStyleViolation): +class WrongMagicCommentViolation(SimpleStyleViolation): """ Restricts to use several control (or magic) comments. diff --git a/wemake_python_styleguide/visitors/tokenize/wrong_comments.py b/wemake_python_styleguide/visitors/tokenize/wrong_comments.py index 9619d7413..b0891d201 100644 --- a/wemake_python_styleguide/visitors/tokenize/wrong_comments.py +++ b/wemake_python_styleguide/visitors/tokenize/wrong_comments.py @@ -14,13 +14,31 @@ ) """ +import re import tokenize +from typing.re import Match, Pattern +from wemake_python_styleguide.errors.tokens import WrongMagicCommentViolation from wemake_python_styleguide.visitors.base import BaseTokenVisitor +NOQA_CHECK: Pattern = re.compile(r'^noqa:?($|[A-Z\d\,\s]+)') + class WrongCommentVisitor(BaseTokenVisitor): """Checks comment tokens.""" + def _check_noqa(self, token: tokenize.TokenInfo) -> None: + comment_text = token.string[1:].strip() + match: Match = NOQA_CHECK.match(comment_text) + if not match: + return + + excludes = match.groups()[0].strip() + if not excludes: + # We can not pass the actual line here, + # since it will be ignored due to `noqa` comment: + self.add_error(WrongMagicCommentViolation(text=comment_text)) + def visit_comment(self, token: tokenize.TokenInfo) -> None: """Performs comment checks.""" + self._check_noqa(token) From daaf848152849f879567b685f29a8df0303eb293 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Sun, 23 Sep 2018 15:30:29 +0300 Subject: [PATCH 7/7] Adds `type:` comment validation --- CHANGELOG.md | 2 +- .../test_wrong_comments/test_noqa_comment.py | 3 + .../test_wrong_comments/test_typed_ast.py | 59 +++++++++++++++++++ wemake_python_styleguide/constants.py | 3 +- wemake_python_styleguide/errors/tokens.py | 5 +- .../visitors/tokenize/wrong_comments.py | 21 ++++++- 6 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 tests/test_visitors/test_tokenize/test_wrong_comments/test_typed_ast.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d4e873c7..422bb2af8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ We used to have incremental versioning before `0.1.0`. - Adds `--i-control-code` option to ignore `InitModuleHasLogicViolation` - Adds check for underscored numbers - Forbids `u''` strings -- Adds `flake8`, `noqa`, and `type` comments check +- Adds `noqa` and `type` comments checks ### Misc diff --git a/tests/test_visitors/test_tokenize/test_wrong_comments/test_noqa_comment.py b/tests/test_visitors/test_tokenize/test_wrong_comments/test_noqa_comment.py index 3845b117a..43fce93e6 100644 --- a/tests/test_visitors/test_tokenize/test_wrong_comments/test_noqa_comment.py +++ b/tests/test_visitors/test_tokenize/test_wrong_comments/test_noqa_comment.py @@ -14,7 +14,9 @@ 'x = 10_00 # noqa: Z002, Z114', 'wallet = 10_00 # noqa: Z002', 'x = 1000 # noqa: Z002', + 'x = 1000 # noqa: Z002 ', 'print(12 + 3) # regular comment', + 'print(12 + 3) #', 'print(12 + 3)', '', ]) @@ -35,6 +37,7 @@ def test_correct_comments( @pytest.mark.parametrize('code', [ 'x = 10_00 # noqa', + 'x = 10_00 # noqa ', 'x = 10_00 #noqa', 'x = 10_00#noqa', 'wallet = 10_00 # noqa: some comments', diff --git a/tests/test_visitors/test_tokenize/test_wrong_comments/test_typed_ast.py b/tests/test_visitors/test_tokenize/test_wrong_comments/test_typed_ast.py new file mode 100644 index 000000000..f97a6b8bf --- /dev/null +++ b/tests/test_visitors/test_tokenize/test_wrong_comments/test_typed_ast.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +import pytest + +from wemake_python_styleguide.visitors.tokenize.wrong_comments import ( + WrongCommentVisitor, + WrongMagicCommentViolation, +) + + +@pytest.mark.parametrize('code', [ + '1 + "12" # type: ignore', + '1 + "12" # type:ignore', + 'total = 1000 # type is not clear', + 'print(12 + 3) # regular comment', + 'print(12 + 3) #', + 'print(12 + 3)', + '', +]) +def test_correct_comments( + parse_tokens, + assert_errors, + default_options, + code, +): + """Ensures that correct comments do not raise a warning.""" + file_tokens = parse_tokens(code) + + visitor = WrongCommentVisitor(default_options, file_tokens=file_tokens) + visitor.run() + + assert_errors(visitor, []) + + +@pytest.mark.parametrize('code', [ + 'total = 1000 # type: int', + 'total = 1000 # type:int', + 'total = 1000 # type: int ', + 'total = 1000#type:int', + 'numbs = [1, 2, 3] # type: missing', + 'numbs = [1, 2, 3] # type: List[int]', + 'numbs = [1, 2, 3] # type: List["int"]', + "numbs = [1, 2, 3] # type: List['int']", + 'field = SomeField() # type: drf.Field', + '# type: fixme', +]) +def test_incorrect_noqa_comment( + parse_tokens, + assert_errors, + default_options, + code, +): + """Ensures that incorrect `type` comments raise a warning.""" + file_tokens = parse_tokens(code) + + visitor = WrongCommentVisitor(default_options, file_tokens=file_tokens) + visitor.run() + + assert_errors(visitor, [WrongMagicCommentViolation]) diff --git a/wemake_python_styleguide/constants.py b/wemake_python_styleguide/constants.py index b9986040d..8ddba45bf 100644 --- a/wemake_python_styleguide/constants.py +++ b/wemake_python_styleguide/constants.py @@ -100,7 +100,8 @@ #: List of nested classes' names we allow to use. NESTED_CLASSES_WHITELIST = frozenset(( - 'Meta', + 'Meta', # django forms, models, drf, etc + 'Params', # factoryboy specific )) #: List of nested functions' names we allow to use. diff --git a/wemake_python_styleguide/errors/tokens.py b/wemake_python_styleguide/errors/tokens.py index e3d76fa3a..dd0ccf526 100644 --- a/wemake_python_styleguide/errors/tokens.py +++ b/wemake_python_styleguide/errors/tokens.py @@ -72,14 +72,11 @@ class WrongMagicCommentViolation(SimpleStyleViolation): We do not allow to use: 1. ``# noqa`` comment without specified errors - 2. ``# flake8: noqa`` comment to disable the whole file - 3. ``type: some_type`` comments to specify a type for ``typed_ast`` + 2. ``type: some_type`` comments to specify a type for ``typed_ast`` Reasoning: We cover several different use-cases in a single rule. ``# noqa`` comment is restricted because it can hide other errors. - ``# flake8: noqa`` is restricted because it is hard to tell - what files are ignored with this comment. ``type: int`` comment is restricted because we can already use type annotations instead. diff --git a/wemake_python_styleguide/visitors/tokenize/wrong_comments.py b/wemake_python_styleguide/visitors/tokenize/wrong_comments.py index b0891d201..d6f5dd652 100644 --- a/wemake_python_styleguide/visitors/tokenize/wrong_comments.py +++ b/wemake_python_styleguide/visitors/tokenize/wrong_comments.py @@ -22,13 +22,17 @@ from wemake_python_styleguide.visitors.base import BaseTokenVisitor NOQA_CHECK: Pattern = re.compile(r'^noqa:?($|[A-Z\d\,\s]+)') +TYPE_CHECK: Pattern = re.compile(r'^type:\s?([\w\d\[\]\'\"\.]+)$') class WrongCommentVisitor(BaseTokenVisitor): """Checks comment tokens.""" + def _get_comment_text(self, token: tokenize.TokenInfo) -> str: + return token.string[1:].strip() + def _check_noqa(self, token: tokenize.TokenInfo) -> None: - comment_text = token.string[1:].strip() + comment_text = self._get_comment_text(token) match: Match = NOQA_CHECK.match(comment_text) if not match: return @@ -36,9 +40,22 @@ def _check_noqa(self, token: tokenize.TokenInfo) -> None: excludes = match.groups()[0].strip() if not excludes: # We can not pass the actual line here, - # since it will be ignored due to `noqa` comment: + # since it will be ignored due to `# noqa` comment: self.add_error(WrongMagicCommentViolation(text=comment_text)) + def _check_typed_ast(self, token: tokenize.TokenInfo) -> None: + comment_text = self._get_comment_text(token) + match: Match = TYPE_CHECK.match(comment_text) + if not match: + return + + declared_type = match.groups()[0].strip() + if declared_type != 'ignore': + self.add_error( + WrongMagicCommentViolation(token, text=comment_text), + ) + def visit_comment(self, token: tokenize.TokenInfo) -> None: """Performs comment checks.""" self._check_noqa(token) + self._check_typed_ast(token)