Skip to content

Commit

Permalink
Prepare 0.26 (#113)
Browse files Browse the repository at this point in the history
* Update docs

* Bump version

* Fix typo

* Add fixture for overriding

* Rewrite tests with pytester

* Remove commented plugins
  • Loading branch information
yakimka authored Oct 6, 2024
1 parent 9448ffa commit 7e55ab0
Show file tree
Hide file tree
Showing 9 changed files with 248 additions and 98 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ We follow [Semantic Versions](https://semver.org/).

## Version next

## Version 0.26.0

- Rewrite `helpers.enter` as class-based context manager (now it behaves more predictably)
- Added `registry.touched` property and `registry.clear_touched` method for tracking
dependencies usage (useful for testing)
- Added pytest integration for simpler testing

## Version 0.25.0

Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ mypy: ## Run mypy
.PHONY: test
test: ## Run tests
$(RUN) poetry run pytest --cov=tests --cov=picodi $(args)
$(RUN) poetry run pytest --dead-fixtures || true
$(RUN) poetry run pytest --dead-fixtures

.PHONY: test-docs
test-docs: ## Check docs
Expand Down
2 changes: 1 addition & 1 deletion ci.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ run_ci () {
poetry check
poetry run pip check
poetry run pytest --cov=tests --cov=picodi --cov-report=xml --junitxml=jcoverage.xml
poetry run pytest --dead-fixtures || true
poetry run pytest --dead-fixtures
poetry build
poetry export --format=requirements.txt --output=dist/requirements.txt
# print shasum of the built packages
Expand Down
122 changes: 69 additions & 53 deletions docs/testing.rst
Original file line number Diff line number Diff line change
@@ -1,48 +1,43 @@
Testing
=======

Overriding dependencies
Overriding Dependencies
-----------------------

Picodi aims to be a good citizen by providing a way to test your code. It
enables you to :doc:`override` dependencies in your tests, allowing you to replace
enables you to :doc:`overriding` in your tests, allowing you to replace
them with mocks or stubs as needed.

Picodi's lifespan in tests
Picodi's Lifespan in Tests
--------------------------

For effective testing, it’s essential to have a clean environment for each test.
Picodi's scopes can introduce issues if not managed properly. To avoid this,
you should ensure that dependencies are properly teardown after each test.
In this example, we use the ``pytest`` framework, but the same principle applies
to other test frameworks.
For effective testing, it’s essential to maintain a clean environment for each test.
Picodi's scopes can introduce issues if not managed properly. To prevent this,
ensure that dependencies are properly torn down after each test.
In this example, we showcase example of pseudo framework,
but you can adapt it to your testing framework.

.. testcode::

# root conftest.py
import pytest

from picodi import shutdown_dependencies


@pytest.fixture(autouse=True)
async def _shutdown_picodi_dependencies():
yield
async def teardown_hook():
await shutdown_dependencies()


Detecting dependency usage
Detecting Dependency Usage
--------------------------

Sometimes, you need to know whether a particular dependency was used during a test so
that you can run cleanup logic.
that you can run cleanup logic afterward.
For example, you may need to clear database tables, collections, etc., after each test.
While it's possible to write a custom dependency with teardown logic and override it
While it is possible to write a custom dependency with teardown logic and override it
in tests, this approach is not always convenient.

Picodi provides a way to detect if a dependency was used in a test. For example:
Picodi provides a way to detect if a dependency was used in a test. For instance:

You have a MongoDB dependency, and you want to drop the test database during the teardown.
You have a MongoDB dependency, and you want to drop the test database during teardown.

.. code-block:: python
Expand Down Expand Up @@ -73,91 +68,108 @@ You have a MongoDB dependency, and you want to drop the test database during the
.. testcode::

# root conftest.py
import pytest

from picodi import registry
from picodi.helpers import enter

# from deps import get_mongo_database, get_mongo_client, get_mongo_database_name


# Override the MongoDB database name for tests
@pytest.fixture(autouse=True)
async def _override_deps_for_tests():
# Override MongoDB database name for tests
async def setup_hook():
with registry.override(get_mongo_database_name, lambda: "test_db"):
yield


@pytest.fixture(autouse=True)
async def _drop_mongo_database():
yield
async def teardown_hook(mongo_test_db_name):
# If the `get_mongo_database` dependency was used, this block will execute,
# and the test database will be dropped during teardown
# and the test database will be dropped during teardown.
if get_mongo_database in registry.touched:
async with enter(get_mongo_client) as mongo_client:
await mongo_client.drop_database("test_db")
await mongo_client.drop_database(mongo_test_db_name)

# Clear touched dependencies after each test.
# This is important to properly detect dependency usage.
# Clear touched dependencies after each test to ensure correct detection
registry.clear_touched()


Pytest integration
Pytest Integration
------------------

Section above shows how to integrate Picodi with `pytest` by yourself. However, Picodi
provides a built-in ``pytest`` plugin that simplifies the process.
Picodi provides a built-in ``pytest`` plugin that simplifies the process of
managing dependencies in your tests.

Setup pytest plugin
********************
Setting Up the Pytest Plugin
****************************

To use builtin Picodi plugin for pytest you need to add to root conftest.py of your project:
To use Picodi's built-in plugin for pytest,
add the following to the root ``conftest.py`` of your project:

.. code-block:: python
.. testcode::

# conftest.py
pytest_plugins = [
"picodi.integrations._pytest",
# If you use asyncio in your tests, add also the following plugin,
# it needs to be added after the main plugin.
# If you use asyncio in your tests, add the following plugin as well.
# It must be added after the main plugin.
"picodi.integrations._pytest_asyncio",
]

For using ``_pytest_asyncio`` plugin you need to install
To use the ``_pytest_asyncio`` plugin, you need to install the
`pytest-asyncio <https://pypi.org/project/pytest-asyncio/>`_ package.

Now Picodi will automatically handle dependency shutdown and cleanup for you.
Now, Picodi will automatically handle dependency shutdown and cleanup for you.

Override marker
****************
Override Marker
***************

You can use ``picodi_override`` marker to override dependencies in your tests.
You can use the ``picodi_override`` marker to override dependencies in your tests.

.. code-block:: python
import pytest
@pytest.mark.picodi_override(original_dependency, override_dependency)
def test_foo(): ...
def test_foo():
pass
# or for multiple dependencies at once
# Or for multiple dependencies at once:
@pytest.mark.picodi_override(
[
(original_dependency, override_dependency),
(second_original_dependency, second_override_dependency),
]
)
def test_bar(): ...
def test_bar():
pass
Override Fixture
*****************

You can also use a ``picodi_overrides`` fixture to override dependencies in your tests.

.. testcode::

import pytest


@pytest.fixture()
def picodi_overrides():
return [(original_dependency, override_dependency)]


@pytest.mark.usefixtures("picodi_overrides")
def test_foo():
pass

Example
********
*******

So previous example can be rewritten as:
The previous examples can be rewritten as:

.. code-block:: python
# root conftest.py
import pytest
from picodi import registry
Expand All @@ -167,12 +179,16 @@ So previous example can be rewritten as:
pytestmark = pytest.mark.picodi_override(get_mongo_database_name, lambda: "test_db")
# `shutdown_dependencies` is called automatically after each test
@pytest.fixture(autouse=True)
async def _drop_mongo_database():
yield
# If the `get_mongo_database` dependency was used, this block will execute,
# and the test database will be dropped during teardown
# and the test database will be dropped during teardown.
if get_mongo_database in registry.touched:
async with enter(get_mongo_client) as mongo_client:
await mongo_client.drop_database("test_db")
# `registry.clear_touched()` is called automatically after each test
16 changes: 14 additions & 2 deletions picodi/integrations/_pytest.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,21 @@ def _picodi_teardown(_picodi_clear_touched: None, _picodi_shutdown: None) -> Non


@pytest.fixture()
def picodi_overrides(request: pytest.FixtureRequest) -> list[tuple[Callable, Callable]]:
def picodi_overrides() -> list[tuple[Callable, Callable]]:
"""
Get overrides from markers that will be used in the test.
Usually, you don't need to use this fixture directly.
Use `picodi_override` marker instead. But if you want to stick to
some custom logic you can inherit from this fixture.
"""
return []


@pytest.fixture()
def picodi_overrides_from_marks(
request: pytest.FixtureRequest,
) -> list[tuple[Callable, Callable]]:
for marker in request.node.iter_markers(name="picodi_override"):
if len(marker.args) not in (1, 2):
pytest.fail(
Expand All @@ -76,8 +83,13 @@ def picodi_overrides(request: pytest.FixtureRequest) -> list[tuple[Callable, Cal
@pytest.fixture(autouse=True)
def _picodi_override_setup(
picodi_overrides: list[tuple[Callable, Callable]],
picodi_overrides_from_marks: list[tuple[Callable, Callable]],
) -> Generator[None, None, None]:
overrides = []
for item in picodi_overrides_from_marks + picodi_overrides:
if item not in overrides:
overrides.append(item)
with ExitStack() as stack:
for get_dep, override in picodi_overrides:
for get_dep, override in overrides:
stack.enter_context(registry.override(get_dep, override))
yield
4 changes: 2 additions & 2 deletions picodi/integrations/_pytest_asyncio.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

from typing import TYPE_CHECKING

import pytest
import pytest_asyncio

from picodi import shutdown_dependencies

if TYPE_CHECKING:
from collections.abc import AsyncGenerator


@pytest.fixture()
@pytest_asyncio.fixture()
async def _picodi_shutdown() -> AsyncGenerator[None, None]:
"""
Shutdown dependencies after the test (async version).
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry]
name = "picodi"
description = "Simple Dependency Injection for Python"
version = "0.25.0"
version = "0.26.0"
license = "MIT"
authors = [
"yakimka"
Expand Down
11 changes: 4 additions & 7 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import pytest

from picodi import registry
from picodi import registry, shutdown_dependencies

pytest_plugins = [
"picodi.integrations._pytest",
"picodi.integrations._pytest_asyncio",
"pytester",
]
pytest_plugins = ["pytester"]


@pytest.fixture(autouse=True)
async def _clear_registry():
async def _cleanup():
yield
await shutdown_dependencies()
registry.clear()


Expand Down
Loading

0 comments on commit 7e55ab0

Please sign in to comment.