diff --git a/beanie/__init__.py b/beanie/__init__.py index 3556d6b8..37878b4a 100644 --- a/beanie/__init__.py +++ b/beanie/__init__.py @@ -26,7 +26,7 @@ from beanie.odm.views import View from beanie.odm.union_doc import UnionDoc -__version__ = "1.11.12" +__version__ = "1.12.0" __all__ = [ # ODM "Document", diff --git a/beanie/odm/documents.py b/beanie/odm/documents.py index b9d7b1b9..6401cbb7 100644 --- a/beanie/odm/documents.py +++ b/beanie/odm/documents.py @@ -212,10 +212,14 @@ async def insert( ]: if isinstance(value, Document): await value.insert(link_rule=WriteRules.WRITE) - if field_info.link_type == LinkTypes.LIST: - for obj in value: - if isinstance(obj, Document): - await obj.insert(link_rule=WriteRules.WRITE) + if field_info.link_type in [ + LinkTypes.LIST, + LinkTypes.OPTIONAL_LIST + ]: + if isinstance(value, List): + for obj in value: + if isinstance(obj, Document): + await obj.insert(link_rule=WriteRules.WRITE) result = await self.get_motor_collection().insert_one( get_dict(self, to_db=True), session=session @@ -343,15 +347,19 @@ async def replace( ignore_revision=ignore_revision, session=session, ) - if field_info.link_type == LinkTypes.LIST: - for obj in value: - if isinstance(obj, Document): - await obj.replace( - link_rule=link_rule, - bulk_writer=bulk_writer, - ignore_revision=ignore_revision, - session=session, - ) + if field_info.link_type in [ + LinkTypes.LIST, + LinkTypes.OPTIONAL_LIST + ]: + if isinstance(value, List): + for obj in value: + if isinstance(obj, Document): + await obj.replace( + link_rule=link_rule, + bulk_writer=bulk_writer, + ignore_revision=ignore_revision, + session=session, + ) use_revision_id = self.get_settings().use_revision find_query: Dict[str, Any] = {"_id": self.id} @@ -396,12 +404,16 @@ async def save( await value.save( link_rule=link_rule, session=session ) - if field_info.link_type == LinkTypes.LIST: - for obj in value: - if isinstance(obj, Document): - await obj.save( - link_rule=link_rule, session=session - ) + if field_info.link_type in [ + LinkTypes.LIST, + LinkTypes.OPTIONAL_LIST + ]: + if isinstance(value, List): + for obj in value: + if isinstance(obj, Document): + await obj.save( + link_rule=link_rule, session=session + ) try: return await self.replace(session=session, **kwargs) @@ -658,13 +670,17 @@ async def delete( link_rule=DeleteRules.DELETE_LINKS, **pymongo_kwargs, ) - if field_info.link_type == LinkTypes.LIST: - for obj in value: - if isinstance(obj, Document): - await obj.delete( - link_rule=DeleteRules.DELETE_LINKS, - **pymongo_kwargs, - ) + if field_info.link_type in [ + LinkTypes.LIST, + LinkTypes.OPTIONAL_LIST + ]: + if isinstance(value, List): + for obj in value: + if isinstance(obj, Document): + await obj.delete( + link_rule=DeleteRules.DELETE_LINKS, + **pymongo_kwargs, + ) return await self.find_one({"_id": self.id}).delete( session=session, bulk_writer=bulk_writer, **pymongo_kwargs diff --git a/beanie/odm/fields.py b/beanie/odm/fields.py index b40421b5..9e54b880 100644 --- a/beanie/odm/fields.py +++ b/beanie/odm/fields.py @@ -131,6 +131,7 @@ class LinkTypes(str, Enum): DIRECT = "DIRECT" OPTIONAL_DIRECT = "OPTIONAL_DIRECT" LIST = "LIST" + OPTIONAL_LIST = "OPTIONAL_LIST" class LinkInfo(BaseModel): diff --git a/beanie/odm/utils/encoder.py b/beanie/odm/utils/encoder.py index d1c5accd..fdfda831 100644 --- a/beanie/odm/utils/encoder.py +++ b/beanie/odm/utils/encoder.py @@ -102,6 +102,11 @@ def encode_document(self, obj): obj_dict[k] = o.to_ref() else: obj_dict[k] = o + if link_fields[k].link_type == LinkTypes.OPTIONAL_LIST: + if o is not None: + obj_dict[k] = [link.to_ref() for link in o] + else: + obj_dict[k] = o else: obj_dict[k] = o obj_dict[k] = encoder.encode(obj_dict[k]) diff --git a/beanie/odm/utils/relations.py b/beanie/odm/utils/relations.py index d54e77f2..d0ebfec7 100644 --- a/beanie/odm/utils/relations.py +++ b/beanie/odm/utils/relations.py @@ -38,8 +38,12 @@ def detect_link(field: ModelField) -> Optional[LinkInfo]: ): internal_field = field.sub_fields[0] # type: ignore if internal_field.type_ == Link: - if internal_field.allow_none is True: - return None + if field.allow_none is True: + return LinkInfo( + field=field.name, + model_class=internal_field.sub_fields[0].type_, # type: ignore + link_type=LinkTypes.OPTIONAL_LIST, + ) return LinkInfo( field=field.name, model_class=internal_field.sub_fields[0].type_, # type: ignore diff --git a/docs/changelog.md b/docs/changelog.md index bf92b985..10f8ca73 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -2,6 +2,17 @@ Beanie project +## [1.12.0] - 2022-10-06 + +### Improvement + +- Optional list of links field + +### Implementation + +- Author - [Alex Deng](https://github.com/rga-alex-deng) +- PR + ## [1.11.12] - 2022-09-28 ### Improvement @@ -973,4 +984,6 @@ how specific type should be presented in the database [1.11.11]: https://pypi.org/project/beanie/1.11.11 -[1.11.12]: https://pypi.org/project/beanie/1.11.12 \ No newline at end of file +[1.11.12]: https://pypi.org/project/beanie/1.11.12 + +[1.12.0]: https://pypi.org/project/beanie/1.12.0 \ No newline at end of file diff --git a/docs/tutorial/relations.md b/docs/tutorial/relations.md index f589bb7f..1012e53c 100644 --- a/docs/tutorial/relations.md +++ b/docs/tutorial/relations.md @@ -9,6 +9,7 @@ The next field types are supported: - `Link[...]` - `Optional[Link[...]]` - `List[Link[...]]` +- `Optional[List[Link[...]]]` Direct link to the document: @@ -63,6 +64,28 @@ class House(Document): windows: List[Link[Window]] ``` +Optional List of the links: + +```python +from typing import List, Optional + +from beanie import Document, Link + +class Window(Document): + x: int = 10 + y: int = 10 + +class Yard(Document): + v: int = 10 + y: int = 10 + +class House(Document): + name: str + door: Link[Door] + windows: List[Link[Window]] + yards: Optional[List[Link[Yard]]] +``` + Other link patterns are not supported for at this moment. If you need something more specific for your use-case, please open an issue on the GitHub page - diff --git a/pyproject.toml b/pyproject.toml index 88d88d86..72a81bec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "beanie" -version = "1.11.12" +version = "1.12.0" description = "Asynchronous Python ODM for MongoDB" authors = ["Roman "] license = "Apache-2.0" diff --git a/tests/odm/conftest.py b/tests/odm/conftest.py index 5f59209d..56c4b130 100644 --- a/tests/odm/conftest.py +++ b/tests/odm/conftest.py @@ -30,6 +30,7 @@ Window, Door, Roof, + Yard, InheritedDocumentWithActions, DocumentForEncodingTest, DocumentForEncodingTestDate, @@ -38,7 +39,8 @@ DocumentUnion, HouseWithRevision, WindowWithRevision, - DocumentWithActions2, + YardWithRevision, + DocumentWithActions2 ) from tests.odm.views import TestView from tests.odm.models import ( @@ -160,6 +162,7 @@ async def init(loop, db): Window, Door, Roof, + Yard, InheritedDocumentWithActions, DocumentForEncodingTest, DocumentForEncodingTestDate, @@ -169,7 +172,8 @@ async def init(loop, db): DocumentUnion, HouseWithRevision, WindowWithRevision, - DocumentWithActions2, + YardWithRevision, + DocumentWithActions2 ] await init_beanie( database=db, diff --git a/tests/odm/models.py b/tests/odm/models.py index cf645d0f..1c38f502 100644 --- a/tests/odm/models.py +++ b/tests/odm/models.py @@ -376,6 +376,11 @@ class DocumentWithExtrasKw(Document, extra=Extra.allow): num_1: int +class Yard(Document): + v: int + w: int + + class Window(Document): x: int y: int @@ -393,6 +398,7 @@ class House(Document): windows: List[Link[Window]] door: Link[Door] roof: Optional[Link[Roof]] + yards: Optional[List[Link[Yard]]] name: Indexed(str) = Field(hidden=True) height: Indexed(int) = 2 @@ -448,6 +454,15 @@ class Settings: union_doc = DocumentUnion +class YardWithRevision(Document): + v: int + w: int + + class Settings: + use_revision = True + use_state_management = True + + class WindowWithRevision(Document): x: int y: int diff --git a/tests/odm/test_relations.py b/tests/odm/test_relations.py index 867cc78a..e8b3b222 100644 --- a/tests/odm/test_relations.py +++ b/tests/odm/test_relations.py @@ -2,7 +2,7 @@ from beanie.exceptions import DocumentWasNotSaved from beanie.odm.fields import WriteRules, Link, DeleteRules -from tests.odm.models import Window, Door, House, Roof +from tests.odm.models import Window, Door, House, Roof, Yard @pytest.fixture @@ -31,9 +31,14 @@ async def house(house_not_inserted): async def houses(): for i in range(10): roof = Roof() if i % 2 == 0 else None + if i % 2 == 0: + yards = [Yard(v=10, w=10 + i), Yard(v=11, w=10 + i)] + else: + yards = None house = await House( door=Door(t=i), windows=[Window(x=10, y=10 + i), Window(x=11, y=11 + i)], + yards=yards, roof=roof, name="test", height=i, @@ -74,6 +79,9 @@ async def test_prefetch_find_many(self, houses): assert len(items) == 7 for window in items[0].windows: assert isinstance(window, Link) + assert items[0].yards is None + for yard in items[1].yards: + assert isinstance(yard, Link) assert isinstance(items[0].door, Link) assert items[0].roof is None assert isinstance(items[1].roof, Link) @@ -86,6 +94,9 @@ async def test_prefetch_find_many(self, houses): assert len(items) == 7 for window in items[0].windows: assert isinstance(window, Window) + assert items[0].yards == [] + for yard in items[1].yards: + assert isinstance(yard, Yard) assert isinstance(items[0].door, Door) assert items[0].roof is None assert isinstance(items[1].roof, Roof) diff --git a/tests/test_beanie.py b/tests/test_beanie.py index 81542e9f..d838c615 100644 --- a/tests/test_beanie.py +++ b/tests/test_beanie.py @@ -2,4 +2,4 @@ def test_version(): - assert __version__ == "1.11.12" + assert __version__ == "1.12.0"