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

Pylint fixes #59

Closed
wants to merge 13 commits into from
16 changes: 1 addition & 15 deletions pyproject.toml
Expand Up @@ -58,21 +58,7 @@ requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"

[tool.pylint.messages_control]
disable = [
"too-many-instance-attributes",
"too-few-public-methods",
"logging-fstring-interpolation",
"too-many-function-args",
"too-many-arguments",
"bad-continuation",
"too-many-locals",
"too-many-branches",
"too-many-statements",
"too-many-public-methods",
"protected-access",
"broad-except",
"fixme",
]
disable = []

[tool.pylint.format]
max-line-length = "100"
Expand Down
197 changes: 88 additions & 109 deletions src/flexmock/api.py
@@ -1,6 +1,7 @@
"""Flexmock public API."""
# pylint: disable=no-self-use,too-many-lines
# pylint: disable=no-self-use,protected-access,too-many-lines
import inspect
import itertools
import re
import sys
import types
Expand Down Expand Up @@ -28,7 +29,7 @@
RE_TYPE = type(re.compile(""))


class ReturnValue:
class ReturnValue: # pylint: disable=too-few-public-methods
ollipa marked this conversation as resolved.
Show resolved Hide resolved
"""ReturnValue"""

def __init__(self, value: Optional[Any] = None, raises: Optional[Exception] = None) -> None:
Expand Down Expand Up @@ -249,11 +250,12 @@ def _update_method(self, expectation: "Expectation", name: str) -> None:
expectation._update_original(name, obj)
method_type = type(_getattr(expectation, "_original"))
try:
# pylint: disable=fixme
# TODO(herman): this is awful, fix this properly.
# When a class/static method is mocked out on an *instance*
# we need to fetch the type from the class
method_type = type(_getattr(obj.__class__, name))
except Exception:
except Exception: # pylint: disable=broad-except
Comment on lines 252 to +258
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could now use https://docs.python.org/3.7/library/contextlib.html#contextlib.suppress I don't think it's affected by this warning

pass
if method_type in SPECIAL_METHODS:
expectation._original_function = getattr(obj, name)
Expand Down Expand Up @@ -311,75 +313,62 @@ def updated(self: Any) -> Any:
def _create_mock_method(self, name: str) -> Callable[..., Any]:
def _handle_exception_matching(expectation: Expectation) -> None:
# pylint: disable=misplaced-bare-raise
return_values = _getattr(expectation, "_return_values")
if return_values:
raised, instance = sys.exc_info()[:2]
assert raised, "no exception was raised"
message = "%s" % instance
expected = return_values[0].raises
if not expected:
raise
args = return_values[0].value
expected_instance = expected(*args["kargs"], **args["kwargs"])
expected_message = "%s" % expected_instance
if inspect.isclass(expected):
if expected is not raised and expected not in raised.__bases__:
raise ExceptionClassError("expected %s, raised %s" % (expected, raised))
if args["kargs"] and isinstance(args["kargs"][0], RE_TYPE):
if not args["kargs"][0].search(message):
raise (
ExceptionMessageError(
'expected /%s/, raised "%s"'
% (args["kargs"][0].pattern, message)
)
)
elif expected_message and expected_message != message:
return_values = expectation._return_values
ollipa marked this conversation as resolved.
Show resolved Hide resolved
if not return_values:
raise
raised, instance = sys.exc_info()[:2]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
raised, instance = sys.exc_info()[:2]
raised_exception, exception_instance = sys.exc_info()[:2]

assert raised, "no exception was raised"
message = "%s" % instance
ollipa marked this conversation as resolved.
Show resolved Hide resolved
expected = return_values[0].raises
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
expected = return_values[0].raises
expected_exception = return_values[0].raises

if not expected:
raise
args = return_values[0].value
expected_message = "%s" % expected(*args["kargs"], **args["kwargs"])
ollipa marked this conversation as resolved.
Show resolved Hide resolved
if inspect.isclass(expected):
if expected is not raised and expected not in raised.__bases__:
raise ExceptionClassError("expected %s, raised %s" % (expected, raised))
ollipa marked this conversation as resolved.
Show resolved Hide resolved
if args["kargs"] and isinstance(args["kargs"][0], RE_TYPE):
if not args["kargs"][0].search(message):
raise (
ExceptionMessageError(
'expected "%s", raised "%s"' % (expected_message, message)
'expected /%s/, raised "%s"' % (args["kargs"][0].pattern, message)
ollipa marked this conversation as resolved.
Show resolved Hide resolved
)
)
elif expected is not raised:
raise ExceptionClassError('expected "%s", raised "%s"' % (expected, raised))
else:
raise
elif expected_message and expected_message != message:
raise (
ExceptionMessageError(
'expected "%s", raised "%s"' % (expected_message, message)
)
)
ollipa marked this conversation as resolved.
Show resolved Hide resolved
elif expected is not raised:
raise ExceptionClassError('expected "%s", raised "%s"' % (expected, raised))
ollipa marked this conversation as resolved.
Show resolved Hide resolved

def match_return_values(expected: Any, received: Any) -> bool:
if not isinstance(expected, tuple):
expected = (expected,)
if not isinstance(received, tuple):
received = (received,)
if len(received) != len(expected):
return False
for i, val in enumerate(received):
if not _arguments_match(val, expected[i]):
return False
return True
expected = (expected,) if not isinstance(expected, tuple) else expected
received = (received,) if not isinstance(received, tuple) else received
return len(received) == len(expected) and all(
_arguments_match(r, e) for r, e in zip(received, expected)
)
Comment on lines +349 to +351
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can be smart with itertools also:

Suggested change
return len(received) == len(expected) and all(
_arguments_match(r, e) for r, e in zip(received, expected)
)
return all(
_arguments_match(r, e) for r, e in itertools.zip_longest(received, expected, fillvalue=object())
)

(changing the fillvalue to a new object ensures that nothing will be equal to it)

I'm not saying we need to change it, it might also be considered too smart and harder to maintain, but I find it interesting 😄


def pass_thru(
expectation: Expectation, runtime_self: Any, *kargs: Any, **kwargs: Any
) -> Any:
return_values = None
try:
original = _getattr(expectation, "_original")
_mock = _getattr(expectation, "_mock")
if inspect.isclass(_mock):
if type(original) in SPECIAL_METHODS:
original = _getattr(expectation, "_original_function")
return_values = original(*kargs, **kwargs)
else:
return_values = original(runtime_self, *kargs, **kwargs)
else:
return_values = original(*kargs, **kwargs)
except Exception:
return_values = (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit hard to understand what is happening. Can you add some comments for explanation and unpack the code a bit? It's very concise now and I think it makes it harder to read.

We could also have only the function call in the try block:

if something:
    original = functools.partial(expectation._original, runtime_self, *kargs, **kwargs)
else:
    original = functools.partial(expectation._original, *kargs, **kwargs)

try:
    original()
except Exception:
    ...

(
expectation._original_function(*kargs, **kwargs)
if type(expectation._original) in SPECIAL_METHODS
else expectation._original(runtime_self, *kargs, **kwargs)
)
if inspect.isclass(expectation._mock)
else expectation._original(*kargs, **kwargs)
)
Comment on lines +357 to +365
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return_values = (
(
expectation._original_function(*kargs, **kwargs)
if type(expectation._original) in SPECIAL_METHODS
else expectation._original(runtime_self, *kargs, **kwargs)
)
if inspect.isclass(expectation._mock)
else expectation._original(*kargs, **kwargs)
)
return_values = expectation._original(*kargs, **kwargs)
if not inspect.isclass(expectation._mock)
else expectation._original_function(*kargs, **kwargs)
if type(expectation._original) in SPECIAL_METHODS
else expectation._original(runtime_self, *kargs, **kwargs)

except Exception: # pylint: disable=broad-except
return _handle_exception_matching(expectation)
expected_values = _getattr(expectation, "_return_values")
expected_values = expectation._return_values
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder what was the need for _getattr, I hope we are not breaking anything there … Have you looked up the history ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that this is an optimization to avoid calling the custom __getattribute__ that has multiple if statements:
38db75e

Maybe we could just have some hotpath for private attributes. For example:

def __getattribute__(self, name: str) -> Any:
        if name.startswith("_"):
            # Return immediately for private attributes
            return _getattr(self, name)
        if name == "once":
            return _getattr(self, "times")(1)
        if name == "twice":
            return _getattr(self, "times")(2)
        if name == "never":
            return _getattr(self, "times")(0)
        if name in ("at_least", "at_most", "ordered", "one_by_one"):
            return _getattr(self, name)()
        if name == "mock":
            return _getattr(self, "mock")()
        return _getattr(self, name)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that we wouldn't need the _getattr function since it's basically just an alias of object.__getattribute__

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, we can get rid of the function and call object.__getattribute__. However, if we change Expectation.__getattribute__ like I suggested, then there is no need to call object.__getattribute__ (expect inside Expectation.__getattribute__ itself).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could even replace the if-branches with a dict lookup, it would be optimized that way too, or just have those as actual attributes/methods. (I know that it was a choice made before, but I think it takes away from the maintainability of this class anyways.)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems that they were properties before:
365c96a

I think this change was made so that they could be called with or without parenthesis. I think it would be better to have those as actual methods because now code completion doesn't work for them.

if expected_values and not match_return_values(expected_values[0].value, return_values):
raise (
MethodSignatureError(
"expected to return %s, returned %s"
% (expected_values[0].value, return_values)
)
raise MethodSignatureError(
"expected to return %s, returned %s" % (expected_values[0].value, return_values)
ollipa marked this conversation as resolved.
Show resolved Hide resolved
)
return return_values

Expand All @@ -392,77 +381,66 @@ def _handle_matched_expectation(
)
expectation.times_called += 1
expectation.verify(final=False)
_pass_thru = _getattr(expectation, "_pass_thru")
_replace_with = _getattr(expectation, "_replace_with")
if _pass_thru:
if expectation._pass_thru:
return pass_thru(expectation, runtime_self, *kargs, **kwargs)
if _replace_with:
return _replace_with(*kargs, **kwargs)
return_values = _getattr(expectation, "_return_values")
if return_values:
return_value = return_values[0]
del return_values[0]
return_values.append(return_value)
else:
return_value = ReturnValue()
if return_value.raises:
if inspect.isclass(return_value.raises):
raise return_value.raises(
*return_value.value["kargs"], **return_value.value["kwargs"]
)
raise return_value.raises # pylint: disable=raising-bad-type
return return_value.value
if expectation._replace_with:
return expectation._replace_with(*kargs, **kwargs)
return_values = expectation._return_values or [ReturnValue()]
return_value = return_values.pop(0)
return_values.append(return_value)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is different from the existing behavior. If return_values is empty, here return_values would be [ReturnValue()] and in the existing code it is empty (ReturnValue() is never appended to return_values).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually you can just return here early if return_values is empty:

if not expectation._return_values:
    return None
return_value = return_values.pop(0)
return_values.append(return_value)
...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is different from the existing behavior. If return_values is empty, here return_values would be [ReturnValue()] and in the existing code it is empty (ReturnValue() is never appended to return_values).

Actually not, because if expectation._return_value is empty, then the (mutable) reference to it is not what gets assigned to the return_values variable, so expectation._return_value would not be mutated at all in the case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, right. Very confusing 😁 Anyway I think early return when return_valuesis empty would make this better.

if not return_value.raises:
return return_value.value
if inspect.isclass(return_value.raises):
raise return_value.raises(
*return_value.value["kargs"], **return_value.value["kwargs"]
)
raise return_value.raises # pylint: disable=raising-bad-type

def mock_method(runtime_self: Any, *kargs: Any, **kwargs: Any) -> Any:
arguments = {"kargs": kargs, "kwargs": kwargs}
expectation = FlexmockContainer.get_flexmock_expectation(self, name, arguments)
if expectation:
return _handle_matched_expectation(expectation, runtime_self, *kargs, **kwargs)
# inform the user which expectation(s) for the method were _not_ matched
expectations = [
expectation
error_msg = _format_args(name, arguments) + "\n".join(
"\nDid not match expectation %s" % _format_args(name, expectation._args)
ollipa marked this conversation as resolved.
Show resolved Hide resolved
for expectation in reversed(FlexmockContainer.flexmock_objects.get(self, []))
if expectation.name == name
]
error_msg = _format_args(name, arguments)
if expectations:
for expectation in expectations:
error_msg += "\nDid not match expectation %s" % _format_args(
name, expectation._args
)
)
# make sure to clean up expectations to ensure none of them
# interfere with the runner's error reporting mechanism
# e.g. open()
for _, expectations in FlexmockContainer.flexmock_objects.items():
for expectation in expectations:
_getattr(expectation, "reset")()
for expectation in itertools.chain.from_iterable(
christophe-riolo marked this conversation as resolved.
Show resolved Hide resolved
FlexmockContainer.flexmock_objects.values()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a new method to FlexmockContainer that does this? For example FlexmockContainer.saved_expectation.

):
expectation.reset()
raise MethodSignatureError(error_msg)

return mock_method


def flexmock_teardown() -> None:
"""Performs flexmock-specific teardown tasks."""
saved = {}
instances = []
classes = []
for mock_object, expectations in FlexmockContainer.flexmock_objects.items():
saved[mock_object] = expectations[:]
for expectation in expectations:
_getattr(expectation, "reset")()
for mock in saved:
obj = mock._object
if not isinstance(obj, Mock) and not inspect.isclass(obj):
instances.append(obj)
if inspect.isclass(obj):
classes.append(obj)
for obj in instances + classes:
saved_flexmock_objects = FlexmockContainer.flexmock_objects
all_expectations = list(itertools.chain.from_iterable(saved_flexmock_objects.values()))
for expectation in all_expectations[:]:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need to do a copy ? You are not modifying the list.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's needed because Exceptation.reset() calls del self.

expectation.reset()

def _is_instance_or_class(obj: Any) -> bool:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_instance_or_class = lambda obj: (not isinstance(obj, Mock) and not inspect.isclass(obj)) or inspect.isclass(obj)

return bool(
(not isinstance(obj, Mock) and not inspect.isclass(obj)) or inspect.isclass(obj)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that not already a boolean ? 🤔

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is but I guess it was done this way so that pylint doesn't complain about not using the ternary operator.

)

mocked_objects = [
mock._object for mock in saved_flexmock_objects if _is_instance_or_class(mock._object)
]
for obj in mocked_objects:
for attr in UPDATED_ATTRS:
try:
obj_dict = obj.__dict__
if obj_dict[attr].__code__ is Mock.__dict__[attr].__code__:
del obj_dict[attr]
except Exception:
except Exception: # pylint: disable=broad-except
try:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can again use contextlib.suppress here, it would look cleaner

if getattr(obj, attr).__code__ is Mock.__dict__[attr].__code__:
delattr(obj, attr)
Expand All @@ -473,9 +451,8 @@ def flexmock_teardown() -> None:

# make sure this is done last to keep exceptions here from breaking
# any of the previous steps that cleanup all the changes
for mock_object, expectations in saved.items():
for expectation in expectations:
_getattr(expectation, "verify")()
for expectation in all_expectations:
expectation.verify()


class Expectation:
Expand All @@ -486,6 +463,8 @@ class Expectation:
raise.
"""

# pylint: disable=too-many-instance-attributes

def __init__(
self,
mock: Mock,
Expand Down Expand Up @@ -551,7 +530,7 @@ def _get_runnable(self) -> str:
name = source.split("when(")[1].split(")")[0]
elif "def " in source:
name = source.split("def ")[1].split("(")[0]
except Exception:
except Exception: # pylint: disable=broad-except
# couldn't get the source, oh well
pass
return name
Expand Down
4 changes: 2 additions & 2 deletions src/flexmock/integrations.py
Expand Up @@ -53,7 +53,7 @@ def decorated(self: unittest.TextTestResult, test: unittest.TestCase) -> None:
try:
flexmock_teardown()
saved_add_success(self, test)
except Exception:
except Exception: # pylint: disable=broad-except
if hasattr(self, "_pre_flexmock_success"):
self.addFailure(test, sys.exc_info())
if hasattr(self, "_pre_flexmock_success"):
Expand All @@ -73,7 +73,7 @@ def _patch_add_success(klass: Type[unittest.TextTestResult]) -> None:

@wraps(klass.addSuccess)
def decorated(self: unittest.TextTestResult, _test: unittest.TestCase) -> None:
self._pre_flexmock_success = True # type: ignore
self._pre_flexmock_success = True # type: ignore # pylint: disable=protected-access

if klass.addSuccess is not decorated:
klass.addSuccess = decorated # type: ignore
Expand Down
10 changes: 6 additions & 4 deletions tests/flexmock_pytest_test.py
Expand Up @@ -6,12 +6,12 @@
from flexmock.api import flexmock_teardown
from flexmock.exceptions import MethodCallError
from tests import flexmock_test
from tests.flexmock_test import assert_raises


def test_module_level_test_for_pytest():
flexmock(foo="bar").should_receive("foo").once()
assert_raises(MethodCallError, flexmock_teardown)
with pytest.raises(MethodCallError):
flexmock_teardown()


@pytest.fixture()
Expand All @@ -26,14 +26,16 @@ def test_runtest_hook_with_fixture_for_pytest(runtest_hook_fixture):
class TestForPytest(flexmock_test.RegularClass):
def test_class_level_test_for_pytest(self):
flexmock(foo="bar").should_receive("foo").once()
assert_raises(MethodCallError, flexmock_teardown)
with pytest.raises(MethodCallError):
flexmock_teardown()


class TestUnittestClass(flexmock_test.TestFlexmockUnittest):
def test_unittest(self):
mocked = flexmock(a=2)
mocked.should_receive("a").once()
assert_raises(MethodCallError, flexmock_teardown)
with pytest.raises(MethodCallError):
flexmock_teardown()


class TestFailureOnException:
Expand Down