From 746aa9f813a0bd48bc4cb65df68c3469626899fb Mon Sep 17 00:00:00 2001 From: Michael Bianco Date: Thu, 17 Aug 2023 12:43:57 -0600 Subject: [PATCH 1/3] feat: support list in non-nested mutable json --- sqlalchemy_json/__init__.py | 18 ++++++++++++++++-- test/test_sqlalchemy_json.py | 20 ++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/sqlalchemy_json/__init__.py b/sqlalchemy_json/__init__.py index fcfb8e6..c1c40e9 100644 --- a/sqlalchemy_json/__init__.py +++ b/sqlalchemy_json/__init__.py @@ -1,4 +1,4 @@ -from sqlalchemy.ext.mutable import Mutable, MutableDict +from sqlalchemy.ext.mutable import Mutable, MutableDict, MutableList from sqlalchemy.types import JSON from .track import TrackedDict, TrackedList @@ -50,6 +50,20 @@ def coerce(cls, key, value): return NestedMutableList.coerce(key, value) return super(cls).coerce(key, value) +class MutableListOrDict(Mutable): + """SQLAlchemy `mutable` extension with change tracking for a single-depth list or dict.""" + + @classmethod + def coerce(cls, key, value): + if value is None: + return value + if isinstance(value, cls): + return value + if isinstance(value, dict): + return MutableDict.coerce(key, value) + if isinstance(value, list): + return MutableList.coerce(key, value) + return super(cls).coerce(key, value) def mutable_json_type(dbtype=JSON, nested=False): """Type creator for (optionally nested) mutable JSON column types. @@ -57,7 +71,7 @@ def mutable_json_type(dbtype=JSON, nested=False): The default backend data type is sqlalchemy.types.JSON, but can be set to any other type by providing the `dbtype` parameter. """ - mutable_type = NestedMutable if nested else MutableDict + mutable_type = NestedMutable if nested else MutableListOrDict return mutable_type.as_mutable(dbtype) diff --git a/test/test_sqlalchemy_json.py b/test/test_sqlalchemy_json.py index 939f99c..3fd6473 100644 --- a/test/test_sqlalchemy_json.py +++ b/test/test_sqlalchemy_json.py @@ -67,6 +67,20 @@ def author(session): assert author.handles["twitter"] == "@JohnDoe" return author +@pytest.fixture +def author_with_list(session): + author = Author( + name="John Doe", + handles=["@JohnDoe", "JohnDoe"], + ) + session.add(author) + session.commit() + session.refresh(author) + + assert author.name == "John Doe" + assert author.handles == ["@JohnDoe", "JohnDoe"] + + return author @pytest.fixture def article(session, author): @@ -156,3 +170,9 @@ def test_dict_merging(session, article): "someone/somerepo": 10, }, } + +def test_mutable_json_list(session, author_with_list): + author_with_list.handles.append("@mike_bianco") + session.commit() + + assert author_with_list.handles == ["@JohnDoe", "JohnDoe", "@mike_bianco"] \ No newline at end of file From ab6753dd5550dc18f9c990910026de7bcf698fff Mon Sep 17 00:00:00 2001 From: Michael Bianco Date: Thu, 17 Aug 2023 12:47:06 -0600 Subject: [PATCH 2/3] docs: adding readme example --- README.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.rst b/README.rst index a559171..32a39a1 100644 --- a/README.rst +++ b/README.rst @@ -21,6 +21,20 @@ This is essentially the SQLAlchemy `mutable JSON recipe`_. We define a simple au name = Column(Text) handles = Column(MutableJson) +Or, using the declarative mapping style: + +.. code-block:: python + + class Category(Base): + __tablename__ = "categories" + + id = mapped_column(Integer, primary_key=True) + created_at: Mapped[DateTime] = mapped_column(DateTime, default=datetime.now) + updated_at: Mapped[DateTime] = mapped_column( + DateTime, default=datetime.now, onupdate=datetime.now + ) + keywords: Mapped[list[str]] = mapped_column(MutableJson) + The example below loads one of the existing authors and retrieves the mapping of social media handles. The error in the twitter handle is then corrected and committed. The change is detected by SQLAlchemy and the appropriate ``UPDATE`` statement is generated. .. code-block:: python From ae84ce49e37556acee412126a551cb1842c56018 Mon Sep 17 00:00:00 2001 From: Michael Bianco Date: Fri, 18 Aug 2023 08:53:54 -0600 Subject: [PATCH 3/3] Apply suggestions from code review Co-authored-by: Elmer de Looff --- sqlalchemy_json/__init__.py | 1 + test/test_sqlalchemy_json.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/sqlalchemy_json/__init__.py b/sqlalchemy_json/__init__.py index c1c40e9..7350814 100644 --- a/sqlalchemy_json/__init__.py +++ b/sqlalchemy_json/__init__.py @@ -50,6 +50,7 @@ def coerce(cls, key, value): return NestedMutableList.coerce(key, value) return super(cls).coerce(key, value) + class MutableListOrDict(Mutable): """SQLAlchemy `mutable` extension with change tracking for a single-depth list or dict.""" diff --git a/test/test_sqlalchemy_json.py b/test/test_sqlalchemy_json.py index 3fd6473..3fe6962 100644 --- a/test/test_sqlalchemy_json.py +++ b/test/test_sqlalchemy_json.py @@ -79,7 +79,6 @@ def author_with_list(session): assert author.name == "John Doe" assert author.handles == ["@JohnDoe", "JohnDoe"] - return author @pytest.fixture @@ -175,4 +174,5 @@ def test_mutable_json_list(session, author_with_list): author_with_list.handles.append("@mike_bianco") session.commit() - assert author_with_list.handles == ["@JohnDoe", "JohnDoe", "@mike_bianco"] \ No newline at end of file + assert author_with_list.handles == ["@JohnDoe", "JohnDoe", "@mike_bianco"] + \ No newline at end of file