Skip to content

Commit

Permalink
Support patch (#3)
Browse files Browse the repository at this point in the history
* add docs and tests for patch checks
* add support for patch
* increment version
* add note re changed codes in changelog
  • Loading branch information
jdkandersson authored Jan 14, 2023
1 parent 8a75f22 commit 4f592f7
Show file tree
Hide file tree
Showing 6 changed files with 339 additions and 20 deletions.
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

## [Unreleased]

## [v1.2.0] - 2023-01-14

### Added

- Lint checks that ensure `patch` is called with any one or more of the `new`,
`spec`, `spec_set`, `autospec` or `new_callable` arguments

### Fix

- Ensure that error codes are correctly mapped for `NonCallableMock` and
`AsyncMock` which were mapped to the `MagicMock` code before

### Changed

- Changed codes for mock checks:
- `Mock`: `TMS001` -> `TMS010`,
- `MagicMock`: `TMS002` -> `TMS011`,
- `NonCallableMock`: `TMS003` -> `TMS012` and
- `AsyncMock`: `TMS004` -> `TMS013`.

## [v1.1.0] - 2023-01-14

### Added
Expand All @@ -19,3 +39,4 @@
[//]: # "Release links"
[v1.0.0]: https://github.com/jdkandersson/flake8-mock-spec/releases/v1.0.0
[v1.1.0]: https://github.com/jdkandersson/flake8-mock-spec/releases/v1.1.0
[v1.2.0]: https://github.com/jdkandersson/flake8-mock-spec/releases/v1.2.0
57 changes: 48 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ This will produce warnings such as:

```shell
flake8 test_source.py
test_source.py:5:22: TMS001 unittest.mock.Mock instances should be constructed with the spec or spec_set argument, more information: https://github.com/jdkandersson/flake8-mock-spec#fix-tms001
test_source.py:5:22: TMS010 unittest.mock.Mock instances should be constructed with the spec or spec_set argument, more information: https://github.com/jdkandersson/flake8-mock-spec#fix-tms010
```

This can be resolved by changing the code to:
Expand All @@ -49,16 +49,18 @@ def test_foo():

A few rules have been defined to allow for selective suppression:

* `TMS001`: checks that `unittest.mock.Mock` instances are constructed with the
* `TMS010`: checks that `unittest.mock.Mock` instances are constructed with the
`spec` or `spec_set` argument.
* `TMS002`: checks that `unittest.mock.MagicMock` instances are constructed with
* `TMS011`: checks that `unittest.mock.MagicMock` instances are constructed with
the `spec` or `spec_set` argument.
* `TMS003`: checks that `unittest.mock.NonCallableMock` instances are
* `TMS012`: checks that `unittest.mock.NonCallableMock` instances are
constructed with the `spec` or `spec_set` argument.
* `TMS004`: checks that `unittest.mock.AsyncMock` instances are constructed
* `TMS013`: checks that `unittest.mock.AsyncMock` instances are constructed
with the `spec` or `spec_set` argument.
* `TMS020`: checks that `unittest.mock.patch` is called with any one or more of
the `new`, `spec`, `spec_set`, `autospec` or `new_callable` arguments

### Fix TMS001
### Fix TMS010

This linting rule is triggered by creating a `unittest.mock.Mock` instance
without the `spec` or `spec_set` argument. For example:
Expand Down Expand Up @@ -91,7 +93,7 @@ def test_foo():
mocked_foo = mock.Mock(spec_set=Foo)
```

### Fix TMS002
### Fix TMS011

This linting rule is triggered by creating a `unittest.mock.MagicMock` instance
without the `spec` or `spec_set` argument. For example:
Expand Down Expand Up @@ -124,7 +126,7 @@ def test_foo():
mocked_foo = mock.MagicMock(spec_set=Foo)
```

### Fix TMS003
### Fix TMS012

This linting rule is triggered by creating a `unittest.mock.NonCallableMock`
instance without the `spec` or `spec_set` argument. For example:
Expand Down Expand Up @@ -157,7 +159,7 @@ def test_foo():
mocked_foo = mock.NonCallableMock(spec_set=Foo)
```

### Fix TMS004
### Fix TMS013

This linting rule is triggered by creating a `unittest.mock.AsyncMock` instance
without the `spec` or `spec_set` argument. For example:
Expand Down Expand Up @@ -189,3 +191,40 @@ from foo import Foo
def test_foo():
mocked_foo = mock.AsyncMock(spec_set=Foo)
```

### Fix TMS020

This linting rule is triggered by calling `unittest.mock.patch` without any one
or more of the `new`, `spec`, `spec_set`, `autospec` or `new_callable`
arguments. For example:

```Python
from unittest import mock

@mock.patch("Foo")
def test_foo():
pass

with mock.patch("Foo") as mocked_foo:
pass

foo_patcher = patch("Foo")
```

This example can be fixed by using any one or more of the `new`, `spec`,
`spec_set`, `autospec` or `new_callable` arguments:

```Python
from unittest import mock

from foo import Foo

@mock.patch("Foo", spec=Foo)
def test_foo():
pass

with mock.patch("Foo", spec_set=Foo) as mocked_foo:
pass

foo_patcher = patch("Foo", autospec=True)
```
31 changes: 26 additions & 5 deletions flake8_mock_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,41 @@
f"%s unittest.mock.%s instances should be constructed with the {' or '.join(SPEC_ARGS)} "
f"argument, {MORE_INFO_BASE}#fix-%s"
)
MOCK_SPEC_CODE = f"{ERROR_CODE_PREFIX}001"
MOCK_SPEC_CODE = f"{ERROR_CODE_PREFIX}010"
MOCK_SPEC_MSG = MOCK_SPEC_MSG_BASE % (MOCK_SPEC_CODE, MOCK_CLASS, MOCK_SPEC_CODE.lower())
MAGIC_MOCK_SPEC_CODE = f"{ERROR_CODE_PREFIX}002"
MAGIC_MOCK_SPEC_CODE = f"{ERROR_CODE_PREFIX}011"
MAGIC_MOCK_SPEC_MSG = MOCK_SPEC_MSG_BASE % (
MAGIC_MOCK_SPEC_CODE,
MAGIC_MOCK_CLASS,
MAGIC_MOCK_SPEC_CODE.lower(),
)
NON_CALLABLE_MOCK_SPEC_CODE = f"{ERROR_CODE_PREFIX}003"
NON_CALLABLE_MOCK_SPEC_CODE = f"{ERROR_CODE_PREFIX}012"
NON_CALLABLE_MOCK_SPEC_MSG = MOCK_SPEC_MSG_BASE % (
NON_CALLABLE_MOCK_SPEC_CODE,
NON_CALLABLE_MOCK_CLASS,
NON_CALLABLE_MOCK_SPEC_CODE.lower(),
)
ASYNC_MOCK_SPEC_CODE = f"{ERROR_CODE_PREFIX}004"
ASYNC_MOCK_SPEC_CODE = f"{ERROR_CODE_PREFIX}013"
ASYNC_MOCK_SPEC_MSG = MOCK_SPEC_MSG_BASE % (
ASYNC_MOCK_SPEC_CODE,
ASYNC_MOCK_CLASS,
ASYNC_MOCK_SPEC_CODE.lower(),
)
MOCK_MSG_LOOKUP = {
MOCK_CLASS: MOCK_SPEC_MSG,
MAGIC_MOCK_CLASS: MAGIC_MOCK_SPEC_MSG,
NON_CALLABLE_MOCK_CLASS: NON_CALLABLE_MOCK_SPEC_MSG,
ASYNC_MOCK_CLASS: ASYNC_MOCK_SPEC_MSG,
}

# The attribute actually does exist, mypy reports that it doesn't
PATCH_FUNCTION: str = mock.patch.__name__ # type: ignore
PATCH_ARGS = frozenset(("new", "spec", "spec_set", "autospec", "new_callable"))
PATCH_CODE = f"{ERROR_CODE_PREFIX}020"
PATCH_MSG = (
f"{PATCH_CODE} unittest.mock.{PATCH_FUNCTION} should be called with any of the "
f"{', '.join(PATCH_ARGS)} arguments, {MORE_INFO_BASE}#fix-{PATCH_CODE.lower()}"
)


class Problem(NamedTuple):
Expand Down Expand Up @@ -88,10 +103,16 @@ def visit_Call(self, node: ast.Call) -> None: # pylint: disable=invalid-name
Problem(
lineno=node.lineno,
col_offset=node.col_offset,
msg=MOCK_SPEC_MSG if name == MOCK_CLASS else MAGIC_MOCK_SPEC_MSG,
msg=MOCK_MSG_LOOKUP[name],
)
)

if name is not None and name == PATCH_FUNCTION:
if not any(keyword.arg in PATCH_ARGS for keyword in node.keywords):
self.problems.append(
Problem(lineno=node.lineno, col_offset=node.col_offset, msg=PATCH_MSG)
)

# Ensure recursion continues
self.generic_visit(node)

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "flake8-mock-spec"
version = "1.1.0"
version = "1.2.0"
description = "A linter that checks mocks are constructed with the spec argument"
authors = ["David Andersson <[email protected]>"]
license = "Apache 2.0"
Expand Down
33 changes: 32 additions & 1 deletion tests/test_flake8_mock_spec_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@

import pytest

from flake8_mock_spec import MAGIC_MOCK_SPEC_CODE, MOCK_SPEC_CODE, MOCK_SPEC_MSG
from flake8_mock_spec import (
ASYNC_MOCK_SPEC_CODE,
MAGIC_MOCK_SPEC_CODE,
MOCK_SPEC_CODE,
MOCK_SPEC_MSG,
NON_CALLABLE_MOCK_SPEC_CODE,
PATCH_CODE,
)


def test_help():
Expand Down Expand Up @@ -87,6 +94,30 @@ def test_fail(tmp_path: Path):
""",
id=f"{MAGIC_MOCK_SPEC_CODE} disabled",
),
pytest.param(
f"""
from unittest import mock
mock.NonCallableMock() # noqa: {NON_CALLABLE_MOCK_SPEC_CODE}
""",
id=f"{NON_CALLABLE_MOCK_SPEC_CODE} disabled",
),
pytest.param(
f"""
from unittest import mock
mock.AsyncMock() # noqa: {ASYNC_MOCK_SPEC_CODE}
""",
id=f"{ASYNC_MOCK_SPEC_CODE} disabled",
),
pytest.param(
f"""
from unittest import mock
mock.patch() # noqa: {PATCH_CODE}
""",
id=f"{PATCH_CODE} disabled",
),
],
)
def test_pass(code: str, tmp_path: Path):
Expand Down
Loading

0 comments on commit 4f592f7

Please sign in to comment.