Skip to content

Commit

Permalink
Extend recognition of Pytest context managers: add deprecated_call() …
Browse files Browse the repository at this point in the history
…and warns() (#199)

* Add examples for namespaced context manager use

* Rename ActNodeType from pytest_raises to pytest_context_manager

* Soften node analysis function to make space for other pytest CMs

* Extend recognition of Pytest CMs: include deprecated_call() and warns()

* Update CHANGELOG

* Tweak docs

* Improve py versions and black error notes
* Move error codes up 1 place in the toc - put them above test discovery
internals.
* Clean up CM list in AAA01 doc.

* Add pytest CM good examples from @lyz-code

* Clean up Changelog entry: just log new example for CM
  • Loading branch information
jamescooke committed Feb 17, 2023
1 parent 744b628 commit cfaa83e
Show file tree
Hide file tree
Showing 13 changed files with 96 additions and 21 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,16 @@ Added
* 📕 Extended Changelog entries to include markers indicating focus. `Pull #201
<https://github.com/jamescooke/flake8-aaa/pull/201>`_

* 🎈 Support for Pytest context managers ``pytest.warns()`` and
``pytest.deprecated_call()``. `Issue #196
<https://github.com/jamescooke/flake8-aaa/issues/196>`_, `pull #199
<https://github.com/jamescooke/flake8-aaa/pull/199>`_.

* ⛏️ "Bad" example added for scenario where manager will only be found if it is
in the ``pytest`` namespace. To be compatible with Flake8-AAA tests need to
``import pytest`` and not ``from pytest import raises``. `Pull #199
<https://github.com/jamescooke/flake8-aaa/pull/199>`_.

Changed
.......

Expand Down
11 changes: 7 additions & 4 deletions docs/compatibility.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ Flake8-AAA is fully compatible and tested against the latest versions of Python

.. admonition:: See also...

:ref:`Previously supported Python versions <previous-python-versions>`.
See :ref:`full list of previously supported Python versions
<previous-python-versions>` for links to the last supported packages and
documentation.

Flake8
------
Expand Down Expand Up @@ -62,9 +64,10 @@ They are then checked to pass Flake8-AAA's linting.

.. note::

We are aware that new management of blank lines released in Black
``23.1.0`` cause ``AAA03`` errors to be raised and this is tracked `in
issue #200 <https://github.com/jamescooke/flake8-aaa/issues/200>`_.
Black version ``23.1.0`` changed how it managed blank lines by default.
This change causes Flake8-AAA to raise ``AAA03`` errors on tests that
contain context managers and are formatted with Black. See `issue #200
<https://github.com/jamescooke/flake8-aaa/issues/200>`_.

Pytest
------
Expand Down
6 changes: 4 additions & 2 deletions docs/error_codes/AAA01-no-act-block-found-in-test.rst
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,7 @@ The Act block carries out a single action on an object so it's important that
Flake8-AAA can clearly distinguish which line or lines make up the Act block in
every test.

Code blocks wrapped in ``pytest.raises()`` and ``unittest.assertRaises()``
context managers are recognised as Act blocks.
Flake8-AAA recognises code blocks wrapped in Pytest context managers like
``pytest.raises()`` as Act blocks.

It also recognises unittest's ``assertRaises()`` blocks as Act blocks.
2 changes: 1 addition & 1 deletion docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Continue here for more detail about using Flake8-AAA.
:caption: Contents

compatibility
discovery
error_codes/all
discovery
commands
release_checklist
2 changes: 2 additions & 0 deletions examples/bad/bad_expected.out
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
examples/bad/test.py:4:1: AAA01 no Act block found in test
examples/bad/test_aaa01.py:8:1: AAA01 no Act block found in test
examples/bad/test_aaa01_cm_import.py:11:1: AAA01 no Act block found in test
examples/bad/test_aaa01_cm_import.py:18:1: AAA01 no Act block found in test
examples/bad/test_aaa03.py:10:9: AAA03 expected 1 blank line before Act block, found none
examples/bad/test_aaa03.py:3:5: AAA03 expected 1 blank line before Act block, found none
examples/bad/test_aaa03_04.py:10:5: AAA04 expected 1 blank line before Assert block, found none
Expand Down
20 changes: 20 additions & 0 deletions examples/bad/test_aaa01_cm_import.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
"""
Failure of AAA01: when pytest context managers are not imported "in" pytest,
then they can't be found
"""

# Unusual import of raises and warns: docs / convention uses `import pytest` so
# preserves namespace.
from pytest import raises, warns


def test_imported() -> None:
one_stuff = [1]

with raises(IndexError):
one_stuff[1]


def test_warns() -> None:
with warns(UserWarning, match="yamlfix/pull/182"):
pass
11 changes: 11 additions & 0 deletions examples/good/test_with_statement.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import io
import warnings
from typing import Generator, List

import pytest
Expand All @@ -11,6 +12,16 @@ def test_pytest_raises() -> None:
list()[0]


def test_deprecation_warning():
with pytest.deprecated_call():
warnings.warn("deprecate warning", DeprecationWarning)


def test_user_warning():
with pytest.warns(UserWarning):
warnings.warn("my warning", UserWarning)


# --- Use of context managers in tests ---


Expand Down
6 changes: 3 additions & 3 deletions src/flake8_aaa/act_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from .helpers import (
get_first_token,
get_last_token,
node_is_pytest_raises,
node_is_pytest_context_manager,
node_is_result_assignment,
node_is_unittest_raises,
)
Expand Down Expand Up @@ -56,8 +56,8 @@ def build(cls: Type[AN], node: ast.stmt) -> List[AN]:
"""
if node_is_result_assignment(node):
return [cls(node, ActNodeType.result_assignment)]
if node_is_pytest_raises(node):
return [cls(node, ActNodeType.pytest_raises)]
if node_is_pytest_context_manager(node):
return [cls(node, ActNodeType.pytest_context_manager)]
if node_is_unittest_raises(node):
return [cls(node, ActNodeType.unittest_raises)]

Expand Down
18 changes: 15 additions & 3 deletions src/flake8_aaa/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,28 @@ def node_is_result_assignment(node: ast.AST) -> bool:
return False


def node_is_pytest_raises(node: ast.AST) -> bool:
cm_exp = re.compile(r'^\s*with\ pytest\.(raises|deprecated_call|warns)\(')


def node_is_pytest_context_manager(node: ast.AST) -> bool:
"""
Identify node as being one of the Pytest context managers used to catch
exceptions and warnings.
Pytest's context managers as of 7.2 are:
* pytest.raises()
* pytest.deprecated_call()
* pytest.warns()
Args:
node: An ``ast`` node, augmented with ASTTokens
Returns:
bool: ``node`` corresponds to a With node where the context manager is
``pytest.raises``.
a Pytest context manager.
"""
return isinstance(node, ast.With) and get_first_token(node).line.strip().startswith('with pytest.raises')
return isinstance(node, ast.With) and bool(cm_exp.match(get_first_token(node).line))


def node_is_unittest_raises(node: ast.AST) -> bool:
Expand Down
2 changes: 1 addition & 1 deletion src/flake8_aaa/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
'ActNodeType',
(
'marked_act ' # Marked with "# act"
'pytest_raises ' # Wrapped in "pytest.raises" context manager.
'pytest_context_manager ' # Pytest context manager e.g. "pytest.raises"
'result_assignment ' # Simple "result = "
'unittest_raises ' # Wrapped in unittest's "assertRaises" context manager.
),
Expand Down
4 changes: 2 additions & 2 deletions tests/act_node/test_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def test_pytest_raises_block(first_node_with_tokens):
assert len(result) == 1
assert isinstance(result[0], ActNode)
assert result[0].node == first_node_with_tokens.body[0]
assert result[0].block_type == ActNodeType.pytest_raises
assert result[0].block_type == ActNodeType.pytest_context_manager


@pytest.mark.parametrize(
Expand All @@ -45,7 +45,7 @@ def test_unittest_raises_block(first_node_with_tokens):
@pytest.mark.parametrize(
'code_str, expected_type', [
('result = do_thing()', ActNodeType.result_assignment),
('with pytest.raises(Exception):\n do_thing()', ActNodeType.pytest_raises),
('with pytest.raises(Exception):\n do_thing()', ActNodeType.pytest_context_manager),
('data[new_key] = value # act', ActNodeType.marked_act),
('result: Thing = do_thing()', ActNodeType.result_assignment),
('result: List[int] = do_thing()', ActNodeType.result_assignment),
Expand Down
2 changes: 1 addition & 1 deletion tests/function/test_load_act_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ def test_raises_in_cm(function):
result = function.load_act_node()

assert isinstance(result, ActNode)
assert result.block_type == ActNodeType.pytest_raises
assert result.block_type == ActNodeType.pytest_context_manager
assert result.node.first_token.line == ' with pytest.raises(ValidationError):\n'


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,42 @@
import asttokens
import pytest

from flake8_aaa.helpers import node_is_pytest_raises
from flake8_aaa.helpers import node_is_pytest_context_manager


@pytest.mark.parametrize(
'code_str', [
'code_str',
[
# raises() with no vars
'''
def test():
with pytest.raises(Exception):
do_thing()
''',
# raises() with excinfo var
'''
def test_other():
with pytest.raises(Exception) as excinfo:
do_thing()
''',
# deprecated_call()
'''
def test_api2_deprecated() -> None:
with pytest.deprecated_call():
api_call_v2()
''',
# warns()
'''
def test_thing_warning() -> None:
with pytest.warns(RuntimeWarning):
do_thing()
''',
]
)
def test(first_node_with_tokens):
with_node = first_node_with_tokens.body[0]

result = node_is_pytest_raises(with_node)
result = node_is_pytest_context_manager(with_node)

assert result is True

Expand All @@ -37,6 +52,6 @@ def test_no(code_str):
asttokens.ASTTokens(code_str, tree=tree)
node = tree.body[0]

result = node_is_pytest_raises(node)
result = node_is_pytest_context_manager(node)

assert result is False

0 comments on commit cfaa83e

Please sign in to comment.