diff --git a/CHANGELOG.md b/CHANGELOG.md index 6daffd2f8..d68792b0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,13 +2,29 @@ We follow Semantic Versions since the `0.1.0` release. + ## Version 0.0.8 aka The Complex Complexity ### Features +- Now all dependencies are direct, they will be installed together + with this package - Adds direct dependencies, now there's no need to install any extra packages -- Adds `TooDeepNesting` check -- Adds `--max-offset-blocks` option +- Adds `TooDeepNestingViolation` and `TooManyElifsViolation` checks +- Adds `--max-offset-blocks` and `--max-elifs` options +- Adds `TooManyModuleMembersViolation` and `TooManyMethodsViolation` checks +- Adds `--max-module-members` and `--max-methods` options +- Restricts to use `f` strings + +### Bugfixes + +- Removes incorrect `generic_visit()` calls +- Removes some unused `getattr()` calls +- Refactors how options are registered + +### Misc + +- Improved type support for options parsing ## Version 0.0.7 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b6cad8cf4..544ee289e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,6 +28,9 @@ To activate your `virtualenv` run `poetry shell`. ## Tests We use `pytest` and `flake8` for quality control. +We also use `wemake_python_styleguide` itself +to develop `wemake_python_styleguide`. + To run all tests: ```bash diff --git a/pyproject.lock b/pyproject.lock index 2d1363358..4ee234bf9 100644 --- a/pyproject.lock +++ b/pyproject.lock @@ -797,7 +797,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4" version = "1.23" [metadata] -content-hash = "74438a312f16545792a454d48412ac0945801840207190089246750dff05328d" +content-hash = "e1429750118cbce8d59257582e4f5afaa59df8f73be0e39f8a93fa046628ff28" platform = "*" python-versions = "^3.6 || ^3.7" @@ -810,7 +810,7 @@ babel = ["6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669", "8c certifi = ["376690d6f16d32f9d1fe8932551d80b23e9d393a8578c5633a2ed39a64861638", "456048c7e371c089d0a77a5212fb37a2c2dce1e24146e3b7e0261736aaeaa22a"] chardet = ["84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", "fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"] colorama = ["463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda", "48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1"] -coverage = ["03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba", "0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed", "104ab3934abaf5be871a583541e8829d6c19ce7bde2923b2751e0d3ca44db60a", "10a46017fef60e16694a30627319f38a2b9b52e90182dddb6e37dcdab0f4bf95", "15b111b6a0f46ee1a485414a52a7ad1d703bdf984e9ed3c288a4414d3871dcbd", "198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640", "1c383d2ef13ade2acc636556fd544dba6e14fa30755f26812f54300e401f98f2", "23d341cdd4a0371820eb2b0bd6b88f5003a7438bbedb33688cd33b8eae59affd", "28b2191e7283f4f3568962e373b47ef7f0392993bb6660d079c62bd50fe9d162", "2a5b73210bad5279ddb558d9a2bfedc7f4bf6ad7f3c988641d83c40293deaec1", "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"] +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"] execnet = ["a7a84d5fa07a089186a329528f127c9d73b9de57f1a1131b82bb5320ee651f6a", "fc155a6b553c66c838d1a22dba1dc9f5f505c43285a878c6f74a79c024750b83"] @@ -868,7 +868,7 @@ sphinx-readable-theme = ["f5fe65a2e112cb956b366df41e0fc894ff6b6f0e4a4814fcbff692 sphinxcontrib-napoleon = ["614b779888629f14dfdfad6c17bffbb6d3813a0a0917a9541651d85384d4d6bd", "cbb31953b15d2102c18f16f02591f92e2614a08ef0218c9e514e2dc4c4f9daf9"] sphinxcontrib-websupport = ["68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd", "9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9"] stevedore = ["1e153545aca7a6a49d8337acca4f41c212fbfa60bf864ecd056df0cafb9627e8", "c7eac1c0d95824c88b655273da5c17cdde6482b2739f47c30bf851dcc9d3c2c0"] -typed-ast = ["0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58", "10703d3cec8dcd9eef5a630a04056bbc898abc19bac5691612acba7d1325b66d", "1f6c4bd0bdc0f14246fd41262df7dfc018d65bb05f6e16390b7ea26ca454a291", "25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a", "29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9", "2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892", "3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9", "519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded", "57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa", "668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe", "68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd", "6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85", "79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6", "8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46", "898f818399cafcdb93cbbe15fc83a33d05f18e29fb498ddc09b0214cdfc7cd51", "94b091dc0f19291adcb279a108f5d38de2430411068b219f41b343c03b28fb1f", "a26863198902cda15ab4503991e8cf1ca874219e0118cbf07c126bce7c4db129", "a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c", "bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea", "c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863", "c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559", "edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87", "f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6"] +typed-ast = ["0948004fa228ae071054f5208840a1e88747a357ec1101c17217bfe99b299d58", "25d8feefe27eb0303b73545416b13d108c6067b846b543738a25ff304824ed9a", "29464a177d56e4e055b5f7b629935af7f49c196be47528cc94e0a7bf83fbc2b9", "2e214b72168ea0275efd6c884b114ab42e316de3ffa125b267e732ed2abda892", "3e0d5e48e3a23e9a4d1a9f698e32a542a4a288c871d33ed8df1b092a40f3a0f9", "519425deca5c2b2bdac49f77b2c5625781abbaf9a809d727d3a5596b30bb4ded", "57fe287f0cdd9ceaf69e7b71a2e94a24b5d268b35df251a88fef5cc241bf73aa", "668d0cec391d9aed1c6a388b0d5b97cd22e6073eaa5fbaa6d2946603b4871efe", "68ba70684990f59497680ff90d18e756a47bf4863c604098f10de9716b2c0bdd", "6de012d2b166fe7a4cdf505eee3aaa12192f7ba365beeefaca4ec10e31241a85", "79b91ebe5a28d349b6d0d323023350133e927b4de5b651a8aa2db69c761420c6", "8550177fa5d4c1f09b5e5f524411c44633c80ec69b24e0e98906dd761941ca46", "a8034021801bc0440f2e027c354b4eafd95891b573e12ff0418dec385c76785c", "bc978ac17468fe868ee589c795d06777f75496b1ed576d308002c8a5756fb9ea", "c05b41bc1deade9f90ddc5d988fe506208019ebba9f2578c622516fd201f5863", "c9b060bd1e5a26ab6e8267fd46fc9e02b54eb15fffb16d112d4c7b1c12987559", "edb04bdd45bfd76c8292c4d9654568efaedf76fe78eb246dde69bdb13b2dad87", "f19f2a4f547505fe9072e15f6f4ae714af51b5a681a97f187971f50c283193b6"] typing = ["4027c5f6127a6267a435201981ba156de91ad0d1d98e9ddc2aa173453453492d", "57dcf675a99b74d64dacf6fba08fb17cf7e3d5fdff53d4a30ea2a5e7e52543d4", "a4c8473ce11a65999c8f59cb093e70686b6c84c98df58c1dae9b3b196089858a"] typing-extensions = ["1c0a8e3b4ce55207a03dd0dcb98bc47a704c71f14fe4311ec860cc8af8f4bd27", "8b0962ecb92847974514b1724c8ae2b6dd1ffe86bcdfac429517f5e583ada658", "be7b05ddab71727fabf1f071365043cf034e4cdac9cade1f1d61a6cc526aaafe"] urllib3 = ["a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", "b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5"] diff --git a/pyproject.toml b/pyproject.toml index 63c43c81e..34c87d769 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wemake-python-styleguide" -version = "0.0.7" +version = "0.0.8" description = "Opinionated styleguide that we use in wemake.services" license = "MIT" diff --git a/setup.cfg b/setup.cfg index 92e62c64d..0a218a37b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,8 +23,10 @@ accept-encodings = utf-8 [tool:pytest] flake8-ignore = - # These imports are API declaration: + # These function names are part of 3d party API: wemake_python_styleguide/visitors/*.py N802 + # These modules should contain a lot of classes: + wemake_python_styleguide/errors/*.py Z208 # Disable some pydocstyle checks: *.py D100 D104 D106 D401 diff --git a/tests/test_visitors/conftest.py b/tests/test_visitors/conftest.py index e87072df1..d24b79a3c 100644 --- a/tests/test_visitors/conftest.py +++ b/tests/test_visitors/conftest.py @@ -11,15 +11,6 @@ from wemake_python_styleguide.options import defaults from wemake_python_styleguide.visitors.base.visitor import BaseNodeVisitor -Options = namedtuple('options', [ - 'max_arguments', - 'max_expressions', - 'max_local_variables', - 'max_returns', - 'min_variable_length', - 'max_offset_blocks', -]) - @pytest.fixture(scope='session') def parse_ast_tree(): @@ -46,18 +37,24 @@ def factory(visiter: BaseNodeVisitor, errors: Sequence[str]): @pytest.fixture(scope='session') def options(): """Returns the options builder.""" + default_values = { + 'max_arguments': defaults.MAX_ARGUMENTS, + 'max_expressions': defaults.MAX_EXPRESSIONS, + 'max_local_variables': defaults.MAX_LOCAL_VARIABLES, + 'max_returns': defaults.MAX_RETURNS, + 'min_variable_length': defaults.MIN_VARIABLE_LENGTH, + 'max_offset_blocks': defaults.MAX_OFFSET_BLOCKS, + 'max_elifs': defaults.MAX_ELIFS, + 'max_module_members': defaults.MAX_MODULE_MEMBERS, + 'max_methods': defaults.MAX_METHODS, + } + + Options = namedtuple('options', default_values.keys()) + def factory(**kwargs): - default_values = { - 'max_arguments': defaults.MAX_ARGUMENTS, - 'max_expressions': defaults.MAX_EXPRESSIONS, - 'max_local_variables': defaults.MAX_LOCAL_VARIABLES, - 'max_returns': defaults.MAX_RETURNS, - 'min_variable_length': defaults.MIN_VARIABLE_LENGTH, - 'max_offset_blocks': defaults.MAX_OFFSET_BLOCKS, - } - - default_values.update(kwargs) - return Options(**default_values) + final_options = default_values.copy() + final_options.update(kwargs) + return Options(**final_options) return factory diff --git a/tests/test_visitors/test_complexity/test_counts/test_method_counts.py b/tests/test_visitors/test_complexity/test_counts/test_method_counts.py new file mode 100644 index 000000000..4ae0feb91 --- /dev/null +++ b/tests/test_visitors/test_complexity/test_counts/test_method_counts.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- + +import pytest + +from wemake_python_styleguide.visitors.complexity.counts import ( + MethodMembersVisitor, + TooManyMethodsViolation, +) + +module_without_methods = """ +def first(): ... + +def second(): ... +""" + +class_with_methods = """ +class First(object): + def method(self): ... + + def method2(self): ... +""" + +class_with_class_methods = """ +class First(object): + @classmethod + def method(cls): ... + + @classmethod + def method2(cls): ... +""" + + +@pytest.mark.parametrize('code', [ + module_without_methods, + class_with_methods, + class_with_class_methods, +]) +def test_method_counts_normal( + assert_errors, parse_ast_tree, code, default_options, +): + """Testing that regular classes and functions work well.""" + tree = parse_ast_tree(code) + + visiter = MethodMembersVisitor(default_options) + visiter.visit(tree) + + assert_errors(visiter, []) + + +@pytest.mark.parametrize('code', [ + class_with_methods, + class_with_class_methods, +]) +def test_method_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_methods=1) + visiter = MethodMembersVisitor(option_values) + visiter.visit(tree) + + assert_errors(visiter, [TooManyMethodsViolation]) diff --git a/tests/test_visitors/test_complexity/test_counts/test_module_counts.py b/tests/test_visitors/test_complexity/test_counts/test_module_counts.py new file mode 100644 index 000000000..3ead72c81 --- /dev/null +++ b/tests/test_visitors/test_complexity/test_counts/test_module_counts.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- + +import pytest + +from wemake_python_styleguide.visitors.complexity.counts import ( + ModuleMembersVisitor, + TooManyModuleMembersViolation, +) + +module_with_function_and_class = """ +def first(): ... + +class Second(object): ... +""" + +module_with_methods = """ +class First(object): + def method(self): ... + +class Second(object): + def method2(self): ... +""" + + +@pytest.mark.parametrize('code', [ + module_with_function_and_class, + module_with_methods, +]) +def test_module_counts_normal( + assert_errors, parse_ast_tree, code, default_options, +): + """Testing that classes and functions in a module work well.""" + tree = parse_ast_tree(code) + + visiter = ModuleMembersVisitor(default_options) + visiter.visit(tree) + + assert_errors(visiter, []) + + +@pytest.mark.parametrize('code', [ + module_with_function_and_class, + module_with_methods, +]) +def test_module_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_module_members=1) + visiter = ModuleMembersVisitor(option_values) + visiter.visit(tree) + + assert_errors(visiter, [TooManyModuleMembersViolation]) diff --git a/tests/test_visitors/test_complexity/test_function/test_elifs.py b/tests/test_visitors/test_complexity/test_function/test_elifs.py new file mode 100644 index 000000000..97eac539e --- /dev/null +++ b/tests/test_visitors/test_complexity/test_function/test_elifs.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- + +import pytest + +from wemake_python_styleguide.visitors.complexity.function import ( + FunctionComplexityVisitor, + TooManyElifsViolation, +) + +function_with_elifs = """ +def test_module(): + if 1 > 2: + print(1) + elif 2 > 3: + print(2) + elif 3 > 4: + print(3) + else: + print(4) +""" + +function_with_raw_if = """ +def function(): + if 1 == 2: + print(1) +""" + +function_with_if_else = """ +def function(param): + if 1 == 2: + print(1) + else: + print(2) +""" + + +@pytest.mark.parametrize('code', [ + function_with_elifs, + function_with_raw_if, + function_with_if_else, +]) +def test_elif_correct_count( + assert_errors, parse_ast_tree, code, default_options, +): + """Testing that all `if`/`elif`/`else` stuff is allowed.""" + tree = parse_ast_tree(code) + + visiter = FunctionComplexityVisitor(default_options) + visiter.visit(tree) + + assert_errors(visiter, []) + + +def test_elif_incorrect_count(assert_errors, parse_ast_tree, options): + """Testing that incorrect number of `elif` stuff is restricted.""" + tree = parse_ast_tree(function_with_elifs) + + option_values = options(max_elifs=1) + visiter = FunctionComplexityVisitor(option_values) + visiter.visit(tree) + + assert_errors(visiter, [TooManyElifsViolation]) diff --git a/tests/test_visitors/test_complexity/test_offset/test_offset_visitor.py b/tests/test_visitors/test_complexity/test_offset_visitor.py similarity index 84% rename from tests/test_visitors/test_complexity/test_offset/test_offset_visitor.py rename to tests/test_visitors/test_complexity/test_offset_visitor.py index e6a4cca7f..b07d3d597 100644 --- a/tests/test_visitors/test_complexity/test_offset/test_offset_visitor.py +++ b/tests/test_visitors/test_complexity/test_offset_visitor.py @@ -13,6 +13,12 @@ def container(): x = 1 """ +nested_if2 = """ +def container(): + if some_value: + call_other() +""" + nested_for = """ def container(): for i in '123': @@ -27,6 +33,15 @@ def container(): raise """ +nested_try2 = """ +def container(): + if some: + try: + some() + except Exception: + raise +""" + nested_with = """ if True: with open('some') as temp: @@ -43,8 +58,10 @@ def container(): @pytest.mark.parametrize('code', [ nested_if, + nested_if2, nested_for, nested_try, + nested_try2, nested_with, nested_while, ]) @@ -60,8 +77,10 @@ def test_nested_offset(assert_errors, parse_ast_tree, code, default_options): @pytest.mark.parametrize('code, number_of_errors', [ (nested_if, 1), + (nested_if2, 1), (nested_for, 1), (nested_try, 2), + (nested_try2, 4), (nested_with, 1), (nested_while, 1), ]) diff --git a/tests/test_visitors/test_wrong_string.py b/tests/test_visitors/test_wrong_string.py new file mode 100644 index 000000000..1c15b235d --- /dev/null +++ b/tests/test_visitors/test_wrong_string.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- + +import pytest + +from wemake_python_styleguide.visitors.wrong_string import ( + FormattedStringViolation, + WrongStringVisitor, +) + +regular_string = "'some value'" +string_variable = "some = '123'" +formated_string = "'x + y = {0}'.format(2)" +key_formated_string = "'x + y = {res}'.format(res=2)" +variable_format = """ +some = 'x = {0}' +some.format(2) +""" + +f_string = "f'x + y = {2 + 2}'" +f_empty_string = "f''" + + +@pytest.mark.parametrize('code', [ + regular_string, + string_variable, + formated_string, + key_formated_string, + variable_format, +]) +def test_string_normal( + assert_errors, parse_ast_tree, code, default_options, +): + """Testing that regular strings work well.""" + tree = parse_ast_tree(code) + + visiter = WrongStringVisitor(default_options) + visiter.visit(tree) + + assert_errors(visiter, []) + + +@pytest.mark.parametrize('code', [ + f_string, + f_empty_string, +]) +def test_wrong_string(assert_errors, parse_ast_tree, code, default_options): + """Testing that violations are raised when reaching max value.""" + tree = parse_ast_tree(code) + + visiter = WrongStringVisitor(default_options) + visiter.visit(tree) + + assert_errors(visiter, [FormattedStringViolation]) diff --git a/wemake_python_styleguide/checker.py b/wemake_python_styleguide/checker.py index 59a992f1b..dc66d69c5 100644 --- a/wemake_python_styleguide/checker.py +++ b/wemake_python_styleguide/checker.py @@ -3,10 +3,20 @@ from ast import Module from typing import Generator +from flake8.options.manager import OptionManager + from wemake_python_styleguide.compat import maybe_set_parent from wemake_python_styleguide.options.config import Configuration -from wemake_python_styleguide.types import CheckResult, ConfigurationOptions +from wemake_python_styleguide.types import ( + CheckResult, + ConfigurationOptions, + VisitorSequence, +) from wemake_python_styleguide.version import version +from wemake_python_styleguide.visitors.complexity.counts import ( + MethodMembersVisitor, + ModuleMembersVisitor, +) from wemake_python_styleguide.visitors.complexity.function import ( FunctionComplexityVisitor, ) @@ -29,7 +39,7 @@ ) #: Visitors that should be working by default: -ENABLED_VISITORS = ( +ENABLED_VISITORS: VisitorSequence = [ # Styling and correctness: WrongRaiseVisitor, WrongFunctionCallVisitor, @@ -43,7 +53,9 @@ FunctionComplexityVisitor, NestedComplexityVisitor, OffsetVisitor, -) + ModuleMembersVisitor, + MethodMembersVisitor, +] class Checker(object): @@ -60,13 +72,13 @@ class Checker(object): config = Configuration() options: ConfigurationOptions - def __init__(self, tree: Module, filename: str = '-') -> None: + def __init__(self, tree: Module, filename: str = 'stdin') -> None: """Creates new checker instance.""" self.tree = maybe_set_parent(tree) self.filename = filename @classmethod - def add_options(cls, parser): # TODO: types + def add_options(cls, parser: OptionManager): """Calls Configuration instance method for registering options.""" cls.config.register_options(parser) @@ -83,7 +95,7 @@ def run(self) -> Generator[CheckResult, None, None]: After all configuration is parsed and passed. """ for visitor_class in ENABLED_VISITORS: - visiter = visitor_class(self.options) + visiter = visitor_class(self.options, filename=self.filename) visiter.visit(self.tree) for error in visiter.errors: diff --git a/wemake_python_styleguide/compat.py b/wemake_python_styleguide/compat.py index e623e7323..7890eaf30 100644 --- a/wemake_python_styleguide/compat.py +++ b/wemake_python_styleguide/compat.py @@ -14,6 +14,6 @@ def maybe_set_parent(tree: ast.AST) -> ast.AST: for statement in ast.walk(tree): for child in ast.iter_child_nodes(statement): if not hasattr(child, 'parent'): # noqa: Z113 - setattr(child, 'parent', statement) # noqa: Z113 + setattr(child, 'parent', statement) return tree diff --git a/wemake_python_styleguide/constants.py b/wemake_python_styleguide/constants.py index 63cded770..d7f5e7e71 100644 --- a/wemake_python_styleguide/constants.py +++ b/wemake_python_styleguide/constants.py @@ -10,6 +10,8 @@ import sys from typing import Tuple +# TODO: 'stdin' filename should be a constant + #: List of functions we forbid to use. BAD_FUNCTIONS = frozenset(( # Code generation: @@ -69,6 +71,9 @@ 'handler', 'file', 'klass', + 'foo', + 'bar', + 'baz', ) if sys.version_info < (3, 7): # pragma: no cover @@ -83,9 +88,10 @@ #: List of magic methods that are forbiden to use. BAD_MAGIC_METHODS = frozenset(( - '__del__', # since we don't use `del` - '__delitem__', # since we don't use `del` - '__delete__', # since we don't use `del` + # Since we don't use `del`: + '__del__', + '__delitem__', + '__delete__', '__dir__', # since we don't use `dir()` '__delattr__', # since we don't use `delattr()` diff --git a/wemake_python_styleguide/errors/__init__.py b/wemake_python_styleguide/errors/__init__.py index e69de29bb..40a96afc6 100644 --- a/wemake_python_styleguide/errors/__init__.py +++ b/wemake_python_styleguide/errors/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/wemake_python_styleguide/errors/complexity.py b/wemake_python_styleguide/errors/complexity.py index f50f42cec..ec6b7af38 100644 --- a/wemake_python_styleguide/errors/complexity.py +++ b/wemake_python_styleguide/errors/complexity.py @@ -118,19 +118,21 @@ class TooManyArgumentsViolation(BaseStyleViolation): _code = 'Z203' -class TooManyBranchesViolation(BaseStyleViolation): +class TooManyElifsViolation(BaseStyleViolation): """ - This rule forbids to have to many branches in a function. + This rule forbids to use many `elif` branches. - When there are too many branches, functions are hard to test. - They are also hard to read and hard to change and read. + This rule is specifically important, becase many `elif` branches indicate + a complex flow in your design: you are reimplementing `switch` in python. + + There are different design patters to use instead. Note: Returns Z204 as error code """ - _error_tmpl = '{0} Found too many branches "{1}"' + _error_tmpl = '{0} Found too many "{1}" branches' _code = 'Z204' @@ -183,3 +185,51 @@ class TooDeepNestingViolation(BaseStyleViolation): _error_tmpl = '{0} Found too deep nesting "{1}"' _code = 'Z207' + + +class TooManyModuleMembersViolation(BaseStyleViolation): + """ + This rule forbids to have many classes and functions in a single module. + + Having many classes and functions in a single module is a bad thing. + Because soon it will be hard to read this code and understand it. + + It is better to split this module into several modules or a package. + + We do not make any differences between classes and functions in this check. + They are treated as the same unit of logic. + We also do no care about functions and classes been public or not. + However, methods are counted separatelly on a per-class basis. + + Note: + Returns Z208 as error code + + """ + + _error_tmpl = '{0} Found too many members "{1}"' + _code = 'Z208' + + +class TooManyMethodsViolation(BaseStyleViolation): + """ + This rule forbids to have many methods in a single class. + + We do not make any difference between instance and class methods. + We also do no care about functions and classes been public or not. + + What to do if you have too many methods in a single class? + Split this class in several classes. + Then use composition or inheritance to refactor your code. + + This will protect you from "God object" anti-pattern. + See: https://en.wikipedia.org/wiki/God_object + + This rule do not count attributes of a class. + + Note: + Returns Z209 as error code + + """ + + _error_tmpl = '{0} Found too many methods "{1}"' + _code = 'Z209' diff --git a/wemake_python_styleguide/errors/general.py b/wemake_python_styleguide/errors/general.py index e02180a4a..8934093f6 100644 --- a/wemake_python_styleguide/errors/general.py +++ b/wemake_python_styleguide/errors/general.py @@ -4,6 +4,7 @@ These rules checks some general rules. Like: + 1. Naming 2. Using some builtins 3. Using keywords @@ -187,3 +188,24 @@ class WrongModuleMetadataViolation(BaseStyleViolation): _error_tmpl = '{0} Found wrong metadata variable {1}' _code = 'Z117' + + +class FormattedStringViolation(BaseStyleViolation): + """ + This rule forbids to use `f` strings. + + Example:: + + # Wrong: + f'Result is: {2 + 2}' + + # Correct: + 'Result is: {0}'.format(2 + 2) + + Note: + Returns Z118 as error code + + """ + + _error_tmpl = '{0} Found `f` string {1}' + _code = 'Z118' diff --git a/wemake_python_styleguide/logics/functions.py b/wemake_python_styleguide/logics/functions.py index 7c7fd9060..fa7f0d9eb 100644 --- a/wemake_python_styleguide/logics/functions.py +++ b/wemake_python_styleguide/logics/functions.py @@ -44,5 +44,8 @@ def is_method(function_type: Optional[str]) -> bool: >>> is_method('classmethod') True + >>> is_method('') + False + """ return function_type in ['method', 'classmethod'] diff --git a/wemake_python_styleguide/logics/imports.py b/wemake_python_styleguide/logics/imports.py new file mode 100644 index 000000000..5517af1e0 --- /dev/null +++ b/wemake_python_styleguide/logics/imports.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- + +import ast + +from wemake_python_styleguide.types import AnyImport + + +def get_error_text(node: AnyImport) -> str: + """Returns correct error text for import nodes.""" + module = getattr(node, 'module', None) + if module is not None: + return module + + if isinstance(node, ast.Import): + return node.names[0].name + return '.' diff --git a/wemake_python_styleguide/options/config.py b/wemake_python_styleguide/options/config.py index caa06b52a..6b48c6825 100644 --- a/wemake_python_styleguide/options/config.py +++ b/wemake_python_styleguide/options/config.py @@ -1,8 +1,38 @@ # -*- coding: utf-8 -*- +from typing import Dict, Sequence, Union + +from flake8.options.manager import OptionManager + from wemake_python_styleguide.options import defaults +class _Option(object): + """This class represent `flake8` option object.""" + + def __init__( + self, + name: str, + default_value: int, + help_text: str, + option_type: type = int, + parse_from_config: bool = True, + ) -> None: + self.name = name + self.default_value = default_value + self.help_text = help_text + self.option_type = option_type + self.parse_from_config = parse_from_config + + def to_option(self) -> Dict[str, Union[str, int, type]]: + return { + 'parse_from_config': self.parse_from_config, + 'type': self.option_type, + 'default': self.default_value, + 'help': self.help_text, + } + + class Configuration(object): """ Provides configuration options for `wemake-python-styleguide` plugin. @@ -34,55 +64,73 @@ class Configuration(object): variable name, defaults to ``MIN_VARIABLE_LENGTH`` - `max_offset_blocks` - maximum number of block to nest expressions, defaults to ``MAX_OFFSET_BLOCKS`` + - `max_elifs` - maximum number of `elif` blocks, defaults to ``MAX_ELIFS`` + - `max_module_members` - maximum number of classes and functions + in a single module, defaults to ``MAX_MODULE_MEMBERS`` + - `max_methods` - maximum number of methods in a single class, + defaults to ``MAX_METHODS`` """ - def register_options(self, parser) -> None: # TODO: types + def _all_options(self) -> Sequence[_Option]: + 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.', + ), + ] + + def register_options(self, parser: OptionManager) -> None: """Registers options for our plugin.""" - parser.add_option( - '--max-returns', - parse_from_config=True, - type='int', - default=defaults.MAX_RETURNS, - help='Maximum allowed number of return statements in one function.', - ) - - parser.add_option( - '--max-local-variables', - parse_from_config=True, - type='int', - default=defaults.MAX_LOCAL_VARIABLES, - help='Maximum allowed number of local variables in one function.', - ) - - parser.add_option( - '--max-expressions', - parse_from_config=True, - type='int', - default=defaults.MAX_EXPRESSIONS, - help='Maximum allowed number of expressions in one function.', - ) - - parser.add_option( - '--max-arguments', - parse_from_config=True, - type='int', - default=defaults.MAX_ARGUMENTS, - help='Maximum allowed number of arguments in one function.', - ) - - parser.add_option( - '--min-variable-length', - parse_from_config=True, - type='int', - default=defaults.MIN_VARIABLE_LENGTH, - help='Minimum required length of the variable name.', - ) - - parser.add_option( - '--max-offset-blocks', - parse_from_config=True, - type='int', - default=defaults.MAX_OFFSET_BLOCKS, - help='Maximum number of blocks to nest different structures.', - ) + options = self._all_options() + for option in options: + parser.add_option(option.name, **option.to_option()) diff --git a/wemake_python_styleguide/options/defaults.py b/wemake_python_styleguide/options/defaults.py index 1b1c20975..bc669a3d1 100644 --- a/wemake_python_styleguide/options/defaults.py +++ b/wemake_python_styleguide/options/defaults.py @@ -31,3 +31,12 @@ #: 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 + +#: Maximum number of classes and functions in a single module: +MAX_MODULE_MEMBERS = 7 + +#: Maximum number of methods in a single class: +MAX_METHODS = 7 diff --git a/wemake_python_styleguide/types.py b/wemake_python_styleguide/types.py index ef01dc924..86816bfb6 100644 --- a/wemake_python_styleguide/types.py +++ b/wemake_python_styleguide/types.py @@ -7,16 +7,26 @@ """ import ast -from typing import Tuple, Union +from typing import TYPE_CHECKING, Sequence, Tuple, Type, Union from typing_extensions import Protocol +if TYPE_CHECKING: # pragma: no cover + # This solves cycle imports problem: + from .visitors.base import visitor # noqa: Z100,Z101,F401 + +#: Visitors container, that has all enable visitors' classes: +VisitorSequence = Sequence[Type['visitor.BaseNodeVisitor']] + #: In cases we need to work with both import types: AnyImport = Union[ast.Import, ast.ImportFrom] #: Flake8 API format to return error messages: CheckResult = Tuple[int, int, str, type] +#: Code members that we count in a module: +ModuleMembers = Union[ast.FunctionDef, ast.ClassDef] + class ConfigurationOptions(Protocol): """ @@ -34,3 +44,6 @@ class or structure. max_expressions: int min_variable_length: int max_offset_blocks: int + max_elifs: int + max_module_members: int + max_methods: int diff --git a/wemake_python_styleguide/visitors/base/visitor.py b/wemake_python_styleguide/visitors/base/visitor.py index baa204697..f753e9e62 100644 --- a/wemake_python_styleguide/visitors/base/visitor.py +++ b/wemake_python_styleguide/visitors/base/visitor.py @@ -13,16 +13,18 @@ class BaseNodeVisitor(NodeVisitor): Attributes: options: contains the options objects passed and parsed by `flake8`. + filename: filename passed by `flake8`. errors: list of errors for the specific checker. """ - options: ConfigurationOptions - - def __init__(self, options: ConfigurationOptions) -> None: + def __init__( + self, options: ConfigurationOptions, filename: str = 'stdin', + ) -> None: """Creates new visitor instance.""" super().__init__() self.options = options + self.filename = filename self.errors: List[BaseStyleViolation] = [] def add_error(self, error: BaseStyleViolation) -> None: diff --git a/wemake_python_styleguide/visitors/complexity/counts.py b/wemake_python_styleguide/visitors/complexity/counts.py index 777e4e5a4..c72d06780 100644 --- a/wemake_python_styleguide/visitors/complexity/counts.py +++ b/wemake_python_styleguide/visitors/complexity/counts.py @@ -1,3 +1,86 @@ # -*- coding: utf-8 -*- -# TODO: count the number of functions per file/class, classes per file +import ast +from collections import defaultdict +from typing import DefaultDict + +from wemake_python_styleguide.errors.complexity import ( + 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.visitors.base.visitor import BaseNodeVisitor + + +class ModuleMembersVisitor(BaseNodeVisitor): + """Counts classes and functions in a module.""" + + def __init__(self, *args, **kwargs) -> None: + """Creates a counter for tracked metrics.""" + super().__init__(*args, **kwargs) + self._public_items_count = 0 + + def _check_members_count(self, node: ModuleMembers): + """This method increases the number of module members.""" + parent = getattr(node, 'parent', None) + is_real_method = is_method(getattr(node, 'function_type', 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 visit_ClassDef(self, node: ast.ClassDef): + """ + Counts the number of `class`es in a single module. + + Raises: + TooManyModuleMembersViolation + + """ + self._check_members_count(node) + self.generic_visit(node) + + def visit_FunctionDef(self, node: ast.FunctionDef): + """ + Counts the number of functions in a single module. + + Raises: + TooManyModuleMembersViolation + + """ + self._check_members_count(node) + self.generic_visit(node) + + +class MethodMembersVisitor(BaseNodeVisitor): + """Counts methods in a single class.""" + + def __init__(self, *args, **kwargs) -> None: + """Creates a counter for tracked methods in different classes.""" + super().__init__(*args, **kwargs) + self._methods: DefaultDict[ast.ClassDef, int] = defaultdict(int) + + def _check_method(self, node: ast.FunctionDef): + 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 visit_FunctionDef(self, node: ast.FunctionDef): + """ + Counts the number of methods in a single class. + + Raises: + TooManyMethodsViolation + + """ + self._check_method(node) + self.generic_visit(node) diff --git a/wemake_python_styleguide/visitors/complexity/function.py b/wemake_python_styleguide/visitors/complexity/function.py index 86289092c..ce114b527 100644 --- a/wemake_python_styleguide/visitors/complexity/function.py +++ b/wemake_python_styleguide/visitors/complexity/function.py @@ -1,62 +1,32 @@ # -*- coding: utf-8 -*- -# TODO: implement TooDeepNestingViolation, TooManyBranchesViolation import ast from collections import defaultdict -from typing import DefaultDict, List +from typing import DefaultDict, List, Type +from wemake_python_styleguide.errors.base import BaseStyleViolation from wemake_python_styleguide.errors.complexity import ( TooManyArgumentsViolation, + TooManyElifsViolation, TooManyExpressionsViolation, TooManyLocalsViolation, 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.types import ConfigurationOptions from wemake_python_styleguide.visitors.base.visitor import BaseNodeVisitor -class FunctionComplexityVisitor(BaseNodeVisitor): - """ - This class checks for complexity inside functions. +class _ComplexityCounter(object): + """Helper class to encapsulate logics from the visitor.""" - This includes: - 1. Number of arguments - 2. Number of `return`s - 3. Number of expressions - 4. Number of local variables - """ - - def __init__(self, options: ConfigurationOptions) -> None: - """Creates config parser instance and counters for tracked metrics.""" - super().__init__(options) + 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 _check_arguments_count(self, node: ast.FunctionDef): - counter = 0 - has_extra_self_or_cls = 0 - if is_method(getattr(node, 'function_type', None)): - has_extra_self_or_cls = 1 - - counter += len(node.args.args) - counter += len(node.args.kwonlyargs) - - if node.args.vararg: - counter += 1 - - if node.args.kwarg: - counter += 1 - - if counter > self.options.max_arguments + has_extra_self_or_cls: - self.add_error( - TooManyArgumentsViolation(node, text=node.name), - ) - - # TODO: move this logics inside into another place: def _update_variables(self, function: ast.FunctionDef, variable_name: str): """ Increases the counter of local variables. @@ -70,38 +40,78 @@ def _update_variables(self, function: ast.FunctionDef, variable_name: str): limit_exceeded = has_just_exceeded_limit( len(function_variables), - self.options.max_local_variables, + self.delegate.options.max_local_variables, ) if limit_exceeded: - self.add_error( + self.delegate.add_error( TooManyLocalsViolation(function, text=function.name), ) - # TODO: move this logics inside into another place: - def _update_returns(self, function: ast.FunctionDef): - self.returns[function.name] += 1 + def _update_counter( + self, + function: ast.FunctionDef, + counter: DefaultDict[str, int], + max_value: int, + exception: Type[BaseStyleViolation], + ): + counter[function.name] += 1 limit_exceeded = has_just_exceeded_limit( - self.returns[function.name], - self.options.max_returns, + counter[function.name], max_value, ) if limit_exceeded: - self.add_error( - TooManyReturnsViolation(function, text=function.name), + self.delegate.add_error(exception(function, text=function.name)) + + def _update_elifs(self, node: ast.If, count: int = 0): + 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)) + + def _check_sub_node(self, node: ast.FunctionDef, sub_node): + 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) - # TODO: move this logics inside into another place: - def _update_expression(self, function: ast.FunctionDef): - self.expressions[function.name] += 1 - limit_exceeded = has_just_exceeded_limit( - self.expressions[function.name], - self.options.max_expressions, - ) - if limit_exceeded: - self.add_error( - TooManyExpressionsViolation(function, text=function.name), + def check_arguments_count(self, node: ast.FunctionDef): + """Checks the number of the arguments in a function.""" + counter = 0 + has_extra_arg = 0 + if is_method(getattr(node, 'function_type', 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), ) - def _check_function_complexity(self, node: ast.FunctionDef): + def check_function_complexity(self, node: ast.FunctionDef): """ In this function we iterate all the internal body's node. @@ -109,17 +119,27 @@ def _check_function_complexity(self, node: ast.FunctionDef): """ for body_item in node.body: for sub_node in ast.walk(body_item): - is_variable = isinstance(sub_node, ast.Name) - context = getattr(sub_node, 'ctx', None) + self._check_sub_node(node, sub_node) - if is_variable and isinstance(context, ast.Store): - self._update_variables(node, getattr(sub_node, 'id')) - if isinstance(sub_node, ast.Return): - self._update_returns(node) +class FunctionComplexityVisitor(BaseNodeVisitor): + """ + This class checks for complexity inside functions. + + This includes: + + 1. Number of arguments + 2. Number of `return`s + 3. Number of expressions + 4. Number of local variables + 5. Number of `elif`s + + """ - if isinstance(sub_node, ast.Expr): - self._update_expression(node) + def __init__(self, *args, **kwargs) -> None: + """Creates a counter for tracked metrics.""" + super().__init__(*args, **kwargs) + self._counter = _ComplexityCounter(self) def visit_FunctionDef(self, node: ast.FunctionDef): """ @@ -130,8 +150,9 @@ def visit_FunctionDef(self, node: ast.FunctionDef): TooManyReturnsViolation TooManyLocalsViolation TooManyArgumentsViolation + TooManyElifsViolation """ - self._check_arguments_count(node) - self._check_function_complexity(node) + self._counter.check_arguments_count(node) + self._counter.check_function_complexity(node) self.generic_visit(node) diff --git a/wemake_python_styleguide/visitors/wrong_import.py b/wemake_python_styleguide/visitors/wrong_import.py index 16eb30328..7c81e94a7 100644 --- a/wemake_python_styleguide/visitors/wrong_import.py +++ b/wemake_python_styleguide/visitors/wrong_import.py @@ -10,50 +10,59 @@ NestedImportViolation, SameAliasImportViolation, ) +from wemake_python_styleguide.logics.imports import get_error_text from wemake_python_styleguide.types import AnyImport from wemake_python_styleguide.visitors.base.visitor import BaseNodeVisitor -class WrongImportVisitor(BaseNodeVisitor): - """This class is responsible for finding wrong imports.""" +class _ImportsChecker(object): - def _get_error_text(self, node: ast.AST) -> str: - module = getattr(node, 'module', None) - if module is not None: - return module + def __init__(self, delegate: 'WrongImportVisitor') -> None: + self.delegate = delegate - if isinstance(node, ast.Import): - return node.names[0].name - return '.' - - def _check_nested_import(self, node: ast.AST): - text = self._get_error_text(node) + def check_nested_import(self, node: AnyImport): + text = get_error_text(node) parent = getattr(node, 'parent', None) if not isinstance(parent, ast.Module): - self.add_error(NestedImportViolation(node, text=text)) + self.delegate.add_error(NestedImportViolation(node, text=text)) - def _check_local_import(self, node: ast.ImportFrom): - text = self._get_error_text(node) + def check_local_import(self, node: ast.ImportFrom): + text = get_error_text(node) if node.level != 0: - self.add_error(LocalFolderImportViolation(node, text=text)) + self.delegate.add_error( + LocalFolderImportViolation(node, text=text), + ) - def _check_future_import(self, node: ast.ImportFrom): + def check_future_import(self, node: ast.ImportFrom): if node.module == '__future__': for alias in node.names: if alias.name not in FUTURE_IMPORTS_WHITELIST: - self.add_error( + self.delegate.add_error( FutureImportViolation(node, text=alias.name), ) - def _check_dotted_raw_import(self, node: ast.Import): + def check_dotted_raw_import(self, node: ast.Import): for alias in node.names: if '.' in alias.name: - self.add_error(DottedRawImportViolation(node, text=alias.name)) + self.delegate.add_error( + DottedRawImportViolation(node, text=alias.name), + ) - def _check_alias(self, node: AnyImport): + def check_alias(self, node: AnyImport): for alias in node.names: if alias.asname == alias.name: - self.add_error(SameAliasImportViolation(node, text=alias.name)) + self.delegate.add_error( + SameAliasImportViolation(node, text=alias.name), + ) + + +class WrongImportVisitor(BaseNodeVisitor): + """This class is responsible for finding wrong imports.""" + + def __init__(self, *args, **kwargs) -> None: + """Creates a checher for tracked violations.""" + super().__init__(*args, **kwargs) + self._checker = _ImportsChecker(self) def visit_Import(self, node: ast.Import): """ @@ -65,9 +74,9 @@ def visit_Import(self, node: ast.Import): NestedImportViolation """ - self._check_nested_import(node) - self._check_dotted_raw_import(node) - self._check_alias(node) + self._checker.check_nested_import(node) + self._checker.check_dotted_raw_import(node) + self._checker.check_alias(node) self.generic_visit(node) def visit_ImportFrom(self, node: ast.ImportFrom): @@ -81,8 +90,8 @@ def visit_ImportFrom(self, node: ast.ImportFrom): FutureImportViolation """ - self._check_local_import(node) - self._check_nested_import(node) - self._check_future_import(node) - self._check_alias(node) + self._checker.check_local_import(node) + self._checker.check_nested_import(node) + self._checker.check_future_import(node) + self._checker.check_alias(node) self.generic_visit(node) diff --git a/wemake_python_styleguide/visitors/wrong_string.py b/wemake_python_styleguide/visitors/wrong_string.py new file mode 100644 index 000000000..531a7781d --- /dev/null +++ b/wemake_python_styleguide/visitors/wrong_string.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +from wemake_python_styleguide.errors.general import FormattedStringViolation +from wemake_python_styleguide.visitors.base.visitor import BaseNodeVisitor + + +class WrongStringVisitor(BaseNodeVisitor): + """Restricts to use `f` strings.""" + + def visit_JoinedStr(self, node): # type is not defined in `ast` yet + """ + Restricts to use `f` strings. + + Raises: + FormattedStringViolation + + """ + self.add_error(FormattedStringViolation(node)) + self.generic_visit(node)