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

Feature/skip actions #218

Merged
merged 9 commits into from
Mar 14, 2022
Merged
6 changes: 5 additions & 1 deletion beanie/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
Replace,
SaveChanges,
ValidateOnSave,
Before,
After,
)
from beanie.odm.bulk import BulkWriter
from beanie.odm.fields import (
Expand All @@ -20,7 +22,7 @@
from beanie.odm.utils.general import init_beanie
from beanie.odm.documents import Document

__version__ = "1.10.0"
__version__ = "1.10.1"
__all__ = [
# ODM
"Document",
Expand All @@ -36,6 +38,8 @@
"Replace",
"SaveChanges",
"ValidateOnSave",
"Before",
"After",
# Bulk Write
"BulkWriter",
# Migrations
Expand Down
29 changes: 27 additions & 2 deletions beanie/odm/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import inspect
from enum import Enum
from functools import wraps
from typing import Callable, List, Union, Dict, TYPE_CHECKING, Any, Type
from typing import Callable, List, Union, Dict, TYPE_CHECKING, Any, Type, Optional

if TYPE_CHECKING:
from beanie.odm.documents import Document
Expand All @@ -26,6 +26,10 @@ class ActionDirections(str, Enum): # TODO think about this name
AFTER = "AFTER"


Before = ActionDirections.BEFORE
After = ActionDirections.AFTER


class ActionRegistry:
_actions: Dict[Type, Any] = {}

Expand Down Expand Up @@ -90,23 +94,31 @@ async def run_actions(
instance: "Document",
event_type: EventTypes,
action_direction: ActionDirections,
exclude: List[Union[ActionDirections, str]],
):
"""
Run actions
:param instance: Document - object of the Document subclass
:param event_type: EventTypes - event types
:param action_direction: ActionDirections - before or after
"""
if action_direction in exclude:
return

document_class = instance.__class__
actions_list = cls.get_action_list(
document_class, event_type, action_direction
)
coros = []
for action in actions_list:
if action.__name__ in exclude:
continue

if inspect.iscoroutinefunction(action):
coros.append(action(instance))
elif inspect.isfunction(action):
action(instance)

await asyncio.gather(*coros)


Expand Down Expand Up @@ -169,11 +181,23 @@ def wrap_with_actions(event_type: EventTypes):

def decorator(f: Callable):
@wraps(f)
async def wrapper(self, *args, **kwargs):
async def wrapper(
self,
*args,
skip_actions: Optional[List[Union[ActionDirections, str]]] = None,
**kwargs,
):
# Forwards the parameter
kwargs["skip_actions"] = skip_actions

if skip_actions is None:
skip_actions = []

await ActionRegistry.run_actions(
self,
event_type=event_type,
action_direction=ActionDirections.BEFORE,
exclude=skip_actions,
)

result = await f(self, *args, **kwargs)
Expand All @@ -182,6 +206,7 @@ async def wrapper(self, *args, **kwargs):
self,
event_type=event_type,
action_direction=ActionDirections.AFTER,
exclude=skip_actions,
)

return result
Expand Down
10 changes: 7 additions & 3 deletions beanie/odm/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ async def insert(
*,
link_rule: WriteRules = WriteRules.DO_NOTHING,
session: Optional[ClientSession] = None,
**kwargs,
) -> DocType:
"""
Insert the document (self) to the collection
Expand Down Expand Up @@ -599,6 +600,7 @@ async def replace(
session: Optional[ClientSession] = None,
bulk_writer: Optional[BulkWriter] = None,
link_rule: WriteRules = WriteRules.DO_NOTHING,
**kwargs,
) -> DocType:
"""
Fully update the document in the database
Expand Down Expand Up @@ -663,6 +665,7 @@ async def save(
self: DocType,
session: Optional[ClientSession] = None,
link_rule: WriteRules = WriteRules.DO_NOTHING,
**kwargs,
) -> DocType:
"""
Update an existing model in the database or insert it if it does not yet exist.
Expand Down Expand Up @@ -691,9 +694,9 @@ async def save(
)

try:
return await self.replace(session=session)
return await self.replace(session=session, **kwargs)
except (ValueError, DocumentNotFound):
return await self.insert(session=session)
return await self.insert(session=session, **kwargs)

@saved_state_needed
@wrap_with_actions(EventTypes.SAVE_CHANGES)
Expand All @@ -703,6 +706,7 @@ async def save_changes(
ignore_revision: bool = False,
session: Optional[ClientSession] = None,
bulk_writer: Optional[BulkWriter] = None,
**kwargs,
) -> None:
"""
Save changes.
Expand Down Expand Up @@ -1211,7 +1215,7 @@ def dict(
)

@wrap_with_actions(event_type=EventTypes.VALIDATE_ON_SAVE)
async def validate_self(self):
async def validate_self(self, *args, **kwargs):
# TODO it can be sync, but needs some actions controller improvements
if self.get_settings().model_settings.validate_on_save:
self.parse_obj(self)
Expand Down
2 changes: 1 addition & 1 deletion beanie/odm/utils/self_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
def validate_self_before(f: Callable):
@wraps(f)
async def wrapper(self: "DocType", *args, **kwargs):
await self.validate_self()
await self.validate_self(*args, **kwargs)
return await f(self, *args, **kwargs)

return wrapper
13 changes: 12 additions & 1 deletion docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
# Changelog

Beanie project
Beanie project

## [1.10.1] - 2022-02-24

### Improvement

- Skip actions

### Implementation

- Author - [Paul Renvoisé](https://github.com/paul-finary)
- PR <https://github.com/roman-right/beanie/pull/218>

## [1.10.0] - 2022-02-24

Expand Down
51 changes: 47 additions & 4 deletions docs/tutorial/actions.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
# Event-based actions

You can register methods as pre- or post- actions for document events like `insert`, `replace` and etc.
You can register methods as pre- or post- actions for document events.

Currently supported events:

- Insert
- Replace
- SaveChanges
- ValidateOnSave

Currently supported directions:
- Before
- After

Current operations creating events:
- `insert()` and `save()` for Insert
- `replace()` and `save()` for Replace
- `save_changes()` for SaveChanges
- `insert()`, `replace()`, `save_changes()`, and `save()` for ValidateOnSave

To register an action you can use `@before_event` and `@after_event` decorators respectively.

```python
Expand Down Expand Up @@ -41,7 +50,7 @@ class Sample(Document):
self.name = self.name.capitalize()
```

This will capitalize the `name` field value before each document insert and replace
This will capitalize the `name` field value before each document insert and replace.

And sync and async methods could work as actions.

Expand All @@ -55,4 +64,38 @@ class Sample(Document):
@after_event([Insert, Replace])
async def send_callback(self):
await client.send(self.id)
```
```

Actions can be selectively skipped by passing the parameter `skip_actions` when calling
the operations that trigger events. `skip_actions` accepts a list of directions and action names.

```python
from beanie import Insert, Replace, Before, After

class Sample(Document):
num: int
name: str

@before_event(Insert)
def capitalize_name(self):
self.name = self.name.capitalize()

@before_event(Replace)
def redact_name(self):
self.name = "[REDACTED]"

@after_event(Replace)
def num_change(self):
self.num -= 1

sample = Sample()

# capitalize_name will not be executed
await sample.insert(skip_actions=['capitalize_name'])

# num_change will not be executed
await sample.replace(skip_actions=[After])

# redact_name and num_change will not be executed
await sample.replace(skip_actions[Before, 'num_change'])
```
3 changes: 2 additions & 1 deletion docs/tutorial/state_management.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ i = Item(name="Test", attributes={"attribute_1": 1.0, "attribute_2": 2.0})
await i.insert()
i.attributes = {"attribute_1": 1.0}
await i.save_changes()
# Changes will consist of: {"attributes.attribute": 1.0}
# Changes will consist of: {"attributes.attribute_1": 1.0}
# Keeping attribute_2
```

However, there's some cases where you want to replace the whole object when one of its attributes changed.
Expand Down
4 changes: 2 additions & 2 deletions pydoc-markdown.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ renderer:
- title: Getting started
source: docs/getting-started.md
- title: Tutorial
children:
children:
- title: Defining a document
source: docs/tutorial/defining-a-document.md
- title: Initialization
Expand All @@ -72,7 +72,7 @@ renderer:
source: docs/tutorial/cache.md
- title: Revision
source: docs/tutorial/revision.md
- title: Save changes
- title: State Management
source: docs/tutorial/state_management.md
- title: On save validation
source: docs/tutorial/on_save_validation.md
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 = "beanie"
version = "1.10.0"
version = "1.10.1"
description = "Asynchronous Python ODM for MongoDB"
authors = ["Roman <roman-right@protonmail.com>"]
license = "Apache-2.0"
Expand Down
6 changes: 6 additions & 0 deletions tests/odm/documents/test_validation_on_save.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,9 @@ async def test_validate_on_save_action():
doc = DocumentWithValidationOnSave(num_1=1, num_2=2)
await doc.insert()
assert doc.num_2 == 3


async def test_validate_on_save_skip_action():
doc = DocumentWithValidationOnSave(num_1=1, num_2=2)
await doc.insert(skip_actions=['num_2_plus_1'])
assert doc.num_2 == 2
50 changes: 46 additions & 4 deletions tests/odm/test_actions.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,65 @@
import pytest

from beanie import Before, After

from tests.odm.models import DocumentWithActions, InheritedDocumentWithActions


@pytest.mark.parametrize(
"doc_class", [DocumentWithActions, InheritedDocumentWithActions]
)
async def test_actions_insert_replace(doc_class):
test_name = "test_name"
async def test_actions_insert(doc_class):
test_name = f"test_actions_insert_{doc_class}"
sample = doc_class(name=test_name)

# TEST INSERT
await sample.insert()
assert sample.name != test_name
assert sample.name == test_name.capitalize()
assert sample.num_1 == 1
assert sample.num_2 == 9

# TEST REPLACE

@pytest.mark.parametrize(
"doc_class", [DocumentWithActions, InheritedDocumentWithActions]
)
async def test_actions_replace(doc_class):
test_name = f"test_actions_replace_{doc_class}"
sample = doc_class(name=test_name)

await sample.insert()

await sample.replace()
assert sample.num_1 == 2
assert sample.num_3 == 99


@pytest.mark.parametrize(
"doc_class", [DocumentWithActions, InheritedDocumentWithActions]
)
async def test_skip_actions_insert(doc_class):
test_name = f"test_skip_actions_insert_{doc_class}"
sample = doc_class(name=test_name)

await sample.insert(skip_actions=[After, 'capitalize_name'])
# capitalize_name has been skipped
assert sample.name == test_name
# add_one has not been skipped
assert sample.num_1 == 1
# num_2_change has been skipped
assert sample.num_2 == 10


@pytest.mark.parametrize(
"doc_class", [DocumentWithActions, InheritedDocumentWithActions]
)
async def test_skip_actions_replace(doc_class):
test_name = f"test_skip_actions_replace{doc_class}"
sample = doc_class(name=test_name)

await sample.insert()

await sample.replace(skip_actions=[Before, 'num_3_change'])
# add_one has been skipped
assert sample.num_1 == 1
# num_3_change has been skipped
assert sample.num_3 == 100
2 changes: 1 addition & 1 deletion tests/test_beanie.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@


def test_version():
assert __version__ == "1.10.0"
assert __version__ == "1.10.1"