diff --git a/docs/conf.py b/docs/conf.py index 68a845f..467cd41 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -15,7 +15,7 @@ project = "mock-alchemy" author = "Rajiv Sarvepalli" copyright = f"{datetime.now().year}, {author}" -version = "0.2.0" +version = "0.2.2" extensions = [ "sphinx.ext.autodoc", "sphinx.ext.napoleon", diff --git a/docs/user_guide/index.rst b/docs/user_guide/index.rst index f5baffa..160c4c5 100644 --- a/docs/user_guide/index.rst +++ b/docs/user_guide/index.rst @@ -445,6 +445,63 @@ for specific objects being present and ensure their values are correct and still anyalsis3 = session.query(CombinedAnalysis).get({"pk1": 3}) assert anyalsis3 == expected_anyalsis3 +Abstract Classes +^^^^^^^^^^^^^^^^ + +It is important to note that mock_alchemy uses Python object attributes instead of SQLAlchemy table attributes. Therefore, please make sure when testing that the column values +are added as object attributes. Refer to the `SQLAlchemy documentation `__ for the details on SQLAlchemy initialization, but for this library, +attributes from Python objects are currently used to get column values. In the below example, this is why the primary key must be created inside the class Concrete's __init__. Normally, this is not a +concern since if you do not write a constructor for your Python object, SQLAlchemy intiailzes the attributes for you. However, if you do write a constructor make sure that the class itself has those +attributes set. + +.. code-block:: python + + import datetime + import mock + + from sqlalchemy import Integer + from sqlalchemy import Column + from sqlalchemy.ext.declarative import declarative_base + + from mock_alchemy.mocking import UnifiedAlchemyMagicMock + + Base = declarative_base() + + + class BaseModel(Base): + """Abstract data model to test.""" + + __abstract__ = True + created = Column(Integer, nullable=False, default=3) + createdby = Column(Integer, nullable=False, default={}) + updated = Column(Integer, nullable=False, default=1) + updatedby = Column(Integer, nullable=False, default={}) + disabled = Column(Integer, nullable=True) + + class Concrete(BaseModel): + """A testing SQLAlchemy object.""" + + __tablename__ = "concrete" + id = Column(Integer, primary_key=True) + + def __init__(self, **kwargs: Any) -> None: + """Creates a Concrete object.""" + self.id = kwargs.pop("id") + super(Concrete, self).__init__(**kwargs) + + def __eq__(self, other: Concrete) -> bool: + """Equality override.""" + return self.id == other.id + + objs = Concrete(id=1) + session = UnifiedAlchemyMagicMock( + data=[ + ([mock.call.query(Concrete)], [objs]), + ] + ) + ret = session.query(Concrete).get(1) + assert ret == objs + Contribute ----------- diff --git a/poetry.lock b/poetry.lock index cd43ddf..2aa12a7 100644 --- a/poetry.lock +++ b/poetry.lock @@ -959,16 +959,16 @@ python-versions = "*" [[package]] name = "urllib3" -version = "1.26.3" +version = "1.26.4" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" [package.extras] -brotli = ["brotlipy (>=0.6.0)"] secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +brotli = ["brotlipy (>=0.6.0)"] [[package]] name = "virtualenv" @@ -1237,39 +1237,20 @@ markupsafe = [ {file = "MarkupSafe-1.1.1-cp35-cp35m-win32.whl", hash = "sha256:6dd73240d2af64df90aa7c4e7481e23825ea70af4b4922f8ede5b9e35f78a3b1"}, {file = "MarkupSafe-1.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:9add70b36c5666a2ed02b43b335fe19002ee5235efd4b8a89bfcf9005bebac0d"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_6_intel.whl", hash = "sha256:24982cc2533820871eba85ba648cd53d8623687ff11cbb805be4ff7b4c971aff"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d53bc011414228441014aa71dbec320c66468c1030aae3a6e29778a3382d96e5"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:00bc623926325b26bb9605ae9eae8a215691f33cae5df11ca5424f06f2d1f473"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:717ba8fe3ae9cc0006d7c451f0bb265ee07739daf76355d06366154ee68d221e"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:3b8a6499709d29c2e2399569d96719a1b21dcd94410a586a18526b143ec8470f"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:84dee80c15f1b560d55bcfe6d47b27d070b4681c699c572af2e3c7cc90a3b8e0"}, - {file = "MarkupSafe-1.1.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:b1dba4527182c95a0db8b6060cc98ac49b9e2f5e64320e2b56e47cb2831978c7"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-win32.whl", hash = "sha256:535f6fc4d397c1563d08b88e485c3496cf5784e927af890fb3c3aac7f933ec66"}, {file = "MarkupSafe-1.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b1282f8c00509d99fef04d8ba936b156d419be841854fe901d8ae224c59f0be5"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl", hash = "sha256:8defac2f2ccd6805ebf65f5eeb132adcf2ab57aa11fdf4c0dd5169a004710e7d"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:bf5aa3cbcfdf57fa2ee9cd1822c862ef23037f5c832ad09cfea57fa846dec193"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:46c99d2de99945ec5cb54f23c8cd5689f6d7177305ebff350a58ce5f8de1669e"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:ba59edeaa2fc6114428f1637ffff42da1e311e29382d81b339c1817d37ec93c6"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:6fffc775d90dcc9aed1b89219549b329a9250d918fd0b8fa8d93d154918422e1"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:a6a744282b7718a2a62d2ed9d993cad6f5f585605ad352c11de459f4108df0a1"}, - {file = "MarkupSafe-1.1.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:195d7d2c4fbb0ee8139a6cf67194f3973a6b3042d742ebe0a9ed36d8b6f0c07f"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win32.whl", hash = "sha256:b00c1de48212e4cc9603895652c5c410df699856a2853135b3967591e4beebc2"}, {file = "MarkupSafe-1.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:9bf40443012702a1d2070043cb6291650a0841ece432556f784f004937f0f32c"}, {file = "MarkupSafe-1.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6788b695d50a51edb699cb55e35487e430fa21f1ed838122d722e0ff0ac5ba15"}, {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:cdb132fc825c38e1aeec2c8aa9338310d29d337bebbd7baa06889d09a60a1fa2"}, {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:13d3144e1e340870b25e7b10b98d779608c02016d5184cfb9927a9f10c689f42"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:acf08ac40292838b3cbbb06cfe9b2cb9ec78fce8baca31ddb87aaac2e2dc3bc2"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:d9be0ba6c527163cbed5e0857c451fcd092ce83947944d6c14bc95441203f032"}, - {file = "MarkupSafe-1.1.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:caabedc8323f1e93231b52fc32bdcde6db817623d33e100708d9a68e1f53b26b"}, {file = "MarkupSafe-1.1.1-cp38-cp38-win32.whl", hash = "sha256:596510de112c685489095da617b5bcbbac7dd6384aeebeda4df6025d0256a81b"}, {file = "MarkupSafe-1.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e8313f01ba26fbbe36c7be1966a7b7424942f670f38e666995b88d012765b9be"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d73a845f227b0bfe8a7455ee623525ee656a9e2e749e4742706d80a6065d5e2c"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:98bae9582248d6cf62321dcb52aaf5d9adf0bad3b40582925ef7c7f0ed85fceb"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:2beec1e0de6924ea551859edb9e7679da6e4870d32cb766240ce17e0a0ba2014"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:7fed13866cf14bba33e7176717346713881f56d9d2bcebab207f7a036f41b850"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:6f1e273a344928347c1290119b493a1f0303c52f5a5eae5f16d74f48c15d4a85"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:feb7b34d6325451ef96bc0e36e1a6c0c1c64bc1fbec4b854f4529e51887b1621"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-win32.whl", hash = "sha256:22c178a091fc6630d0d045bdb5992d2dfe14e3259760e713c490da5323866c39"}, - {file = "MarkupSafe-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7d644ddb4dbd407d31ffb699f1d140bc35478da613b441c582aeb7c43838dd8"}, {file = "MarkupSafe-1.1.1.tar.gz", hash = "sha256:29872e92839765e546828bb7754a68c418d927cd064fd4708fab9fe9c8bb116b"}, ] mccabe = [ @@ -1632,8 +1613,8 @@ typing-extensions = [ {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, ] urllib3 = [ - {file = "urllib3-1.26.3-py2.py3-none-any.whl", hash = "sha256:1b465e494e3e0d8939b50680403e3aedaa2bc434b7d5af64dfd3c958d7f5ae80"}, - {file = "urllib3-1.26.3.tar.gz", hash = "sha256:de3eedaad74a2683334e282005cd8d7f22f4d55fa690a2a1020a416cb0a47e73"}, + {file = "urllib3-1.26.4-py2.py3-none-any.whl", hash = "sha256:2f4da4594db7e1e110a944bb1b551fdf4e6c136ad42e4234131391e21eb5b0df"}, + {file = "urllib3-1.26.4.tar.gz", hash = "sha256:e7b021f7241115872f92f43c6508082facffbd1c048e3c6e2bb9c2a157e28937"}, ] virtualenv = [ {file = "virtualenv-20.4.2-py2.py3-none-any.whl", hash = "sha256:2be72df684b74df0ea47679a7df93fd0e04e72520022c57b479d8f881485dbe3"}, diff --git a/pyproject.toml b/pyproject.toml index 5fd452a..a755657 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "mock-alchemy" -version = "0.2.1" +version = "0.2.2" description = "SQLAlchemy mock helpers." license = "MIT" homepage = "https://github.com/rajivsarvepalli/mock-alchemy" diff --git a/src/mock_alchemy/utils.py b/src/mock_alchemy/utils.py index 91d123a..9bc4a2d 100644 --- a/src/mock_alchemy/utils.py +++ b/src/mock_alchemy/utils.py @@ -222,18 +222,19 @@ def build_identity_map(items: Sequence[Any]) -> Dict: return idmap -def get_item_attr(idmap: Dict, access: Union[Dict, Tuple, int]) -> Any: +def get_item_attr(idmap: Dict, access: Union[Dict, Tuple, Any]) -> Any: """Access dictionary in different methods. Utility for accessing dict by different key types (for get). Args: idmap: A dictionary of identity map of SQLAlchemy objects. - access: The access pattern which should either be int, dictionary, or - a tuple. If it is dictionary it should map to the names of the primary keys - of the SQLAlchemy objects. If it is a tuple, it should be a set of keys to - search for. If it is an int, then the objects in question must have only one - primary key. + access: The access pattern which should either be basic data type, dictionary, + or a tuple. If it is dictionary it should map to the names of the primary + keys of the SQLAlchemy objects. If it is a tuple, it should be a set of + keys to search for. If it is not a dict or a tuple, then the objects in + question must have only one primary key of the type passed + (such as a string, integer, etc.). Returns: An SQlAlchemy object that was requested. @@ -253,7 +254,7 @@ def get_item_attr(idmap: Dict, access: Union[Dict, Tuple, int]) -> Any: for names in sorted(access): keys.append(access[names]) return idmap.get(tuple(keys)) - elif isinstance(access, int): - return idmap.get((access,)) - else: + elif isinstance(access, tuple): return idmap.get(access) + else: + return idmap.get((access,)) diff --git a/tests/test_mocking.py b/tests/test_mocking.py index fde95ff..be8661f 100644 --- a/tests/test_mocking.py +++ b/tests/test_mocking.py @@ -1,6 +1,7 @@ """Testing the module for mocking in mock-alchemy.""" from __future__ import annotations +from typing import Any from unittest import mock import pytest @@ -85,7 +86,7 @@ class SomeClass(Base): name = Column(String(50)) def __repr__(self) -> str: - """Get strin of object.""" + """Get string of object.""" return str(self.pk1) def __eq__(self, other: SomeClass) -> bool: @@ -167,7 +168,7 @@ def __eq__(self, other: Model) -> bool: return NotImplemented def __repr__(self) -> str: - """Get strin of object.""" + """Get string of object.""" return str(self.pk1) s = UnifiedAlchemyMagicMock() @@ -213,7 +214,7 @@ class Model2(Base): name = Column(String) def __repr__(self) -> str: - """Get strin of object.""" + """Get string of object.""" return str(self.pk1) s = UnifiedAlchemyMagicMock( @@ -256,7 +257,7 @@ class Data(Base): name = Column(String) def __repr__(self) -> str: - """Get strin of object.""" + """Get string of object.""" return str(self.pk1) + self.name s = UnifiedAlchemyMagicMock( @@ -307,3 +308,81 @@ def __repr__(self) -> str: assert expected_data == ["8test9", "9test10", "10test11", "1test12", "11test13"] ret = s.query(Data).filter(Data.data_p1 < 13).all() assert ret == [] + + +def test_abstract_classes() -> None: + """Tests mock for SQLAlchemy with inheritance and abstract classes.""" + + class BaseModel(Base): + """Abstract data model to test.""" + + __abstract__ = True + created = Column(Integer, nullable=False, default=3) + createdby = Column(Integer, nullable=False, default={}) + updated = Column(Integer, nullable=False, default=1) + updatedby = Column(Integer, nullable=False, default={}) + disabled = Column(Integer, nullable=True) + + class Concrete(BaseModel): + """A testing SQLAlchemy object.""" + + __tablename__ = "concrete" + id = Column(Integer, primary_key=True) + + def __init__(self, **kwargs: Any) -> None: + """Creates a Concrete object.""" + self.id = kwargs.pop("id") + super(Concrete, self).__init__(**kwargs) + + def __eq__(self, other: Concrete) -> bool: + """Equality override.""" + return self.id == other.id + + objs = Concrete(id=1) + session = UnifiedAlchemyMagicMock( + data=[ + ([mock.call.query(Concrete)], [objs]), + ] + ) + ret = session.query(Concrete).get(1) + assert ret == objs + + +def test_get_tuple() -> None: + """Tests mock for SQLAlchemy with getting by a tuple.""" + + class SomeObject(Base): + """SQLAlchemy object for testing.""" + + __tablename__ = "some_table1" + pk1 = Column(String, primary_key=True) + name = Column(String(50)) + + def __repr__(self) -> str: + """Get string of object.""" + return str(self.pk1) + + mock_session = UnifiedAlchemyMagicMock() + mock_session.add(SomeObject(pk1="123", name="test")) + user = mock_session.query(SomeObject).get(("123",)) + assert user is not None + + +def test_get_singular() -> None: + """Tests mock for SQLAlchemy with getting by a singular value.""" + + class SomeData(Base): + """SQLAlchemy object for testing.""" + + __tablename__ = "some_table2" + pk1 = Column(String, primary_key=True) + name = Column(String(50)) + + def __repr__(self) -> str: + """Get string of object.""" + return str(self.pk1) + + mock_session = UnifiedAlchemyMagicMock() + mock_session.add(SomeData(pk1="123", name="test")) + user = mock_session.query(SomeData).get("123") + assert user is not None