From 8abe0406da3f0c6435ea7d85c45e263c72770f48 Mon Sep 17 00:00:00 2001 From: James Cooke Date: Tue, 9 May 2023 23:00:39 +0100 Subject: [PATCH] Build out large Act blocks option (#218) * Add LARGE act block style option value * Restore add_node_parents() helper * Reinstate greedy act blocks for Large act block style * Reinstate test on add_node_parents * Add pytest icdiff to test requirements * Add test on Function.mark_act() * Fix typo in tox config * Prevent mypy caches leaking between runs on local * Fix lint: ignore stuffing of parent attr; update comment * Bump Black to 23.3.0 from 22.12.0 * Adjust Checker to use a default config class attr * Remove old file * Build out FirstChildFinder to help with large style act blocks * Fix lint: remove unused import * Switch from adding parents to using first child finder visitor * Stub in temp tox config to run large act block config * Update Black formatted examples and add environment to tox * Update CHANGELOG --- CHANGELOG.rst | 13 ++-- Makefile | 2 +- configs/black_compatible.ini | 2 + docs/command_line.rst | 6 ++ .../black/test_django_fakery_factories.py | 1 - examples/black/test_with_statement.py | 47 +++++++++----- .../black/test_with_statement_unittest.py | 1 - requirements/examples.in | 2 +- requirements/examples.txt | 8 ++- requirements/test.in | 1 + requirements/test.txt | 21 ++++--- src/flake8_aaa/block.py | 34 ++++++++-- src/flake8_aaa/checker.py | 7 ++- src/flake8_aaa/conf.py | 2 +- src/flake8_aaa/function.py | 2 +- src/flake8_aaa/helpers.py | 4 +- src/flake8_aaa/visitors.py | 32 ++++++++++ tests/block/block/test_build_act.py | 63 +++++++++++++++++++ tests/checker/test_init.py | 15 ++++- tests/checker/test_parse_options.py | 7 +-- .../act_block_style/test_allowed_values.py | 2 +- tests/conf/config/test_load_options.py | 2 +- tests/function/test_mark_act.py | 29 +++++++++ tests/visitors/__init__.py | 0 tests/visitors/test_find_first_child_nodes.py | 50 +++++++++++++++ tox.ini | 20 ++++-- 26 files changed, 318 insertions(+), 55 deletions(-) create mode 100644 configs/black_compatible.ini create mode 100644 src/flake8_aaa/visitors.py create mode 100644 tests/visitors/__init__.py create mode 100644 tests/visitors/test_find_first_child_nodes.py diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a92d2fc..505a9d7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -23,17 +23,22 @@ Unreleased_ See also `latest documentation `_. -* ⛏️ Move Black formatted test examples to their own directory. This will help +* 🎈 New "large" Act block style option added. This allows context managers to + join the Act block when they directly wrap the Act node. This behaviour is + provided to provide compatibility with Black versions ``23.*``. Fixes `issue + 200 `_. + +* ⛏️ Moved Black formatted test examples to their own directory. This helps when running Flake8 against Black formatted tests which need ``--aaa-act-block-style=large``. Also fix up associated Makefile recipes and update example README file. -* ⛏️ Remove list of nodes from ``Block`` class and instead keep the start and +* ⛏️ Removed list of nodes from ``Block`` class and instead kept the start and end line numbers of the block. This allows for any structural discoveries while doing AST node traversal (e.g. when parsing Large style Act blocks) to be used to calculate the size of the Act block itself. The alternative would - be to store the list of nodes in the Act block, and then re-walk them when - working out the block's span, which would be duplication of effort. + have been to store the list of nodes in the Act block, and then re-walk them + when working out the block's span, which would be duplication of effort. * ⛏️ Remove unused ``MultiNodeBlock``. diff --git a/Makefile b/Makefile index 75da3a9..f3fc4e1 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,7 @@ lintexamples: flake8 examples/good examples/bad | sort > flake8.out diff examples/bad/flake8_expected.out flake8.out @echo "=== mypy ===" - mypy examples/conftest.py examples/good --ignore-missing-imports --exclude examples/good/black/ + mypy examples/conftest.py examples/good --ignore-missing-imports --exclude examples/black/ --no-incremental mypy examples/bad --ignore-missing-imports @echo "=== black ===" black --check --diff --verbose examples/black diff --git a/configs/black_compatible.ini b/configs/black_compatible.ini new file mode 100644 index 0000000..b69b2bb --- /dev/null +++ b/configs/black_compatible.ini @@ -0,0 +1,2 @@ +[flake8] +aaa_act_block_style = large diff --git a/docs/command_line.rst b/docs/command_line.rst index 96fc60e..438235d 100644 --- a/docs/command_line.rst +++ b/docs/command_line.rst @@ -6,6 +6,12 @@ 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. +.. warning:: + + Command line mode does not support ``--aaa-act-block-style=large`` option + or associated configuration. `Issue regarding this is open on GitHub + `_. + Invocation, output and return value ----------------------------------- diff --git a/examples/black/test_django_fakery_factories.py b/examples/black/test_django_fakery_factories.py index 9050abe..ecdebad 100644 --- a/examples/black/test_django_fakery_factories.py +++ b/examples/black/test_django_fakery_factories.py @@ -22,7 +22,6 @@ def test_default(self): class TestUserFactory(TestCase): - user_model = get_user_model() def test_default(self): diff --git a/examples/black/test_with_statement.py b/examples/black/test_with_statement.py index bd3feb6..86f2602 100644 --- a/examples/black/test_with_statement.py +++ b/examples/black/test_with_statement.py @@ -1,4 +1,5 @@ import io +import pathlib import warnings from typing import Generator, List @@ -12,12 +13,12 @@ def test_pytest_raises() -> None: list()[0] -def test_deprecation_warning(): +def test_deprecation_warning() -> None: with pytest.deprecated_call(): warnings.warn("deprecate warning", DeprecationWarning) -def test_user_warning(): +def test_user_warning() -> None: with pytest.warns(UserWarning): warnings.warn("my warning", UserWarning) @@ -25,31 +26,48 @@ def test_user_warning(): # --- Use of context managers in tests --- -def test_simple(hello_world_path) -> None: +def test_simple(hello_world_path: pathlib.Path) -> None: """ - `with` statement is part of arrange. Blank lines are maintained around Act. + Test checks "simple" context manager in both Act block styles: + + * DEFAULT: `with` statement is part of arrange. Blank lines are maintained + around Act. + * LARGE: When formatted with Black, context manager is squashed against act + node. Large act block mode allows the context manager to join the act + block and linting passes. """ with open(hello_world_path) as f: - result = f.read() assert result == "Hello World!\n" -def test_whole(hello_world_path) -> None: +def test_whole(hello_world_path: pathlib.Path) -> None: """ - `with` statement wraps whole of test + `with` statement wraps whole of test. This checks context manager in both + Act block styles: + + * DEFAULT: `with` statement is part of Arrange. Result assignment is Act. + Blank lines are maintained. + * LARGE: When formatted with Black, context manager is squashed against + result assignment. Act block grows to consume context manager because + it's the first node in the context manager body. However, new large Act + block _still_ finishes at the end of the result assignment. There is + then a blank line and the assert block, even though that's inside the + context manager. """ with open(hello_world_path) as f: - result = f.read() assert result == "Hello World!\n" + assert f.fileno() > 0 -def test_extra_arrange(hello_world_path) -> None: +def test_extra_arrange(hello_world_path: pathlib.Path) -> None: """ - Any extra arrangement goes in the `with` block. + Any extra arrangement goes in the `with` block. Works "as-is" for Large act + block style because `f.read()` node prevents the `result = ` act node from + joining the `with` context manager. """ with open(hello_world_path) as f: f.read() @@ -59,7 +77,7 @@ def test_extra_arrange(hello_world_path) -> None: assert result == "" -def test_assert_in_block(hello_world_path) -> None: +def test_assert_in_block(hello_world_path: pathlib.Path) -> None: """ Any assertion that needs the `with` block open, goes after Act and a BL. """ @@ -73,19 +91,18 @@ def test_assert_in_block(hello_world_path) -> None: assert result == "" -def test_pytest_assert_raises_in_block(hello_world_path) -> None: +def test_pytest_assert_raises_in_block(hello_world_path: pathlib.Path) -> None: """ Checking on a raise in a with block works with Pytest. """ with open(hello_world_path) as f: - with pytest.raises(io.UnsupportedOperation): f.write("hello back") assert f.read() == "Hello World!\n" -def test_pytest_assert_raises_on_with(hello_world_path) -> None: +def test_pytest_assert_raises_on_with(hello_world_path: pathlib.Path) -> None: """ Checking on the raise from a with statement works with Pytest. """ @@ -96,7 +113,7 @@ def test_pytest_assert_raises_on_with(hello_world_path) -> None: assert "invalid mode" in str(excinfo.value) -def test_with_in_assert(hello_world_path) -> None: +def test_with_in_assert(hello_world_path: pathlib.Path) -> None: """ Using with statement in Assert block is valid """ diff --git a/examples/black/test_with_statement_unittest.py b/examples/black/test_with_statement_unittest.py index 2a95953..a8c5bb9 100644 --- a/examples/black/test_with_statement_unittest.py +++ b/examples/black/test_with_statement_unittest.py @@ -14,7 +14,6 @@ def test_assert_raises_in_block(self): Checking on a raise in a with block works with unittest. """ with open(self.hello_world_path) as f: - with self.assertRaises(io.UnsupportedOperation): f.write("hello back") diff --git a/requirements/examples.in b/requirements/examples.in index 292fe88..8ee11c4 100644 --- a/requirements/examples.in +++ b/requirements/examples.in @@ -1,5 +1,5 @@ # py37 -black<23 +black flake8 mypy pytest diff --git a/requirements/examples.txt b/requirements/examples.txt index 39ff79d..8780dba 100644 --- a/requirements/examples.txt +++ b/requirements/examples.txt @@ -4,7 +4,7 @@ # # pip-compile --output-file=examples.txt examples.in # -black==22.12.0 +black==23.3.0 # via -r examples.in click==8.1.3 # via black @@ -29,10 +29,12 @@ mypy-extensions==1.0.0 # black # mypy packaging==23.1 - # via pytest + # via + # black + # pytest pathspec==0.11.1 # via black -platformdirs==3.2.0 +platformdirs==3.5.0 # via black pluggy==1.0.0 # via pytest diff --git a/requirements/test.in b/requirements/test.in index f8d8a6f..9310564 100644 --- a/requirements/test.in +++ b/requirements/test.in @@ -5,5 +5,6 @@ isort mypy==0.930 pygments pytest +pytest-icdiff restructuredtext-lint yapf diff --git a/requirements/test.txt b/requirements/test.txt index 348c687..e056171 100644 --- a/requirements/test.txt +++ b/requirements/test.txt @@ -4,16 +4,16 @@ # # pip-compile --output-file=test.txt test.in # -attrs==22.2.0 - # via pytest docutils==0.19 # via restructuredtext-lint exceptiongroup==1.1.1 # via pytest -faker==18.4.0 +faker==18.6.0 # via -r test.in flake8==4.0.1 # via -r test.in +icdiff==2.0.6 + # via pytest-icdiff importlib-metadata==4.2.0 # via # flake8 @@ -29,17 +29,23 @@ mypy==0.930 # via -r test.in mypy-extensions==1.0.0 # via mypy -packaging==23.0 +packaging==23.1 # via pytest pluggy==1.0.0 # via pytest +pprintpp==0.4.0 + # via pytest-icdiff pycodestyle==2.8.0 # via flake8 pyflakes==2.4.0 # via flake8 -pygments==2.14.0 +pygments==2.15.1 # via -r test.in -pytest==7.2.2 +pytest==7.3.1 + # via + # -r test.in + # pytest-icdiff +pytest-icdiff==0.6 # via -r test.in python-dateutil==2.8.2 # via faker @@ -51,6 +57,7 @@ tomli==2.0.1 # via # mypy # pytest + # yapf typed-ast==1.5.4 # via mypy typing-extensions==4.5.0 @@ -58,7 +65,7 @@ typing-extensions==4.5.0 # faker # importlib-metadata # mypy -yapf==0.32.0 +yapf==0.33.0 # via -r test.in zipp==3.15.0 # via importlib-metadata diff --git a/src/flake8_aaa/block.py b/src/flake8_aaa/block.py index 384b180..e778c00 100644 --- a/src/flake8_aaa/block.py +++ b/src/flake8_aaa/block.py @@ -5,6 +5,7 @@ from .exceptions import EmptyBlock from .helpers import filter_arrange_nodes, get_first_token, get_last_token from .types import LineType +from .visitors import find_first_child_nodes _Block = TypeVar('_Block', bound='Block') @@ -38,18 +39,41 @@ def __init__(self, first_line_no: int, last_line_no: int, lt: LineType) -> None: def build_act( cls: Type[_Block], node: ast.stmt, - test_func_node: ast.FunctionDef, # use this in TODO200 - act_block_style: ActBlockStyle, # use this in TODO200 + test_func_node: ast.FunctionDef, + act_block_style: ActBlockStyle, ) -> _Block: """ - Act block is a single node by default. TODO200 + Using the provided `node` as Act Node, build the Act Block depending on + the `act_block_style`. + + Act blocks are simply a single Act node in default mode. However, + "large" Act blocks include the Act node and any context managers that + wrap them. Args: node: Act node already found by Function.mark_act() test_func_node: Node of test function / method. - act_block_style: Currently always DEFAULT. + act_block_style: Default Act Blocks are just the Act node. Large + Act Blocks can absorb context managers that wrap them. """ - first, last = get_span(node, node) + if act_block_style == ActBlockStyle.DEFAULT: + first, last = get_span(node, node) + return cls(first, last, LineType.act) + + # --- LARGE Act Block behaviour... + + # Walk up the parent nodes of the act node, but only when act node is + # the first child. This allows act block to absorb context managers + # that have the act node first in their body. + child_parent_map = find_first_child_nodes(test_func_node) + wrapper_node = node + while True: + try: + wrapper_node = child_parent_map[wrapper_node] + except KeyError: + break + + first, last = get_span(wrapper_node, node) return cls(first, last, LineType.act) @classmethod diff --git a/src/flake8_aaa/checker.py b/src/flake8_aaa/checker.py index 9988214..71f1bdb 100644 --- a/src/flake8_aaa/checker.py +++ b/src/flake8_aaa/checker.py @@ -24,12 +24,15 @@ class Checker: name = __short_name__ version = __version__ + default_config = Config.default_options() + def __init__(self, tree: AST, lines: List[str], filename: str): self.tree = tree self.lines = lines self.filename = filename self.ast_tokens: Optional[asttokens.ASTTokens] = None - self.config: Config = Config.default_options() + + self.config: Config = self.default_config @staticmethod def add_options(option_manager) -> None: @@ -56,7 +59,7 @@ def parse_options(cls, option_manager, options: argparse.Namespace, args) -> Non Raises: UnexpectedConfigValue: When config can't be loaded. """ - cls.config = Config.load_options(options) + cls.default_config = Config.load_options(options) def load(self) -> None: self.ast_tokens = asttokens.ASTTokens(''.join(self.lines), tree=self.tree) diff --git a/src/flake8_aaa/conf.py b/src/flake8_aaa/conf.py index 92dc3ea..dfe0f72 100644 --- a/src/flake8_aaa/conf.py +++ b/src/flake8_aaa/conf.py @@ -11,7 +11,7 @@ @enum.unique class ActBlockStyle(enum.Enum): DEFAULT = 'default' - # LARGE = 'large' # TODO200 + LARGE = 'large' @classmethod def allowed_values(cls: Type[_ActBlockStyle]) -> List[str]: diff --git a/src/flake8_aaa/function.py b/src/flake8_aaa/function.py index b183817..d8cffc3 100644 --- a/src/flake8_aaa/function.py +++ b/src/flake8_aaa/function.py @@ -177,7 +177,7 @@ def mark_act(self, act_block_style: ActBlockStyle) -> int: lines in ``line_markers``. Args: - act_block_style: Currently only DEFAULT. TODO200 + act_block_style: Passed through to `build_act()` method. Returns: Number of lines covered by the Act block (used for debugging / diff --git a/src/flake8_aaa/helpers.py b/src/flake8_aaa/helpers.py index 088cbdc..06820be 100644 --- a/src/flake8_aaa/helpers.py +++ b/src/flake8_aaa/helpers.py @@ -29,10 +29,12 @@ class TestFuncLister(ast.NodeVisitor): """ Helper to walk the ast Tree and find functions that looks like tests. Matching function nodes are kept in ``_found_func`` attr. + + TODO: move to visitors.py """ def __init__(self, skip_noqa: bool): - super(TestFuncLister, self).__init__() + super().__init__() self.skip_noqa = skip_noqa self._found_funcs: List[ast.FunctionDef] = [] diff --git a/src/flake8_aaa/visitors.py b/src/flake8_aaa/visitors.py new file mode 100644 index 0000000..5bef01c --- /dev/null +++ b/src/flake8_aaa/visitors.py @@ -0,0 +1,32 @@ +import ast +from typing import Dict + + +class FirstChildFinder(ast.NodeVisitor): + """ + When building Large style act blocks, we need to know if any particular act + node is the "first" child of a context manager. If it _is_, then the + context manager can join the act block. + + Builds `_child_parent` dict which maps child node to parent node. This + allows the act block builder to check if the act node is a first child + using the key, and if so, it then has the parent so can recurse upwards. + """ + + def __init__(self) -> None: + super().__init__() + self.child_parent: Dict[ast.AST, ast.With] = {} + + def visit_With(self, node: ast.With) -> None: + self.child_parent[node.body[0]] = node + for child_node in node.body: + self.visit(child_node) + + +def find_first_child_nodes(tree: ast.AST) -> Dict[ast.AST, ast.With]: + """ + Wrapper for FirstChildFinder visitor - see docstring above + """ + first_child_finder = FirstChildFinder() + first_child_finder.visit(tree) + return first_child_finder.child_parent diff --git a/tests/block/block/test_build_act.py b/tests/block/block/test_build_act.py index 2207ae1..e9869f0 100644 --- a/tests/block/block/test_build_act.py +++ b/tests/block/block/test_build_act.py @@ -40,3 +40,66 @@ def test(first_node_with_tokens: ast.FunctionDef) -> None: assert result.first_line_no == 8 assert result.last_line_no == 9 assert result.line_type == LineType.act + + +@pytest.mark.parametrize( + 'code_str', [ + ''' +def test(): # line 2 + with mock.patch('things.thinger'): # <-- Act block starts here with Large + with pytest.raises(ValueError): + things() +''' + ] +) +def test_large_first_child(first_node_with_tokens: ast.FunctionDef) -> None: + """ + Large Act blocks absorb statements that contain them when Act node is first + child of the wrapping context manager. + """ + with_mock_node = first_node_with_tokens.body[0] + with_pytest_node = with_mock_node.body[0] # type: ignore[attr-defined] + + result = Block.build_act( + node=with_pytest_node, + test_func_node=first_node_with_tokens, + act_block_style=ActBlockStyle.LARGE, + ) + + assert isinstance(result, Block) + assert result.first_line_no == 3 + assert result.last_line_no == 5 + assert result.line_type == LineType.act + + +@pytest.mark.parametrize( + 'code_str', [ + ''' +def test(): + with open('somefile.txt') as f: + f.read() + + result = check_state(f) # line 6 + + assert result is False +''' + ] +) +def test_large_second_child(first_node_with_tokens: ast.FunctionDef) -> None: + """ + Large style Act block *don't* absorb context managers that are one or more + lines of code "away". In this case, the Act block is not the first child. + """ + with_open_node = first_node_with_tokens.body[0] + result_assignment = with_open_node.body[1] # type: ignore[attr-defined] + + result = Block.build_act( + node=result_assignment, + test_func_node=first_node_with_tokens, + act_block_style=ActBlockStyle.LARGE, + ) + + assert isinstance(result, Block) + assert result.first_line_no == 6 + assert result.last_line_no == 6 + assert result.line_type == LineType.act diff --git a/tests/checker/test_init.py b/tests/checker/test_init.py index 437be31..c5ed9b8 100644 --- a/tests/checker/test_init.py +++ b/tests/checker/test_init.py @@ -3,7 +3,7 @@ import pytest from flake8_aaa import Checker -from flake8_aaa.conf import Config +from flake8_aaa.conf import ActBlockStyle, Config @pytest.fixture @@ -14,7 +14,7 @@ def ast_example() -> ast.AST: # --- TESTS --- -def test(ast_example) -> None: +def test(ast_example: ast.AST) -> None: result = Checker(ast_example, [], '__FILENAME__') assert result.tree == ast_example @@ -22,3 +22,14 @@ def test(ast_example) -> None: assert result.filename == '__FILENAME__' assert result.ast_tokens is None assert result.config == Config.default_options() + + +def test_set_config(ast_example: ast.AST) -> None: + """ + Config can be set in Checker + """ + Checker.default_config = Config(act_block_style=ActBlockStyle.LARGE) + + result = Checker(ast_example, [], '__FILENAME__') + + assert result.config.act_block_style == ActBlockStyle.LARGE diff --git a/tests/checker/test_parse_options.py b/tests/checker/test_parse_options.py index 6eabb4a..f894eb3 100644 --- a/tests/checker/test_parse_options.py +++ b/tests/checker/test_parse_options.py @@ -3,7 +3,7 @@ import pytest from flake8_aaa import Checker -from flake8_aaa.conf import ActBlockStyle, Config +from flake8_aaa.conf import ActBlockStyle from flake8_aaa.exceptions import UnexpectedConfigValue @@ -17,13 +17,12 @@ def test() -> None: by this method. """ option_manager = None # Fake because it's not used by SUT - options = argparse.Namespace(aaa_act_block_style='default') + options = argparse.Namespace(aaa_act_block_style='Large') result = Checker.parse_options(option_manager, options, []) assert result is None - assert isinstance(Checker.config, Config) - assert Checker.config.act_block_style == ActBlockStyle.DEFAULT + assert Checker.default_config.act_block_style == ActBlockStyle.LARGE # --- FAILURES --- diff --git a/tests/conf/act_block_style/test_allowed_values.py b/tests/conf/act_block_style/test_allowed_values.py index 124d694..418d3cd 100644 --- a/tests/conf/act_block_style/test_allowed_values.py +++ b/tests/conf/act_block_style/test_allowed_values.py @@ -4,4 +4,4 @@ def test() -> None: result = ActBlockStyle.allowed_values() - assert result == ['default'] + assert result == ['default', 'large'] diff --git a/tests/conf/config/test_load_options.py b/tests/conf/config/test_load_options.py index 7d2d205..a232f69 100644 --- a/tests/conf/config/test_load_options.py +++ b/tests/conf/config/test_load_options.py @@ -34,4 +34,4 @@ def test_unknown(faker: Faker) -> None: assert excinfo.value.option_name == 'aaa_act_block_style' assert excinfo.value.value == unknown_value - assert excinfo.value.allowed_values == ['default'] + assert excinfo.value.allowed_values == ['default', 'large'] diff --git a/tests/function/test_mark_act.py b/tests/function/test_mark_act.py index 8a2748c..da5de07 100644 --- a/tests/function/test_mark_act.py +++ b/tests/function/test_mark_act.py @@ -34,6 +34,35 @@ def test_simple(function_bl_cmt_def: Function) -> None: ] +@pytest.mark.parametrize( + 'code_str', [ + ''' +def test(hello_world_path): + with open(hello_world_path) as f: + + result = f.read() + + assert result == 'Hello World!' +''' + ] +) +def test_simple_large(function_bl_cmt_def: Function) -> None: + """ + When run with Large act block style, `with` statement is part of Act. + """ + result = function_bl_cmt_def.mark_act(ActBlockStyle.LARGE) + + assert result == 3 + assert function_bl_cmt_def.line_markers.types == [ + LineType.func_def, + LineType.act, # <- with open(...) + LineType.blank_line, + LineType.act, + LineType.blank_line, + LineType.unprocessed, + ] + + @pytest.mark.parametrize( 'code_str', [ ''' diff --git a/tests/visitors/__init__.py b/tests/visitors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/visitors/test_find_first_child_nodes.py b/tests/visitors/test_find_first_child_nodes.py new file mode 100644 index 0000000..9284c53 --- /dev/null +++ b/tests/visitors/test_find_first_child_nodes.py @@ -0,0 +1,50 @@ +import pytest + +from flake8_aaa.visitors import find_first_child_nodes + + +@pytest.mark.parametrize( + 'code_str', [ + ''' +def test(): + with mock.patch('things.thinger'): + with pytest.raises(ValueError): + things() +''' + ] +) +def test(first_node_with_tokens) -> None: + with_mock_node = first_node_with_tokens.body[0] + with_pytest_node = with_mock_node.body[0] + things_node = with_pytest_node.body[0] + + result = find_first_child_nodes(first_node_with_tokens) + + assert result == { + with_pytest_node: with_mock_node, + things_node: with_pytest_node, + } + + +@pytest.mark.parametrize( + 'code_str', [ + ''' +def test_extra_arrange(hello_world_path: pathlib.Path) -> None: + with open(hello_world_path) as f: + f.read() + + result = f.read() + + assert result == '' +''' + ] +) +def test_not_first_child(first_node_with_tokens) -> None: + with_open_node = first_node_with_tokens.body[0] + f_read_node = with_open_node.body[0] + + result = find_first_child_nodes(first_node_with_tokens) + + assert result == { + f_read_node: with_open_node, + } diff --git a/tox.ini b/tox.ini index 006258a..d82fcc3 100644 --- a/tox.ini +++ b/tox.ini @@ -7,7 +7,7 @@ # In increasing level of integration: # * lint: Lint all the things! # * test: Run all tests on code, run pytest on stdlib examples. -# * meta; Integration tests - run as plugin and command against code and examples. +# * meta: Integration tests - run as plugin and command against code and examples. # # Additional: # * docs: Run documentation build. @@ -30,7 +30,7 @@ envlist = py3{7,8,9,10,11}-lint_{code,examples} py3{7,8,9,10,11}-test_{code,examples} - py3{7,8,9,10,11}-meta_plugin_{dogfood,default,option,config} + py3{7,8,9,10,11}-meta_plugin_{dogfood,default,option,config,black} py3{7,8,9,10,11}-meta_command py310-docs @@ -146,9 +146,9 @@ deps = flake8>=4 # Common full integration test command used to against good and bad examples, # both with default and various configs +# TODO use --output-file for output commands = flake8 {env:FLAKE8FLAGS:} examples/good - flake8 {env:FLAKE8FLAGS:} examples/black bash -c "flake8 {env:FLAKE8FLAGS:} examples/bad/ | sort > {envtmpdir}/out" bash -c "sort examples/bad/bad_expected.out > {envtmpdir}/expected_out" diff {envtmpdir}/out {envtmpdir}/expected_out @@ -158,7 +158,7 @@ allowlist_externals = [testenv:py3{7,8,9,10,11}-meta_plugin_dogfood] # No FLAKE8FLAGS set, so default behaviour -description = 🐕 Run -m flake_aaa against it's own tests +description = 🐕 Run -m flake_aaa against its own tests labels = {[base_meta_plugin]labels} meta_plugin_dogfood @@ -202,6 +202,18 @@ deps = {[base_meta_plugin]deps} commands = {[base_meta_plugin]commands} allowlist_externals = {[base_meta_plugin]allowlist_externals} +[testenv:py3{7,8,9,10,11}-meta_plugin_black] +# Run Black examples passing Act block style 'large' as command line option and +# passing as config. +description = 🎈 Run -m flake_aaa against Black formatted examples +labels = + {[base_meta_plugin]labels} + meta_plugin_black +deps = {[base_meta_plugin]deps} +commands = + flake8 --aaa-act-block-style=large examples/black + flake8 --config=configs/black_compatible.ini examples/black + # --- META: command --- # Run `... -m flake8_aaa`) on all example files. Check errors from bad examples # are as expected.