Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add new rule #4

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Some rules in linter should be configurated:
[flake8]
scenario_params_max_count = 8 # VDR109
allowed_to_redefine_list = page,page2 # VDR311
is_context_assert_optional = true # VDR400
```

## Rules
Expand Down Expand Up @@ -53,3 +54,7 @@ allowed_to_redefine_list = page,page2 # VDR311
11. [VDR310. Some steps should not have an assertion](./flake8_vedro/rules/VDR310.md)
12. [VDR311. Scope variables should not be redefined](./flake8_vedro/rules/VDR311.md)
13. [VDR312. Scope variables should not be partially redefined](./flake8_vedro/rules/VDR312.md)


### Contexts Rules
14. [VDR400. Contexts should have specific assertions](./flake8_vedro/rules/VDR400md)
1 change: 1 addition & 0 deletions flake8_vedro/abstract_checkers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .context_checker import ContextChecker
from .scenario_checker import ScenarioChecker
from .scenario_helper import ScenarioHelper
from .step_checker import StepsChecker
11 changes: 11 additions & 0 deletions flake8_vedro/abstract_checkers/context_checker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from abc import ABC, abstractmethod
from typing import List

from flake8_plugin_utils import Error


class ContextChecker(ABC):

@abstractmethod
def check_context(self, context, config) -> List[Error]:
pass
6 changes: 5 additions & 1 deletion flake8_vedro/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@


class Config:
def __init__(self, max_params_count: int,
def __init__(self, is_context_assert_optional: bool,
max_params_count: int,
allowed_to_redefine_list: Optional[List]):
self.is_context_assert_optional = is_context_assert_optional
self.max_params_count = max_params_count
self.allowed_to_redefine_list = allowed_to_redefine_list if allowed_to_redefine_list else []


class DefaultConfig(Config):
def __init__(self,
is_context_assert_optional: bool = True,
max_params_count: int = 1,
allowed_to_redefine_list: Optional[List] = None
):
super().__init__(
is_context_assert_optional=is_context_assert_optional,
max_params_count=max_params_count,
allowed_to_redefine_list=allowed_to_redefine_list
)
1 change: 1 addition & 0 deletions flake8_vedro/errors/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from .errors import (
ContextCallInParams,
ContextWithoutAssert,
DecoratorVedroOnly,
ExceedMaxParamsCount,
ImportedInterfaceInWrongStep,
Expand Down
5 changes: 5 additions & 0 deletions flake8_vedro/errors/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,8 @@ class ScopeVarIsRedefined(Error):
class ScopeVarIsPartiallyRedefined(Error):
code = 'VDR312'
message = 'scope variable "{name}" is partially redefined'


class ContextWithoutAssert(Error):
code = 'VDR400'
message = 'context "{context_name}" does not have an assert'
13 changes: 11 additions & 2 deletions flake8_vedro/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from flake8.options.manager import OptionManager
from flake8_plugin_utils import Plugin, Visitor

from flake8_vedro.visitors import ScenarioVisitor
from flake8_vedro.visitors import ContextVisitor, ScenarioVisitor

from .config import Config
from .defaults import Defaults
Expand Down Expand Up @@ -36,16 +36,24 @@ def _create_visitor(cls, visitor_cls: Callable, filename: Optional[str] = None)

class VedroScenarioStylePlugin(PluginWithFilename):
name = 'flake8_vedro'
version = '1.0.1'
version = '1.0.2'
visitors = [
ScenarioVisitor,
ContextVisitor
]

def __init__(self, tree: ast.AST, filename: str, *args, **kwargs):
super().__init__(tree, filename)

@classmethod
def add_options(cls, option_manager: OptionManager):
option_manager.add_option(
'--is-context-assert-optional',
default='true',
type=str,
parse_from_config=True,
help='If contexts should have specific assertions',
)
option_manager.add_option(
'--scenario-params-max-count',
default=Defaults.MAX_PARAMS_COUNT,
Expand All @@ -66,6 +74,7 @@ def parse_options_to_config(
cls, option_manager: OptionManager, options: argparse.Namespace, args: List[str]
) -> Config:
return Config(
is_context_assert_optional=str_to_bool(options.is_context_assert_optional),
max_params_count=options.scenario_params_max_count,
allowed_to_redefine_list=options.allowed_to_redefine_list,
)
25 changes: 25 additions & 0 deletions flake8_vedro/rules/VDR400md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# VDR400. contexts should have an assertion
The Vedro framework Сontexts should contain an assertion. This ensures that the application state has changed
### ❌ Anti-pattern
```python
# ./contexts/registered_user.py
import vedro
from interfaces.chat_api import ChatApi

@vedro.context
def registered_user(user):
response = ChatApi().register(user) # <- no assert

```
### ✅Best practice
```python
# ./contexts/registered_user.py
import vedro
from interfaces.chat_api import ChatApi

@vedro.context
def registered_user(user):
response = ChatApi().register(user)
assert response.status_code == 200
```

2 changes: 1 addition & 1 deletion flake8_vedro/types/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from .types import StepType
from .types import FuncType
2 changes: 1 addition & 1 deletion flake8_vedro/types/types.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import ast
from typing import Union

StepType = Union[ast.FunctionDef, ast.AsyncFunctionDef]
FuncType = Union[ast.FunctionDef, ast.AsyncFunctionDef]
2 changes: 2 additions & 0 deletions flake8_vedro/visitors/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from .context_checkers import ContextAssertChecker
from .context_visitor import ContextVisitor
from .scenario_checkers import (
LocationChecker,
ParametrizationCallChecker,
Expand Down
1 change: 1 addition & 0 deletions flake8_vedro/visitors/context_checkers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .assert_checker import ContextAssertChecker
31 changes: 31 additions & 0 deletions flake8_vedro/visitors/context_checkers/assert_checker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import ast
from typing import List

from flake8_plugin_utils import Error

from flake8_vedro.abstract_checkers import ContextChecker
from flake8_vedro.errors import ContextWithoutAssert
from flake8_vedro.visitors.context_visitor import Context, ContextVisitor


@ContextVisitor.register_context_checker
class ContextAssertChecker(ContextChecker):

def check_context(self, context: Context, config) -> List[Error]:
errors = []
has_assert = self._has_assert_in_block(context.node.body)

if not has_assert:
errors.append(ContextWithoutAssert(context.node.lineno, context.node.col_offset,
context_name=context.node.name))

return errors

def _has_assert_in_block(self, block: List[ast.stmt]) -> bool:
for line in block:
if isinstance(line, ast.Assert):
return True
elif isinstance(line, (ast.With, ast.AsyncWith)):
if self._has_assert_in_block(line.body):
return True
return False
61 changes: 61 additions & 0 deletions flake8_vedro/visitors/context_visitor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import ast
from typing import List, Optional, Type, Union

from flake8_plugin_utils import Error

from flake8_vedro.abstract_checkers import ContextChecker
from flake8_vedro.config import Config
from flake8_vedro.types import FuncType
from flake8_vedro.visitors._visitor_with_filename import VisitorWithFilename


class Context:
def __init__(self, node: List[FuncType], filename: str):
self.node = node
self.filename = filename


class ContextVisitor(VisitorWithFilename):
context_checkers: List[ContextChecker] = []

def __init__(self, config: Optional[Config] = None,
filename: Optional[str] = None) -> None:
super().__init__(config, filename)

@property
def config(self):
return self._config

@classmethod
def register_context_checker(cls, checker: Type[ContextChecker]):
cls.context_checkers.append(checker())
return checker

@classmethod
def deregister_all(cls):
cls.context_checkers = []

def _check_context_decorator(self, node: Union[ast.FunctionDef, ast.AsyncFunctionDef]):
if self.config.is_context_assert_optional:
return []

for decorator in node.decorator_list:
if (isinstance(decorator, ast.Attribute)
and decorator.value.id == 'vedro'
and decorator.attr == 'context'):
context = Context(node=node,
filename=self.filename)
try:
for checker in self.context_checkers:
self.errors.extend(checker.check_context(context, self.config))
except Exception as e:
print(f'Linter failed: checking {context.filename} with {checker.__class__}.\n'
f'Exception: {e}')

def visit_FunctionDef(self, node: ast.FunctionDef) -> List[Error]:
self._check_context_decorator(node)
return []

def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> List[Error]:
self._check_context_decorator(node)
return []
4 changes: 2 additions & 2 deletions flake8_vedro/visitors/scenario_visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
StepsChecker
)
from flake8_vedro.config import Config
from flake8_vedro.types import StepType
from flake8_vedro.types import FuncType
from flake8_vedro.visitors._visitor_with_filename import VisitorWithFilename


class Context:
def __init__(self, steps: List[StepType], scenario_node: ast.ClassDef,
def __init__(self, steps: List[FuncType], scenario_node: ast.ClassDef,
import_from_nodes: List[ast.ImportFrom],
filename: str):
self.steps = steps
Expand Down
3 changes: 2 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = flake8_vedro
version = 1.0.1
version = 1.0.2

[options.entry_points]
flake8.extension =
Expand All @@ -16,6 +16,7 @@ ignore = W503

# flake8-vedro params
scenario_params_max_count = 8
is_context_assert_optional = true


[mypy]
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ def find_dev_required():

setup(
name="flake8-vedro",
version="1.0.1",
version="1.0.2",
description="flake8 based linter for vedro framework",
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
python_requires=">=3.10",
url="https://github.com/mytestopia/flake8-vedro",
author="Anna",
author_email="[email protected]",
author_email="[email protected]",
license="Apache-2.0",
packages=find_packages(exclude=("tests",)),
package_data={"flake8_vedro": ["py.typed"]},
Expand Down
Loading