diff --git a/CHANGELOG.md b/CHANGELOG.md index caf7d538c..c92113aca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ We follow Semantic Versions. +## Version 0.0.5 + +### Features + +- We now allow `generator_stop` to be a `__future__` import +- We now restrict dotted raw imports like: `import os.path` +- We now check import aliases as regular variable names + +### Misc + +- We have added a `CONTRIBUTING.md` file to help new contributors + + ## Version 0.0.4 ### Features diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..b6cad8cf4 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,69 @@ +# How to contribute + +If you want to start working on this project, +you will need to get familiar with these APIs: + +- [Writing a `flake8` plugin](http://flake8.pycqa.org/en/latest/plugin-development/) +- [Using `ast` module](https://docs.python.org/3/library/ast.html) + +It is also recommended to take a look at these resources: + +- [Missing `ast` guide](https://greentreesnakes.readthedocs.io/en/latest/) + + +## Dependencies + +We use [`poetry`](https://github.com/sdispater/poetry) to manage the dependencies. + +To install them you would need to run two commands: + +```bash +poetry install +poetry develop +``` + +To activate your `virtualenv` run `poetry shell`. + + +## Tests + +We use `pytest` and `flake8` for quality control. +To run all tests: + +```bash +pytest +``` + +This step is mandatory during the CI. + + +## Type checks + +We use `mypy` to run type checks on our code. +To use it: + +```bash +mypy wemake_python_styleguide +``` + +This step is mandatory during the CI. + + +## Before submitting + +Before submitting your code please do the following steps: + +1. Run `pytest` to make sure everything was working before +2. Add any changes you want +3. Adds tests for the new changes +4. Edit documentation if you have changed something significant +5. Run `pytest` again to make sure it is still working +6. Run `mypy` to ensure that types are correct + + +## Other help + +You can contribute by spreading a word about this library. +It would also be a huge contribution to write +a short article on how you are using this project. +What are your best-practices? diff --git a/README.md b/README.md index 01d3fc4ba..ac93e0a27 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # wemake-python-styleguide -[![wemake.services](https://img.shields.io/badge/style-wemake.services-green.svg?label=&logo=data%3Aimage%2Fpng%3Bbase64%2CiVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAALGPC%2FxhBQAAAAFzUkdCAK7OHOkAAAAbUExURQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP%2F%2F%2F5TvxDIAAAAIdFJOUwAjRA8xXANAL%2Bv0SAAAADNJREFUGNNjYCAIOJjRBdBFWMkVQeGzcHAwksJnAPPZGOGAASzPzAEHEGVsLExQwE7YswCb7AFZSF3bbAAAAABJRU5ErkJggg%3D%3D)](http://wemake.services) +[![wemake.services](https://img.shields.io/badge/-wemake.services-green.svg?label=&logo=data%3Aimage%2Fpng%3Bbase64%2CiVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAABGdBTUEAALGPC%2FxhBQAAAAFzUkdCAK7OHOkAAAAbUExURQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP%2F%2F%2F5TvxDIAAAAIdFJOUwAjRA8xXANAL%2Bv0SAAAADNJREFUGNNjYCAIOJjRBdBFWMkVQeGzcHAwksJnAPPZGOGAASzPzAEHEGVsLExQwE7YswCb7AFZSF3bbAAAAABJRU5ErkJggg%3D%3D)](https://wemake.services) [![Build Status](https://travis-ci.org/wemake-services/wemake-python-styleguide.svg?branch=master)](https://travis-ci.org/wemake-services/wemake-python-styleguide) [![Coverage](https://coveralls.io/repos/github/wemake-services/wemake-python-styleguide/badge.svg?branch=master)](https://coveralls.io/github/wemake-services/wemake-python-styleguide?branch=master) [![PyPI version](https://badge.fury.io/py/wemake-python-styleguide.svg)](https://badge.fury.io/py/wemake-python-styleguide) @@ -9,6 +9,9 @@ Welcome to the most opinionated linter ever. +`wemake-python-styleguide` is actually just a `flake8` plugin. +The main goal of this tool is to make our `python` code consistent. + ## Installation diff --git a/pyproject.toml b/pyproject.toml index 03dbc3fcd..ea2853198 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wemake-python-styleguide" -version = "0.0.4" +version = "0.0.5" description = "Opinionated styleguide that we use in wemake.services" license = "MIT" diff --git a/tests/test_visitors/test_wrong_import/test_dotted_raw_import.py b/tests/test_visitors/test_wrong_import/test_dotted_raw_import.py new file mode 100644 index 000000000..7a49e5f27 --- /dev/null +++ b/tests/test_visitors/test_wrong_import/test_dotted_raw_import.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- + +import pytest + +from wemake_python_styleguide.visitors.wrong_import import ( + DottedRawImportViolation, + WrongImportVisitor, +) + +regular_import = """ +import {0} +""" + +regular_import_with_alias = """ +import {0} as alias +""" + +from_import = """ +from {0} import some +""" + +from_import_with_alias = """ +from {0} import some as alias +""" + + +@pytest.mark.parametrize('code', [ + regular_import, + regular_import_with_alias, +]) +@pytest.mark.parametrize('to_import', [ + 'dotted.path' + 'nested.dotted.path', +]) +def test_wrong_dotted_import(assert_errors, parse_ast_tree, code, to_import): + """Testing that dotted raw imports are restricted.""" + tree = parse_ast_tree(code.format(to_import)) + + visiter = WrongImportVisitor() + visiter.visit(tree) + + assert_errors(visiter, [DottedRawImportViolation]) + + +@pytest.mark.parametrize('code', [ + regular_import, + regular_import_with_alias, +]) +@pytest.mark.parametrize('to_import', [ + 'os', + 'sys', +]) +def test_correct_flat_import(assert_errors, parse_ast_tree, code, to_import): + """Testing that flat raw imports are allowed.""" + tree = parse_ast_tree(code.format(to_import)) + + visiter = WrongImportVisitor() + visiter.visit(tree) + + assert_errors(visiter, []) + + +@pytest.mark.parametrize('code', [ + from_import, + from_import_with_alias, +]) +@pytest.mark.parametrize('to_import', [ + 'regular', + 'dotted.path' + 'nested.dotted.path', +]) +def test_regular_from_import(assert_errors, parse_ast_tree, code, to_import): + """Testing that dotted `from` imports are allowed.""" + tree = parse_ast_tree(code.format(to_import)) + + visiter = WrongImportVisitor() + visiter.visit(tree) + + assert_errors(visiter, []) diff --git a/tests/test_visitors/test_wrong_name/test_function_names.py b/tests/test_visitors/test_wrong_name/test_function_names.py index ed847e381..0a9041f88 100644 --- a/tests/test_visitors/test_wrong_name/test_function_names.py +++ b/tests/test_visitors/test_wrong_name/test_function_names.py @@ -63,7 +63,7 @@ def test_too_short_function_names( def test_private_function_names( assert_errors, parse_ast_tree, code, ): - """Testing that function can not have too short names.""" + """Testing that function can not have private names.""" tree = parse_ast_tree(code.format('__hidden')) visiter = WrongNameVisitor() @@ -83,7 +83,7 @@ def test_private_function_names( function_bad_name, method_bad_name, ]) -def test_correct_function_name( +def test_correct_function_names( assert_errors, parse_ast_tree, correct_name, code, ): """Testing that function can have normal names.""" diff --git a/tests/test_visitors/test_wrong_name/test_import_alias.py b/tests/test_visitors/test_wrong_name/test_import_alias.py new file mode 100644 index 000000000..8100f7c72 --- /dev/null +++ b/tests/test_visitors/test_wrong_name/test_import_alias.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- + +import string + +import pytest + +from wemake_python_styleguide.visitors.wrong_name import ( + BAD_VARIABLE_NAMES, + PrivateNameViolation, + TooShortVariableNameViolation, + WrongNameVisitor, + WrongVariableNameViolation, +) + +import_alias = """ +import os as {0} +""" + +from_import_alias = """ +from os import path as {0} +""" + + +@pytest.mark.parametrize('bad_name', BAD_VARIABLE_NAMES) +@pytest.mark.parametrize('code', [ + import_alias, + from_import_alias, +]) +def test_wrong_import_alias_names( + assert_errors, parse_ast_tree, bad_name, code, +): + """Testing that import aliases can not have blacklisted names.""" + tree = parse_ast_tree(code.format(bad_name)) + + visiter = WrongNameVisitor() + visiter.visit(tree) + + assert_errors(visiter, [WrongVariableNameViolation]) + + +@pytest.mark.parametrize('short_name', string.ascii_letters) +@pytest.mark.parametrize('code', [ + import_alias, + from_import_alias, +]) +def test_too_short_import_alias_names( + assert_errors, parse_ast_tree, short_name, code, +): + """Testing that import aliases can not have too short names.""" + tree = parse_ast_tree(code.format(short_name)) + + visiter = WrongNameVisitor() + visiter.visit(tree) + + assert_errors(visiter, [TooShortVariableNameViolation]) + + +@pytest.mark.parametrize('code', [ + import_alias, + from_import_alias, +]) +def test_private_import_alias_names( + assert_errors, parse_ast_tree, code, +): + """Testing that import aliases can not have too private names.""" + tree = parse_ast_tree(code.format('__hidden')) + + visiter = WrongNameVisitor() + visiter.visit(tree) + + assert_errors(visiter, [PrivateNameViolation]) + + +@pytest.mark.parametrize('correct_name', [ + 'my_alias', + 'xy', + 'test', + '_protected', +]) +@pytest.mark.parametrize('code', [ + import_alias, + from_import_alias, +]) +def test_correct_import_alias_names( + assert_errors, parse_ast_tree, correct_name, code, +): + """Testing that import aliases can have normal names.""" + tree = parse_ast_tree(code.format(correct_name)) + + visiter = WrongNameVisitor() + visiter.visit(tree) + + assert_errors(visiter, []) diff --git a/wemake_python_styleguide/checker.py b/wemake_python_styleguide/checker.py index 563d95241..48dab67c4 100644 --- a/wemake_python_styleguide/checker.py +++ b/wemake_python_styleguide/checker.py @@ -77,5 +77,4 @@ def run(self) -> Generator[CheckResult, None, None]: visiter.visit(self.tree) for error in visiter.errors: - lineno, col_offset, message = error.node_items() - yield lineno, col_offset, message, type(self) + yield (*error.node_items(), type(self)) diff --git a/wemake_python_styleguide/constants.py b/wemake_python_styleguide/constants.py index 1db5a5b56..d68f08410 100644 --- a/wemake_python_styleguide/constants.py +++ b/wemake_python_styleguide/constants.py @@ -91,4 +91,5 @@ #: List of allowed ``__future__`` imports. FUTURE_IMPORTS_WHITELIST = frozenset(( 'annotations', + 'generator_stop', )) diff --git a/wemake_python_styleguide/errors.py b/wemake_python_styleguide/errors.py index 4af9055a5..d28461a12 100644 --- a/wemake_python_styleguide/errors.py +++ b/wemake_python_styleguide/errors.py @@ -127,6 +127,27 @@ class FutureImportViolation(BaseStyleViolation): _code = 'Z102' +class DottedRawImportViolation(BaseStyleViolation): + """ + This rule forbids to use imports like ``import os.path``. + + Example:: + + # Correct: + from os import path + + # Wrong: + import os.path + + Note: + Returns Z103 as error code + + """ + + _error_tmpl = '{0} Found dotted raw import "{1}"' + _code = 'Z103' + + class WrongKeywordViolation(BaseStyleViolation): """ This rule forbids to use some keywords from ``python``. diff --git a/wemake_python_styleguide/visitors/wrong_import.py b/wemake_python_styleguide/visitors/wrong_import.py index 06d2bb952..3ea0f4766 100644 --- a/wemake_python_styleguide/visitors/wrong_import.py +++ b/wemake_python_styleguide/visitors/wrong_import.py @@ -4,6 +4,7 @@ from wemake_python_styleguide.constants import FUTURE_IMPORTS_WHITELIST from wemake_python_styleguide.errors import ( + DottedRawImportViolation, FutureImportViolation, LocalFolderImportViolation, NestedImportViolation, @@ -42,13 +43,19 @@ def _check_future_import(self, node: ast.ImportFrom): FutureImportViolation(node, text=alias.name), ) + 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)) + def visit_Import(self, node: ast.Import): - """Used to find nested `import` statements.""" + """Used to find wrong `import` statements.""" self._check_nested_import(node) + self._check_dotted_raw_import(node) self.generic_visit(node) def visit_ImportFrom(self, node: ast.ImportFrom): - """Used to find nested `from import` statements and local imports.""" + """Used to find wrong `from import` statements.""" self._check_local_import(node) self._check_nested_import(node) self._check_future_import(node) diff --git a/wemake_python_styleguide/visitors/wrong_name.py b/wemake_python_styleguide/visitors/wrong_name.py index b0e76323e..3f76d54ea 100644 --- a/wemake_python_styleguide/visitors/wrong_name.py +++ b/wemake_python_styleguide/visitors/wrong_name.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import ast +from typing import Union from wemake_python_styleguide.constants import ( BAD_MODULE_METADATA_VARIABLES, @@ -81,6 +82,16 @@ def visit_Name(self, node: ast.Name): self.generic_visit(node) + def visit_Import(self, node: Union[ast.Import, ast.ImportFrom]): + """Used to check wrong import alias names.""" + for alias in node.names: + if alias.asname: + self._check_name(node, alias.asname) + + self.generic_visit(node) + + visit_ImportFrom = visit_Import + class WrongModuleMetadataVisitor(BaseNodeVisitor): """This class finds wrong metadata information of a module."""