Skip to content

Commit

Permalink
Merge pull request #51 from jamescooke/command-line
Browse files Browse the repository at this point in the history
Command line
  • Loading branch information
jamescooke committed Oct 29, 2018
2 parents 2a2daee + 068145d commit d96aa28
Show file tree
Hide file tree
Showing 26 changed files with 776 additions and 154 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ Added

* Python 3.5 now supported.

* Command line functionality now available to assist with development and
debugging.

Removed
-------

Expand Down
98 changes: 86 additions & 12 deletions docs/commands.rst
Original file line number Diff line number Diff line change
@@ -1,20 +1,94 @@
Controlling flake8-aaa in-code
******************************
Controlling Flake8-AAA
======================

flake8-aaa can be controlled using some special comments in
your test code.
In code
-------

Flake8-AAA can be controlled using some special comments in your test code.

Explicitly marking blocks
=========================
.........................

One can set the act block explicitly using the ``# act`` comment. This is
necessary when there is no assignment possible.

Disabling Flake8-AAA selectively
................................

When Flake8-AAA finds the ``# noqa`` comment at the end of the line that
defines a test function, it will ignore it.

Command line
------------

Flake8-AAA has a simple command line interface to assist with development and
debugging. Its goal is to show the state of analysed test functions, which
lines are considered to be parts of which blocks and any errors that have been
found.

Invocation, output and return value
...................................

With Flake8-AAA installed, it can be called as a Python module::

$ python -m flake8_aaa [test_file]

Where ``[test_file]`` is the path to a file to be checked.

The return value of the execution is the number of errors found in the file,
for example::

$ python -m flake8_aaa ../some_test.py
------+------------------------------------------------------------------------
1 DEF|def test():
2 ARR| x = 1
3 ARR| y = 1
4 ACT| result = x + y
^ AAA03 expected 1 blank line before Act block, found none
5 BL |
6 ASS| assert result == 2
------+------------------------------------------------------------------------
1 | ERROR
$ echo "$?"
1

And once the error above is fixed, the return value returns to zero::

$ python -m flake8_aaa ../some_test.py
------+------------------------------------------------------------------------
1 DEF|def test():
2 ARR| x = 1
3 ARR| y = 1
4 BL |
5 ACT| result = x + y
6 BL |
7 ASS| assert result == 2
------+------------------------------------------------------------------------
0 | ERRORS
$ echo "$?"
0

Line markers
............

Each test found in the passed file is displayed. Each line is annotated with
its line number in the file and a marker to show how Flake8-AAA classified that
line. Line markers are as follows:

ACT
Line is part of the Act Block.

One can set the act block explicitly using the ``# act``
comment. This is necessary when there is no assignment
possible.
ARR
Line is part of an Arrange Block.

Disabling flake8-aaa selectively
================================
ASS
Line is part of the Assert Block.

When flake8-aaa finds the ``# noqa`` comment after the
function/method head it will ignore this function/method.
BL
Line is considered a blank line for layout purposes.

DEF
Test function definition.

???
Unprocessed line. Flake8-AAA has not categorised this line.
20 changes: 20 additions & 0 deletions flake8_aaa/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Dirty hack to allow for direct running of flake8-aaa from the command line,
# bypassing flake8's harness. Goal of this is to allow for debugging of
# analysis, especially around how flake8-aaa has assigned lines of code to
# particular blocks.

import argparse
import sys

from .command_line import do_command_line


def main() -> int:
parser = argparse.ArgumentParser(description='flake8-aaa command line debug')
parser.add_argument('infile', type=argparse.FileType('r'), help='File to be linted')
args = parser.parse_args()
return do_command_line(args.infile)


if __name__ == '__main__':
sys.exit(main())
59 changes: 36 additions & 23 deletions flake8_aaa/act_block.py
Original file line number Diff line number Diff line change
@@ -1,61 +1,74 @@
import ast
from typing import List, Type, TypeVar

from .helpers import node_is_pytest_raises, node_is_result_assignment, node_is_unittest_raises
from .types import ActBlockType
from .types import ActBlockType, LineType

AB = TypeVar('AB', bound='ActBlock') # Place holder for ActBlock instances

class ActBlock(object):

class ActBlock:
"""
Attributes:
node
block_type (ActBlockType)
"""

def __init__(self, node, block_type):
def __init__(self, node: ast.AST, block_type: ActBlockType) -> None:
"""
Args:
node
block_type (ActBlockType)
"""
self.node = node
self.block_type = block_type
self.node = node # type: ast.AST
self.block_type = block_type # type: ActBlockType

@classmethod
def build_body(cls, body):
def build_body(cls: Type[AB], body: List[ast.stmt]):
"""
Args:
body (list (ast.node)): List of nodes from a block.
Returns:
list (ActBlock)
Note:
Return type is probably ``-> List[AB]``, but can't get it to pass.
"""
act_blocks = []
act_blocks = [] # type: List[ActBlock]
for child_node in body:
act_blocks += ActBlock.build(child_node)
return act_blocks

@classmethod
def build(cls, node):
"""
Args:
node (ast.node): A node, decorated with ``ASTTokens``.
Returns:
list(ActBlock)
"""
def build(cls: Type[AB], node: ast.AST) -> List[AB]:
if node_is_result_assignment(node):
return [cls(node, ActBlockType.result_assignment)]
elif node_is_pytest_raises(node):
if node_is_pytest_raises(node):
return [cls(node, ActBlockType.pytest_raises)]
elif node_is_unittest_raises(node):
if node_is_unittest_raises(node):
return [cls(node, ActBlockType.unittest_raises)]

token = node.first_token # type: ignore
# Check if line marked with '# act'
if node.first_token.line.strip().endswith('# act'):
if token.line.strip().endswith('# act'):
return [cls(node, ActBlockType.marked_act)]

# Recurse if it's a context manager
if isinstance(node, ast.With):
return cls.build_body(node.body)

return []

def mark_line_types(self, line_types: List[LineType], first_line_no: int) -> List[LineType]:
"""
Marks the lines occupied by this ActBlock.
Note:
Mutates the ``line_types`` list.
Raises:
AssertionError: When position in ``line_types`` has already been
marked to something other than ``???:unprocessed``.
"""
# Lines calculated relative to file
start_line = self.node.first_token.start[0] # type:ignore
end_line = self.node.last_token.end[0] # type:ignore
for file_line_no in range(start_line, end_line + 1):
assert line_types[file_line_no - first_line_no] is LineType.unprocessed
line_types[file_line_no - first_line_no] = LineType.act_block
return line_types
17 changes: 6 additions & 11 deletions flake8_aaa/arrange_block.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import ast
from typing import List # noqa

from .multi_node_block import MultiNodeBlock
from .types import LineType

class ArrangeBlock(object):
"""
Attributes:
nodes (list (ast Node))
"""

def __init__(self):
self.nodes = []
class ArrangeBlock(MultiNodeBlock):
line_type = LineType.arrange_block

def add_node(self, node):
def add_node(self, node: ast.AST) -> bool:
"""
Add node if it's an "arrangement node".
Returns:
bool: Node is an arrangement node.
"""
if isinstance(node, ast.Pass):
return False
Expand Down
17 changes: 9 additions & 8 deletions flake8_aaa/assert_block.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
class AssertBlock(object):
"""
Attributes:
nodes (list (ast Node))
"""
import ast

def __init__(self):
self.nodes = []
from .multi_node_block import MultiNodeBlock
from .types import LineType

def add_node(self, node):

class AssertBlock(MultiNodeBlock):
line_type = LineType.assert_block

def add_node(self, node: ast.AST) -> bool:
self.nodes.append(node)
return True
25 changes: 18 additions & 7 deletions flake8_aaa/checker.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import typing
from ast import AST

import asttokens

from .__about__ import __short_name__, __version__
Expand All @@ -12,13 +15,13 @@ class Checker(object):
ast_tokens (asttokens.ASTTokens): Tokens for the file.
filename (str): Name of file under check.
lines (list (str))
tree (ast.Module): Tree passed from flake8.
tree (ast.AST): Tree passed from flake8.
"""

name = __short_name__
version = __version__

def __init__(self, tree, lines, filename):
def __init__(self, tree: AST, lines: typing.List[str], filename: str):
"""
Args:
tree
Expand All @@ -30,17 +33,25 @@ def __init__(self, tree, lines, filename):
self.filename = filename
self.ast_tokens = None

def load(self):
def load(self) -> None:
self.ast_tokens = asttokens.ASTTokens(''.join(self.lines), tree=self.tree)

def run(self):
def all_funcs(self) -> typing.Generator[Function, None, None]:
"""
Returns:
generator (Function): Generator of functions the loaded file.
"""
return (Function(f, self.lines) for f in find_test_functions(self.tree))

def run(self) -> typing.Generator[typing.Tuple[int, int, str, typing.Type], None, None]:
"""
(line_number, offset, text, check)
Yields:
tuple (line_number: int, offset: int, text: str, check: type)
"""
if is_test_file(self.filename):
self.load()
for function_def in find_test_functions(self.tree):
for func in self.all_funcs():
try:
Function(function_def, self.lines).check_all()
func.check_all()
except ValidationError as error:
yield error.to_flake8(type(self))
25 changes: 25 additions & 0 deletions flake8_aaa/command_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import ast
from typing import IO

from .checker import Checker


def do_command_line(infile: IO[str]) -> int:
"""
Currently a small stub to create an instance of Checker for the passed
``infile`` and run its test functions through linting.
Args:
infile
Returns:
int: Number of flake8 errors raised.
"""
lines = infile.readlines()
tree = ast.parse(''.join(lines))
checker = Checker(tree, lines, infile.name)
checker.load()
for func in checker.all_funcs():
errors = func.get_errors()
print(func, end='')
return len(errors)
5 changes: 4 additions & 1 deletion flake8_aaa/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import typing


class Flake8AAAException(Exception):
pass

Expand All @@ -15,7 +18,7 @@ def __init__(self, line_number, offset, text):
self.offset = offset
self.text = text

def to_flake8(self, checker_cls):
def to_flake8(self, checker_cls: typing.Type) -> typing.Tuple[int, int, str, typing.Type]:
"""
Args:
checker_cls (type): Class performing the check to be passed back to
Expand Down

0 comments on commit d96aa28

Please sign in to comment.