Skip to content

Commit

Permalink
Merge pull request #218 from paul-finary/feature/skip_actions
Browse files Browse the repository at this point in the history
Feature/skip actions
  • Loading branch information
roman-right committed Mar 14, 2022
2 parents 7cf3d53 + fc19b11 commit d087854
Show file tree
Hide file tree
Showing 12 changed files with 157 additions and 21 deletions.
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"

0 comments on commit d087854

Please sign in to comment.