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

Add async method mock #148

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 2 additions & 4 deletions .github/workflows/ci.yml
Expand Up @@ -16,10 +16,8 @@ jobs:
matrix:
python-version:
[
"pypy-3.7",
"pypy-3.8",
"pypy-3.9",
"3.7",
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 Pypy has released 3.10 version. You can include that here.

"3.8",
"3.9",
"3.10",
Expand Down Expand Up @@ -57,7 +55,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.7"
python-version: "3.11"
Copy link
Member

Choose a reason for hiding this comment

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

The tests are meant to be executed with the lowest Python version we support. Otherwise

Suggested change
python-version: "3.11"
python-version: "3.8"

cache: "poetry"

- name: Install dependencies
Expand Down Expand Up @@ -89,7 +87,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.7"
python-version: "3.11"
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
python-version: "3.11"
python-version: "3.8"

cache: "poetry"

- name: Install dependencies
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/coverage.yml
Expand Up @@ -21,7 +21,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.7"
python-version: "3.11"
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
python-version: "3.11"
python-version: "3.8"

cache: "poetry"

- name: Install dependencies
Expand Down
14 changes: 7 additions & 7 deletions CONTRIBUTING.md
Expand Up @@ -33,32 +33,32 @@ make lint

CI pipeline also runs tests using all the supported Python versions. You can use Tox if you want to run these tests yourself, but first you need to have all different Python versions available in your local system.

One option is to use [Pyenv](https://github.com/pyenv/pyenv) to manage different Python versions. For example, if you would want to run the test suite using Python versions 3.6 and 3.7, you can install the needed Python versions with the following commands:
One option is to use [Pyenv](https://github.com/pyenv/pyenv) to manage different Python versions. For example, if you would want to run the test suite using Python versions 3.10 and 3.11, you can install the needed Python versions with the following commands:

```
pyenv install 3.6.14
pyenv install 3.7.11
pyenv install 3.10.11
pyenv install 3.11.6
```

After the installation, you can make the installed Python versions available globally:

```
pyenv global 3.6.14 3.7.11
pyenv global 3.10.11 3.11.6
```

Finally, you can run Tox and it should discover the installed Python version automatically:

```
tox -e py36,py37
tox -e py310,py311
```

You can omit the `-e` argument if you want to run the tests against all supported Python versions. If everything works, the output should be something similar to this:

```
...
__________ summary __________
py36: commands succeeded
py37: commands succeeded
py310: commands succeeded
py311: commands succeeded
congratulations :)
```

Expand Down
3 changes: 1 addition & 2 deletions docs/index.md
Expand Up @@ -65,11 +65,10 @@ pip install flexmock

Tested to work with:

- Python 3.6
- Python 3.7
- Python 3.8
- Python 3.9
- Python 3.10
- Python 3.11
Copy link
Member

Choose a reason for hiding this comment

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

You can also add Python 3.12 here.

- PyPy3

Automatically integrates with all major test runners, including:
Expand Down
193 changes: 95 additions & 98 deletions docs/requirements.txt

Large diffs are not rendered by default.

317 changes: 163 additions & 154 deletions poetry.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions pyproject.toml
Expand Up @@ -18,7 +18,6 @@ classifiers = [
"Intended Audience :: Developers",
"Development Status :: 5 - Production/Stable",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
Expand Down Expand Up @@ -49,7 +48,7 @@ packages = [{ include = "flexmock", from = "src" }]
"Issue Tracker" = "https://github.com/flexmock/flexmock/issues"

[tool.poetry.dependencies]
python = "^3.7.1"
python = "^3.8.1"
Copy link
Member

Choose a reason for hiding this comment

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

3.8.0 should probably be okay. Earlier we had to bump it up to 3.7.1 because some dependency needed that version.

Suggested change
python = "^3.8.1"
python = "^3.8.0"


[tool.poetry.group.dev.dependencies]
pytest = "*"
Expand All @@ -59,6 +58,7 @@ black = "*"
isort = "*"
tox = "*"
Twisted = "*"
pytest-asyncio = "*"
pytest-cov = "*"
mkdocs-material = "*"
markdown-include = "*"
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -42,7 +42,7 @@
"Topic :: Software Development :: Testing :: Unit",
"Typing :: Typed",
],
python_requires=">=3.7.1,<4.0.0",
python_requires=">=3.8.1,<4.0.0",
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
python_requires=">=3.8.1,<4.0.0",
python_requires=">=3.8.0,<4.0.0",

packages=["flexmock"],
package_dir={"": "src"},
include_package_data=True,
Expand Down
77 changes: 77 additions & 0 deletions src/flexmock/_api.py
Expand Up @@ -28,6 +28,24 @@
RE_TYPE = type(re.compile(""))


async def future_raise(anything: type[BaseException]) -> None:
Copy link
Member

Choose a reason for hiding this comment

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

Using type directly requires Python 3.9.

Suggested change
async def future_raise(anything: type[BaseException]) -> None:
async def future_raise(anything: Type[BaseException]) -> None:

Copy link
Member

Choose a reason for hiding this comment

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

I guess we don't need these functions to be part of the public API?

Suggested change
async def future_raise(anything: type[BaseException]) -> None:
async def _future_raise(anything: type[BaseException]) -> None:

"""Raises the given exception in a a coroutine.

Args:
anything: an exception to be raised when the coroutine is resolved
"""
raise anything


async def future(anything: Any) -> Any:
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
async def future(anything: Any) -> Any:
async def _future(anything: Any) -> Any:

"""Return the given argument in a a coroutine.

Args:
anything: an exception to be returned when the coroutine is resolved
"""
return anything


class ReturnValue:
"""ReturnValue"""

Expand Down Expand Up @@ -229,6 +247,9 @@ def new_instances(self, *args: Any) -> "Expectation":

def _create_expectation(self, name: str, return_value: Optional[Any] = None) -> "Expectation":
expectation = self._get_or_create_expectation(name, return_value)
if hasattr(self._object, name):
expectation._is_async = inspect.iscoroutinefunction(getattr(self._object, name))

FlexmockContainer.add_expectation(self, expectation)

if _isproperty(self._object, name):
Expand Down Expand Up @@ -500,7 +521,14 @@ def _handle_matched_expectation(
args = return_value.value
assert isinstance(args, dict)
raise return_value.raises(*args["kargs"], **args["kwargs"])
if expectation._is_async:
return future_raise(return_value.raises)

raise return_value.raises # pylint: disable=raising-bad-type

if expectation._is_async:
return future(return_value.value)

return return_value.value

def mock_method(runtime_self: Any, *kargs: Any, **kwargs: Any) -> Any:
Expand Down Expand Up @@ -589,6 +617,7 @@ def __init__(
self._original = original

self._name = name
self._is_async: bool = False
self._times_called: int = 0
self._modifier: str = EXACTLY
self._args: Optional[Dict[str, Any]] = None
Expand Down Expand Up @@ -778,6 +807,14 @@ def _match_args(self, given_args: Any) -> bool:
return False
return True

def _verify_not_async_spy(self) -> None:
"""Check if trying to assert the output of an async call."""
is_spy = self._replace_with is self.__dict__.get("_original")
Copy link
Contributor

Choose a reason for hiding this comment

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

I am thinking, this could potentially be extracted in a property, maybe outside of this PR what do you think @ollipa ?


if self._is_async and is_spy:
caller_method = inspect.stack()[1].function
self.__raise(FlexmockError, caller_method + "() can not be used on an async spy")

def mock(self) -> Mock:
"""Return the mock associated with this expectation.

Expand Down Expand Up @@ -872,6 +909,7 @@ def and_return(self, *values: Any) -> "Expectation":
>>> plane.passenger_count()
3
"""
self._verify_not_async_spy()
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we really need to forbid this ? It feels like we could check the result of the spied function in an asynchronous way. It could require more work that could be on us @ollipa

Copy link
Member

Choose a reason for hiding this comment

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

We could forbid it now and allow it in the future when we make an implementation for it.

Copy link
Author

Choose a reason for hiding this comment

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

As olippa said. For now it would not work properly so I preferred raising a clear error.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, that's what I was suggesting, but so we could create an issue for that 👍

if not values:
value = None
elif len(values) == 1:
Expand Down Expand Up @@ -1063,6 +1101,43 @@ def at_most(self) -> "Expectation":
self._modifier = AT_MOST
return self

def make_async(self) -> "Expectation":
"""Set the return values of the expectation to coroutines
Need to be set before the return value is set
Comment on lines +1105 to +1106
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
"""Set the return values of the expectation to coroutines
Need to be set before the return value is set
"""Set the return value to be a coroutine.
Needs to be set before the return value is set.


Returns:
Self, i.e. can be chained with other Expectation methods.

Examples:
>>> flexmock(plane).should_receive("fly").make_async()
<flexmock._api.Expectation object at ...>
>>> plane.fly()
<coroutine object future at ...>
"""
if self._return_values:
self.__raise(FlexmockError, "make_async() should be used before setting a return value")
self._is_async = True

return self

def make_sync(self) -> "Expectation":
"""Make the mocked method synchronous.
Need to be set before the return value is set
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
Need to be set before the return value is set
Needs to be set before the return value is set.


Returns:
Self, i.e. can be chained with other Expectation methods.

Examples:
>>> flexmock(plane).should_receive("fly").make_sync()
<flexmock._api.Expectation object at ...>
>>> plane.fly()
"""
if self._return_values:
self.__raise(FlexmockError, "make_sync() should be used before setting a return value")
self._is_async = False

return self

def ordered(self) -> "Expectation":
"""Makes the expectation respect the order of `should_receive` statements.

Expand Down Expand Up @@ -1144,6 +1219,7 @@ def and_raise(self, exception: Type[BaseException], *args: Any, **kwargs: Any) -
>>> flexmock(plane).should_call("repair").and_raise(RuntimeError, "err msg")
<flexmock._api.Expectation object at ...>
"""
self._verify_not_async_spy()
if not self._callable:
self.__raise(FlexmockError, "can't use and_raise() with attribute stubs")
if inspect.isclass(exception):
Expand Down Expand Up @@ -1206,6 +1282,7 @@ def and_yield(self, *args: Any) -> "Expectation":
>>> next(log)
'land'
"""
self._verify_not_async_spy()
if not self._callable:
self.__raise(FlexmockError, "can't use and_yield() with attribute stubs")
return self.and_return(iter(args))
Expand Down
2 changes: 2 additions & 0 deletions tests/features/__init__.py
Expand Up @@ -8,6 +8,7 @@
from .conditional import ConditionalAssertionsTestCase
from .derived import DerivedTestCase
from .mocking import MockingTestCase
from .mocking_async import MockingAsyncTestCase
from .ordered import OrderedTestCase
from .proxied import ProxiedTestCase
from .spying import SpyingTestCase
Expand All @@ -22,6 +23,7 @@ class FlexmockTestCase(
ConditionalAssertionsTestCase,
DerivedTestCase,
MockingTestCase,
MockingAsyncTestCase,
OrderedTestCase,
ProxiedTestCase,
SpyingTestCase,
Expand Down