├── .github └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .readthedocs.yml ├── LICENSE ├── README.md ├── docs ├── Makefile ├── make.bat └── source │ ├── conf.py │ ├── index.rst │ ├── objectfactory.rst │ └── quickstart.rst ├── environment.yml ├── examples ├── custom_schema.py ├── product_orders.py └── shapes.py ├── objectfactory ├── __init__.py ├── base.py ├── factory.py ├── field.py ├── nested.py └── serializable.py ├── setup.py └── test ├── __init__.py ├── test_boolean.py ├── test_custom_schema.py ├── test_datetime.py ├── test_enum.py ├── test_factory.py ├── test_fields.py ├── test_float.py ├── test_integer.py ├── test_list.py ├── test_nested.py ├── test_serializable.py ├── test_string.py └── testmodule ├── __init__.py └── testclasses.py /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | 10 | test: 11 | uses: ./.github/workflows/test.yml 12 | secrets: inherit 13 | 14 | build: 15 | name: build objectfactory dist 16 | needs: test 17 | 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - name: check out git repository 22 | uses: actions/checkout@v4 23 | 24 | - name: setup python 25 | uses: actions/setup-python@v5 26 | with: 27 | python-version: '3.x' 28 | 29 | - name: install python dependencies 30 | run: pip install build 31 | 32 | - name: build binary wheel and source tarball 33 | run: python3 -m build 34 | 35 | - name: store the dist package 36 | uses: actions/upload-artifact@v4 37 | with: 38 | name: distribution 39 | path: dist/ 40 | 41 | 42 | publish: 43 | name: publish objectfactory to pypi 44 | needs: build 45 | 46 | runs-on: ubuntu-latest 47 | environment: 48 | name: release 49 | url: https://pypi.org/p/objectfactory 50 | permissions: 51 | id-token: write 52 | 53 | steps: 54 | - name: get dist package 55 | uses: actions/download-artifact@v4 56 | with: 57 | name: distribution 58 | path: dist/ 59 | 60 | - name: publish package to pypi 61 | uses: pypa/gh-action-pypi-publish@release/v1 62 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_call: 7 | 8 | jobs: 9 | 10 | test: 11 | name: run objectfactory unit tests 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: check out git repository 16 | uses: actions/checkout@v4 17 | 18 | - name: setup conda env 19 | uses: conda-incubator/setup-miniconda@v2 20 | with: 21 | activate-environment: py-object-factory 22 | environment-file: environment.yml 23 | auto-activate-base: false 24 | 25 | - name: pytest 26 | shell: bash -l {0} 27 | run: pytest -v --cov --cov-report term --cov-report xml 28 | 29 | - name: upload coverage report 30 | env: 31 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 32 | uses: codecov/codecov-action@v2 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Custom 2 | .idea/ 3 | *.DS_Store 4 | *.pytest_cache/ 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | env/ 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | nosetests.xml 49 | coverage.xml 50 | *,cover 51 | .hypothesis/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # IPython Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | 93 | # Rope project settings 94 | .ropeproject -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yml 2 | 3 | version: 2 4 | build: 5 | os: ubuntu-22.04 6 | tools: 7 | python: "miniconda3-4.7" 8 | sphinx: 9 | configuration: docs/source/conf.py 10 | formats: 11 | - pdf 12 | conda: 13 | environment: environment.yml 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2023 Devin A. Conley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # py-object-factory 2 | 3 | [![Build Status](https://github.com/devinaconley/py-object-factory/actions/workflows/test.yml/badge.svg)](https://github.com/devinaconley/py-object-factory/actions/workflows/test.yml) 4 | [![codecov](https://codecov.io/gh/devinaconley/py-object-factory/branch/develop/graph/badge.svg)](https://codecov.io/gh/devinaconley/py-object-factory) 5 | [![Documentation Status](https://readthedocs.org/projects/objectfactory/badge/?version=latest)](https://objectfactory.readthedocs.io/en/latest/?badge=latest) 6 | 7 | 8 | **objectfactory** is a python package to easily implement the factory design pattern for object creation, serialization, and polymorphism 9 | - designed to support polymorphism 10 | - integrates seamlessly with [marshmallow](https://github.com/marshmallow-code/marshmallow) 11 | and other serialization frameworks 12 | - schema inherent in class definition 13 | - load any object with a generic interface 14 | - serialize objects to JSON 15 | 16 | ## Example 17 | Simple **shapes** example: 18 | ```python 19 | import objectfactory 20 | 21 | @objectfactory.register 22 | class Square(objectfactory.Serializable): 23 | side = objectfactory.Field() 24 | 25 | def get_area(self): 26 | return self.side * self.side 27 | 28 | @objectfactory.register 29 | class Triangle(objectfactory.Serializable): 30 | base = objectfactory.Field() 31 | height = objectfactory.Field() 32 | 33 | def get_area(self): 34 | return 0.5 * self.base * self.height 35 | 36 | serialized_data = [ 37 | {"_type": "Square", "side": 2.0}, 38 | {"_type": "Triangle", "base": 1.75, "height": 2.50}, 39 | {"_type": "Square", "side": 1.5}, 40 | ] 41 | 42 | for data in serialized_data: 43 | shape = objectfactory.create(data) 44 | print('class type: {}, shape area: {}'.format(type(shape), shape.get_area())) 45 | ``` 46 | 47 | Output: 48 | ``` 49 | class type: , shape area: 4.0 50 | class type: , shape area: 2.1875 51 | class type: , shape area: 2.25 52 | ``` 53 | 54 | ### More examples 55 | See more advanced examples [here](https://github.com/devinaconley/py-object-factory/tree/develop/examples) 56 | 57 | ## Install 58 | Use [pip](https://pip.pypa.io/en/stable/installing/) for installation 59 | ``` 60 | pip install objectfactory 61 | ``` 62 | 63 | ## Documentation 64 | Read the full documentation at [objectfactory.readthedocs.io](https://objectfactory.readthedocs.io/) 65 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # objectfactory configuration file for sphinx documentation builder 2 | 3 | import os 4 | import sys 5 | 6 | sys.path.insert( 0, os.path.abspath( '../../objectfactory/' ) ) 7 | 8 | # project info 9 | project = 'objectfactory' 10 | copyright = '2018-2025, Devin A. Conley' 11 | author = 'Devin A. Conley' 12 | release = '0.2.0' 13 | 14 | # config 15 | extensions = [ 16 | 'sphinx.ext.autodoc', 17 | 'sphinx_rtd_theme', 18 | 'm2r2' 19 | ] 20 | autoclass_content = 'both' 21 | html_theme = 'sphinx_rtd_theme' 22 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | objectfactory 2 | =============================================== 3 | 4 | .. mdinclude:: ../../README.md 5 | :start-line: 2 6 | :end-line: 15 7 | 8 | .. toctree:: 9 | :maxdepth: 2 10 | 11 | quickstart 12 | objectfactory 13 | 14 | * :ref:`genindex` 15 | -------------------------------------------------------------------------------- /docs/source/objectfactory.rst: -------------------------------------------------------------------------------- 1 | objectfactory 2 | ===================== 3 | 4 | .. automodule:: objectfactory 5 | :members: 6 | :undoc-members: 7 | :show-inheritance: 8 | 9 | objectfactory.base 10 | ------------------------- 11 | 12 | .. automodule:: objectfactory.base 13 | :members: 14 | :undoc-members: 15 | :show-inheritance: 16 | 17 | objectfactory.factory 18 | ---------------------------- 19 | 20 | .. automodule:: objectfactory.factory 21 | :members: 22 | :undoc-members: 23 | :show-inheritance: 24 | 25 | objectfactory.field 26 | -------------------------- 27 | 28 | .. automodule:: objectfactory.field 29 | :members: 30 | :undoc-members: 31 | :show-inheritance: 32 | 33 | objectfactory.serializable 34 | --------------------------------- 35 | 36 | .. automodule:: objectfactory.serializable 37 | :members: 38 | :undoc-members: 39 | :show-inheritance: 40 | -------------------------------------------------------------------------------- /docs/source/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quickstart 2 | ============= 3 | 4 | .. mdinclude:: ../../README.md 5 | :start-line: 2 6 | -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: py-object-factory 2 | channels: 3 | - defaults 4 | dependencies: 5 | - ca-certificates 6 | - certifi 7 | - libedit 8 | - libffi 9 | - ncurses 10 | - openssl 11 | - pip 12 | - python>=3.6,<3.14 13 | - readline 14 | - setuptools 15 | - sqlite 16 | - tk 17 | - wheel 18 | - xz 19 | - zlib 20 | - pip: 21 | - atomicwrites 22 | - attrs 23 | - chardet 24 | - codecov~=2.1.13 25 | - coverage~=7.6.12 26 | - idna 27 | - marshmallow>=3,<4 28 | - more-itertools 29 | - pluggy 30 | - py 31 | - pytest~=7.4.0 32 | - pytest-cov~=2.10.0 33 | - requests 34 | - six 35 | - sphinx~=7.1.2 36 | - sphinx-rtd-theme 37 | - m2r2 38 | - urllib3 39 | - -e . 40 | prefix: /Users/devin/miniconda3/envs/py-object-factory 41 | 42 | -------------------------------------------------------------------------------- /examples/custom_schema.py: -------------------------------------------------------------------------------- 1 | """ 2 | custom marshmallow schema example 3 | 4 | use the objectfactory library with a custom marshmallow schema 5 | to validate and load contact information 6 | """ 7 | import objectfactory 8 | import marshmallow 9 | 10 | 11 | def main(): 12 | contacts = [ 13 | { 14 | '_type': 'Contact', 15 | 'first_name': 'john', 16 | 'last_name': 'smith', 17 | 'phone_number': '123-456-7890', 18 | 'email': 'johnsmith@gmail.com' 19 | }, 20 | { 21 | '_type': 'Contact', 22 | 'first_name': 'john', 23 | 'last_name': 'smith', 24 | 'phone_number': '123-456-78', 25 | 'email': 'johnsmith@gmail.com' 26 | }, 27 | { 28 | '_type': 'Contact', 29 | 'first_name': 'john', 30 | 'last_name': 'smith', 31 | 'phone_number': '123-456-7890', 32 | 'email': 'nonsense' 33 | } 34 | ] 35 | 36 | # load and validate each contact 37 | for c in contacts: 38 | try: 39 | contact = objectfactory.create(c, object_type=Contact) 40 | print( 41 | 'Loaded contact for: {} {}, number: {}, email: {}'.format( 42 | contact.first_name, 43 | contact.last_name, 44 | contact.phone_number, 45 | contact.email 46 | ) 47 | ) 48 | except marshmallow.ValidationError as e: 49 | print('Validation error: {}'.format(e)) 50 | 51 | 52 | class PhoneNumber(marshmallow.fields.Field): 53 | """Custom marshmallow field to validate phone number""" 54 | 55 | def _deserialize(self, value, *args, **kwargs): 56 | try: 57 | x = value.split('-') 58 | assert len(x) == 3 59 | assert len(x[0]) == 3 60 | assert len(x[1]) == 3 61 | assert len(x[2]) == 4 62 | return str(value) 63 | 64 | except AssertionError as e: 65 | raise marshmallow.ValidationError('Invalid phone number') 66 | 67 | def _serialize(self, value, *args, **kwargs): 68 | return str(value) 69 | 70 | 71 | class ContactSchema(marshmallow.Schema): 72 | """Custom marshmallow schema for contact info""" 73 | 74 | first_name = marshmallow.fields.Str() 75 | last_name = marshmallow.fields.Str() 76 | email = marshmallow.fields.Email() 77 | phone_number = PhoneNumber() 78 | 79 | 80 | @objectfactory.register 81 | class Contact(objectfactory.Serializable, schema=ContactSchema): 82 | """ 83 | product order from a dollar store vendor 84 | """ 85 | first_name = objectfactory.String() 86 | last_name = objectfactory.String() 87 | email = objectfactory.String() 88 | phone_number = objectfactory.String() 89 | 90 | 91 | if __name__ == '__main__': 92 | main() 93 | -------------------------------------------------------------------------------- /examples/product_orders.py: -------------------------------------------------------------------------------- 1 | """ 2 | product order example 3 | 4 | use the objectfactory library to handle product orders across multiple vendors. validate incoming 5 | order data, load as python objects, and calculate price and estimated delivery 6 | """ 7 | import objectfactory 8 | 9 | 10 | def main(): 11 | raw_orders = [ 12 | { 13 | '_type': 'DollarStoreProduct', 14 | 'product_id': 'greeting_card', 15 | 'quantity': 3 16 | }, 17 | { 18 | '_type': 'EcommerceGiantProduct', 19 | 'product_id': 'tv' 20 | }, 21 | { 22 | '_type': 'EcommerceGiantProduct', 23 | 'product_id': 'virtual_assistant', 24 | 'quantity': 2 25 | } 26 | ] 27 | 28 | # deserialize raw product order 29 | products = [objectfactory.create(order, object_type=Product) for order in raw_orders] 30 | 31 | # calculate overall price 32 | price = sum([prod.get_price() * prod.quantity for prod in products]) 33 | print('Overall order price: ${}'.format(price)) 34 | 35 | # estimate delivery 36 | days = max([prod.get_delivery_time() for prod in products]) 37 | print('Estimated delivery time is: {} days'.format(days)) 38 | 39 | # validate stocking 40 | in_stock = all([prod.quantity < prod.get_quantity_in_stock() for prod in products]) 41 | print('Products are {}stocked'.format('' if in_stock else 'not ')) 42 | 43 | 44 | class Product(objectfactory.Serializable): 45 | """ 46 | base abstract class for our products 47 | """ 48 | product_id = objectfactory.String() # all products will have an id 49 | quantity = objectfactory.Integer(default=1) # all products will have a quantity 50 | 51 | def get_price(self) -> float: 52 | """ 53 | abstract method to calculate price and return 54 | 55 | :return: float 56 | """ 57 | raise NotImplementedError('get_price method is required') 58 | 59 | def get_delivery_time(self) -> int: 60 | """ 61 | abstract method to get required delivery time 62 | 63 | :return: 64 | """ 65 | raise NotImplementedError('get_delivery_time method is required') 66 | 67 | def get_quantity_in_stock(self) -> int: 68 | """ 69 | abstract method to get quantity in stock 70 | 71 | :return: 72 | """ 73 | raise NotImplementedError('get_quantity_in_stock method is required') 74 | 75 | 76 | @objectfactory.register 77 | class DollarStoreProduct(Product): 78 | """ 79 | product order from a dollar store vendor 80 | """ 81 | 82 | def get_price(self) -> float: 83 | """ 84 | everything is a dollar!!!! 85 | 86 | :return: 87 | """ 88 | return 1.00 89 | 90 | def get_delivery_time(self) -> int: 91 | """ 92 | everything takes about a week to ship 93 | 94 | :return: 95 | """ 96 | return 7 97 | 98 | def get_quantity_in_stock(self) -> int: 99 | """ 100 | mock connection to this vendor's supply data 101 | 102 | :return: 103 | """ 104 | return { 105 | 'greeting_card': 300, 106 | 'candle': 15, 107 | 'glass_vase': 10 108 | }.get(self.product_id, 0) 109 | 110 | 111 | @objectfactory.register 112 | class EcommerceGiantProduct(Product): 113 | """ 114 | product order from an e-commerce giant 115 | """ 116 | 117 | def get_price(self) -> float: 118 | """ 119 | mock connection to this vendor's pricing data 120 | 121 | :return: 122 | """ 123 | return { 124 | 'really_inspiring_book': 15, 125 | 'tv': 450, 126 | 'digital_clock': 10, 127 | 'virtual_assistant': 50 128 | }.get(self.product_id, None) 129 | 130 | def get_delivery_time(self) -> int: 131 | """ 132 | guaranteed 2-day delivery 133 | 134 | :return: 135 | """ 136 | return 2 137 | 138 | def get_quantity_in_stock(self) -> int: 139 | """ 140 | infinite supplies 141 | 142 | :return: 143 | """ 144 | return 1000000 145 | 146 | 147 | if __name__ == '__main__': 148 | main() 149 | -------------------------------------------------------------------------------- /examples/shapes.py: -------------------------------------------------------------------------------- 1 | """ 2 | shapes example 3 | 4 | use the objectfactory library to load shapes of various object types and 5 | calculate their area accordingly 6 | """ 7 | import objectfactory 8 | 9 | 10 | def main(): 11 | serialized_data = [ 12 | {"_type": "Square", "side": 2.0}, 13 | {"_type": "Triangle", "base": 1.75, "height": 2.50}, 14 | {"_type": "Square", "side": 1.5}, 15 | ] 16 | 17 | # load each shape, printing object type and area 18 | for data in serialized_data: 19 | shape = objectfactory.create(data) 20 | print('class type: {}, shape area: {}'.format(type(shape), shape.get_area())) 21 | 22 | 23 | @objectfactory.register 24 | class Square(objectfactory.Serializable): 25 | """ 26 | serializable square class 27 | """ 28 | side = objectfactory.Float() 29 | 30 | def get_area(self) -> float: 31 | """ 32 | calculate area of square 33 | 34 | :return: side^2 35 | """ 36 | return self.side * self.side 37 | 38 | 39 | @objectfactory.register 40 | class Triangle(objectfactory.Serializable): 41 | """ 42 | serializable triangle class 43 | """ 44 | base = objectfactory.Float() 45 | height = objectfactory.Float() 46 | 47 | def get_area(self) -> float: 48 | """ 49 | calculate area of triangle 50 | 51 | :return: 0.5 * base * height 52 | """ 53 | return 0.5 * self.base * self.height 54 | 55 | 56 | if __name__ == '__main__': 57 | main() 58 | -------------------------------------------------------------------------------- /objectfactory/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | objectfactory is a python package to easily implement the factory design pattern 3 | for object creation, serialization, and polymorphism 4 | """ 5 | 6 | # do imports 7 | from .serializable import Serializable 8 | from .factory import Factory, register, create 9 | from .field import Field, Nested, List, Integer, String, Boolean, Float, DateTime, Enum 10 | 11 | __version__ = '0.1.0' 12 | -------------------------------------------------------------------------------- /objectfactory/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | base module 3 | 4 | implements abstract base classes for objectfactory 5 | """ 6 | 7 | from abc import ABC, abstractmethod 8 | 9 | 10 | class FieldABC(ABC): 11 | """ 12 | abstract base class for serializable field 13 | """ 14 | 15 | def __init__(self, default=None, key=None, required=False, allow_none=True): 16 | """ 17 | :param default: default value for field if unset 18 | :param key: dictionary key to use for field serialization 19 | :param required: whether this field is required to deserialize an object 20 | :param allow_none: whether null should be considered a valid value 21 | """ 22 | self._key = key 23 | self._attr_key = None # note: this will be set from parent metaclass __new__ 24 | self._default = default 25 | self._required = required 26 | self._allow_none = allow_none 27 | 28 | @abstractmethod 29 | def __get__(self, instance, owner): 30 | pass 31 | 32 | @abstractmethod 33 | def __set__(self, instance, value): 34 | pass 35 | 36 | @abstractmethod 37 | def marshmallow(self): 38 | """ 39 | create generic marshmallow field to do actual serialization 40 | 41 | :return: associated marshmallow field 42 | """ 43 | pass 44 | 45 | 46 | class SerializableABC(ABC): 47 | """ 48 | abstract base class for serializable object 49 | """ 50 | 51 | @abstractmethod 52 | def serialize(self, include_type: bool = True, use_full_type: bool = True) -> dict: 53 | """ 54 | serialize model to dictionary 55 | 56 | :param include_type: if true, type information will be included in body 57 | :param use_full_type: if true, the fully qualified path with be specified in body 58 | :return: serialized object as dict 59 | """ 60 | pass 61 | 62 | @abstractmethod 63 | def deserialize(self, body: dict): 64 | """ 65 | deserialize model from dictionary 66 | 67 | :param body: serialized data to load into object 68 | """ 69 | pass 70 | -------------------------------------------------------------------------------- /objectfactory/factory.py: -------------------------------------------------------------------------------- 1 | """ 2 | factory module 3 | 4 | implements serializable object factory 5 | """ 6 | # lib 7 | from typing import Type, TypeVar 8 | 9 | # src 10 | from .serializable import Serializable 11 | 12 | # type var for hinting from generic function 13 | T = TypeVar('T', bound=Serializable) 14 | 15 | 16 | class Factory(object): 17 | """ 18 | factory class for registering and creating serializable objects 19 | """ 20 | 21 | def __init__(self, name): 22 | self.name = name 23 | self.registry = {} 24 | 25 | def register(self, serializable: Serializable): 26 | """ 27 | decorator to register class with factory 28 | 29 | :param serializable: serializable object class 30 | :return: registered class 31 | """ 32 | self.registry[serializable.__module__ + '.' + serializable.__name__] = serializable 33 | self.registry[serializable.__name__] = serializable 34 | return serializable 35 | 36 | def create(self, body: dict, object_type: Type[T] = Serializable) -> T: 37 | """ 38 | create object from dictionary 39 | 40 | :param body: serialized object data 41 | :param object_type: (optional) specified object type 42 | :raises TypeError: if the object is not an instance of the specified type 43 | :return: deserialized object of specified type 44 | """ 45 | obj = None 46 | try: 47 | obj = self.registry[body['_type']]() 48 | except KeyError: 49 | pass 50 | if obj is None: 51 | try: 52 | obj = self.registry[body['_type'].split('.')[-1]]() 53 | except KeyError: 54 | pass 55 | if obj is None: 56 | raise ValueError( 57 | 'Object type {} not found in factory registry'.format(body['_type']) 58 | ) 59 | 60 | if not isinstance(obj, object_type): 61 | raise TypeError( 62 | 'Object type {} is not a {}'.format( 63 | type(obj).__name__, 64 | object_type.__name__ 65 | ) 66 | ) 67 | 68 | obj.deserialize(body) 69 | return obj 70 | 71 | 72 | # global registry 73 | _global_factory = Factory('global') 74 | 75 | 76 | def create(body: dict, object_type: Type[T] = Serializable) -> T: 77 | """ 78 | create object from dictionary with the global factory 79 | 80 | :param body: serialized object data 81 | :param object_type: (optional) specified object type 82 | :raises TypeError: if the object is not an instance of the specified type 83 | :return: deserialized object of specified type 84 | """ 85 | return _global_factory.create(body, object_type=object_type) 86 | 87 | 88 | def register(serializable: Serializable): 89 | """ 90 | decorator to register class with the global factory 91 | 92 | :param serializable: serializable object class 93 | :return: registered class 94 | """ 95 | return _global_factory.register(serializable) 96 | -------------------------------------------------------------------------------- /objectfactory/field.py: -------------------------------------------------------------------------------- 1 | """ 2 | field module 3 | 4 | implements serializable fields 5 | """ 6 | 7 | # lib 8 | from copy import deepcopy 9 | import marshmallow 10 | 11 | # src 12 | from .base import FieldABC, SerializableABC 13 | from .factory import create 14 | from .nested import NestedFactoryField 15 | 16 | 17 | class Field(FieldABC): 18 | """ 19 | base class for serializable field 20 | 21 | this is a class level descriptor for abstracting access to fields of 22 | serializable objects 23 | """ 24 | 25 | def __get__(self, instance, owner): 26 | try: 27 | return getattr(instance, self._attr_key) 28 | except AttributeError: 29 | # lazily create copy of default 30 | setattr(instance, self._attr_key, deepcopy(self._default)) 31 | return getattr(instance, self._attr_key) 32 | 33 | def __set__(self, instance, value): 34 | setattr(instance, self._attr_key, value) 35 | 36 | def marshmallow(self): 37 | return marshmallow.fields.Raw( 38 | data_key=self._key, 39 | required=self._required, 40 | allow_none=self._allow_none 41 | ) 42 | 43 | 44 | class Integer(Field): 45 | """ 46 | serializable field for integer data 47 | """ 48 | 49 | def marshmallow(self): 50 | return marshmallow.fields.Integer( 51 | data_key=self._key, 52 | required=self._required, 53 | allow_none=self._allow_none 54 | ) 55 | 56 | 57 | class String(Field): 58 | """ 59 | serializable field for string data 60 | """ 61 | 62 | def marshmallow(self): 63 | return marshmallow.fields.String( 64 | data_key=self._key, 65 | required=self._required, 66 | allow_none=self._allow_none 67 | ) 68 | 69 | 70 | class Boolean(Field): 71 | """ 72 | serializable field for boolean data 73 | """ 74 | 75 | def marshmallow(self): 76 | return marshmallow.fields.Boolean( 77 | data_key=self._key, 78 | required=self._required, 79 | allow_none=self._allow_none 80 | ) 81 | 82 | 83 | class Float(Field): 84 | """ 85 | serializable field for float data 86 | """ 87 | 88 | def marshmallow(self): 89 | return marshmallow.fields.Float( 90 | data_key=self._key, 91 | required=self._required, 92 | allow_none=self._allow_none 93 | ) 94 | 95 | 96 | class DateTime(Field): 97 | """ 98 | serializable field for datetime data 99 | """ 100 | 101 | def __init__( 102 | self, 103 | default=None, 104 | key=None, 105 | date_format=None, 106 | required=False, 107 | allow_none=True 108 | ): 109 | """ 110 | :param default: default value for field if unset 111 | :param key: dictionary key to use for field serialization 112 | :param date_format: date format to use (defaults to iso) 113 | :param required: whether this field is required to deserialize an object 114 | :param allow_none: whether null should be considered a valid value 115 | """ 116 | super().__init__(default=default, key=key, required=required, allow_none=allow_none) 117 | self._date_format = date_format 118 | 119 | def marshmallow(self): 120 | return marshmallow.fields.DateTime( 121 | data_key=self._key, 122 | required=self._required, 123 | allow_none=self._allow_none, 124 | format=self._date_format 125 | ) 126 | 127 | 128 | class Enum(Field): 129 | """ 130 | serializable field for enumerated data 131 | """ 132 | 133 | def __init__( 134 | self, 135 | enum, 136 | default=None, 137 | key=None, 138 | required=False, 139 | allow_none=True, 140 | by_value=False, 141 | ): 142 | """ 143 | :param enum: enum type for field validation 144 | :param default: default value for field if unset 145 | :param key: dictionary key to use for field serialization 146 | :param required: whether this field is required to deserialize an object 147 | :param allow_none: whether null should be considered a valid value 148 | :param by_value: whether to serialize by value or by symbol 149 | """ 150 | super().__init__(default=default, key=key, required=required, allow_none=allow_none) 151 | self._enum = enum 152 | self._by_value = by_value 153 | 154 | def marshmallow(self): 155 | return marshmallow.fields.Enum( 156 | self._enum, 157 | data_key=self._key, 158 | required=self._required, 159 | allow_none=self._allow_none, 160 | by_value=self._by_value 161 | ) 162 | 163 | 164 | class Nested(Field): 165 | """ 166 | field type for nested serializable object 167 | """ 168 | 169 | def __init__( 170 | self, 171 | default=None, 172 | key=None, 173 | field_type=None, 174 | required=False, 175 | allow_none=True 176 | ): 177 | """ 178 | :param default: default value for field if unset 179 | :param key: dictionary key to use for field serialization 180 | :param field_type: specified type for nested object 181 | :param required: whether this field is required to deserialize an object 182 | :param allow_none: whether null should be considered a valid value 183 | """ 184 | super().__init__(default=default, key=key, required=required, allow_none=allow_none) 185 | self._field_type = field_type 186 | 187 | def marshmallow(self): 188 | return NestedFactoryField( 189 | field_type=self._field_type, 190 | data_key=self._key, 191 | required=self._required, 192 | allow_none=self._allow_none 193 | ) 194 | 195 | 196 | class List(Field): 197 | """ 198 | field type for list of serializable objects 199 | """ 200 | 201 | def __init__( 202 | self, 203 | default=None, 204 | key=None, 205 | field_type=None, 206 | required=False, 207 | allow_none=True 208 | ): 209 | """ 210 | :param default: default value for field if unset 211 | :param key: dictionary key to use for field serialization 212 | :param field_type: specified type for list of nested objects 213 | :param required: whether this field is required to deserialize an object 214 | :param allow_none: whether null should be considered a valid value 215 | """ 216 | if default is None: 217 | default = [] 218 | super().__init__(default=default, key=key, required=required, allow_none=allow_none) 219 | self._field_type = field_type 220 | 221 | def marshmallow(self): 222 | if self._field_type is None or issubclass(self._field_type, SerializableABC): 223 | cls = NestedFactoryField(field_type=self._field_type) 224 | elif issubclass(self._field_type, FieldABC): 225 | cls = self._field_type().marshmallow() 226 | elif issubclass(self._field_type, marshmallow.fields.FieldABC): 227 | cls = self._field_type 228 | else: 229 | raise ValueError('Invalid field type in List: {}'.format(self._field_type)) 230 | 231 | return marshmallow.fields.List( 232 | cls, 233 | data_key=self._key, 234 | required=self._required, 235 | allow_none=self._allow_none 236 | ) 237 | -------------------------------------------------------------------------------- /objectfactory/nested.py: -------------------------------------------------------------------------------- 1 | """ 2 | nested field 3 | 4 | implements marshmallow field for objectfactory nested objects 5 | """ 6 | 7 | # lib 8 | import marshmallow 9 | 10 | # src 11 | from .serializable import Serializable 12 | from .factory import create 13 | 14 | 15 | class NestedFactoryField(marshmallow.fields.Field): 16 | 17 | def __init__(self, field_type=None, **kwargs): 18 | super().__init__(**kwargs) 19 | self._field_type = field_type 20 | 21 | def _serialize(self, value, attr, obj, **kwargs): 22 | """ 23 | dump serializable object within the interface of marshmallow field 24 | 25 | :param value: 26 | :param attr: 27 | :param obj: 28 | :param kwargs: 29 | :return: 30 | """ 31 | if not isinstance(value, Serializable): 32 | return {} 33 | return value.serialize(**obj._serialize_kwargs) 34 | 35 | def _deserialize(self, value, attr, data, **kwargs): 36 | """ 37 | create serializable object with factory through interface of marshmallow field 38 | 39 | :param value: 40 | :param attr: 41 | :param data: 42 | :param kwargs: 43 | :return: 44 | """ 45 | if value is None: 46 | return 47 | 48 | if '_type' in value: 49 | obj = create(value) 50 | if self._field_type and not isinstance(obj, self._field_type): 51 | raise ValueError( 52 | '{} is not an instance of type: {}'.format( 53 | type(obj).__name__, self._field_type.__name__) 54 | ) 55 | elif self._field_type: 56 | obj = self._field_type() 57 | obj.deserialize(value) 58 | else: 59 | raise ValueError('Cannot infer type information') 60 | 61 | return obj 62 | -------------------------------------------------------------------------------- /objectfactory/serializable.py: -------------------------------------------------------------------------------- 1 | """ 2 | serializable module 3 | 4 | implements base class and metaclass for serializable objects 5 | """ 6 | 7 | # lib 8 | from abc import ABCMeta 9 | import marshmallow 10 | 11 | # src 12 | from .base import FieldABC, SerializableABC 13 | 14 | 15 | class Meta(ABCMeta): 16 | """ 17 | metaclass for serializable classes 18 | 19 | this is a metaclass to be used for collecting relevant field information when 20 | defining a new serializable class 21 | """ 22 | 23 | def __new__(mcs, name, bases, attributes, schema=None): 24 | """ 25 | define a new serializable object class, collect and register all field descriptors, 26 | construct marshmallow schema 27 | 28 | :param name: class name 29 | :param bases: list of base classes to inherit from 30 | :param attributes: dictionary of class attributes 31 | :param schema: (optional) predefined marshmallow schema 32 | :return: newly defined class 33 | """ 34 | obj = ABCMeta.__new__(mcs, name, bases, attributes) 35 | 36 | # init and collect serializable fields of parents 37 | fields = {} 38 | for base in bases: 39 | fields.update(getattr(base, '_fields', {})) 40 | 41 | # populate with all serializable class descriptors 42 | for attr_name, attr in attributes.items(): 43 | if isinstance(attr, FieldABC): 44 | if attr._key is None: 45 | attr._key = attr_name 46 | attr._attr_key = '_' + attr_name # define key that descriptor will use to access data 47 | fields[attr_name] = attr 48 | 49 | # generate marshmallow schema 50 | if schema is None: 51 | marsh_fields = { 52 | attr_name: attr.marshmallow() 53 | for attr_name, attr in fields.items() 54 | } 55 | schema = marshmallow.Schema.from_dict( 56 | marsh_fields, 57 | name='_{}Schema'.format(name) 58 | ) 59 | 60 | # set fields and schema 61 | setattr(obj, '_fields', fields) 62 | setattr(obj, '_schema', schema) 63 | return obj 64 | 65 | 66 | class Serializable(SerializableABC, metaclass=Meta, schema=None): 67 | """ 68 | base class for serializable objects 69 | """ 70 | _fields = None 71 | _schema = None 72 | 73 | @classmethod 74 | def from_kwargs(cls, **kwargs): 75 | """ 76 | constructor to set field data by keyword args 77 | 78 | :param kwargs: keyword arguments by field 79 | :return: new instance of serializable object 80 | """ 81 | obj = cls() 82 | for key, val in kwargs.items(): 83 | if key in obj._fields: 84 | obj._fields[key].__set__(obj, val) 85 | 86 | return obj 87 | 88 | @classmethod 89 | def from_dict(cls, body: dict): 90 | """ 91 | constructor to set data with dictionary 92 | 93 | :param body: dictionary 94 | :return: new instance of serializable object 95 | """ 96 | obj = cls() 97 | obj.deserialize(body) 98 | 99 | return obj 100 | 101 | def serialize(self, include_type: bool = True, use_full_type: bool = True) -> dict: 102 | self._serialize_kwargs = { 103 | 'include_type': include_type, 104 | 'use_full_type': use_full_type 105 | } 106 | 107 | body = self._schema().dump(self) 108 | if include_type: 109 | if use_full_type: 110 | body['_type'] = self.__class__.__module__ + '.' + self.__class__.__name__ 111 | else: 112 | body['_type'] = self.__class__.__name__ 113 | return body 114 | 115 | def deserialize(self, body: dict): 116 | data = self._schema().load(body, unknown=marshmallow.EXCLUDE) 117 | for name, attr in self._fields.items(): 118 | if attr._key not in body: 119 | continue 120 | if name not in data: 121 | continue 122 | setattr(self, name, data[name]) 123 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open('README.md', 'r') as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name='objectfactory', 8 | version='0.2.0', 9 | author='Devin A. Conley', 10 | author_email='devinaconley@gmail.com', 11 | description='objectfactory is a python package to easily implement the factory design pattern for object creation, serialization, and polymorphism', 12 | long_description=long_description, 13 | long_description_content_type='text/markdown', 14 | url='https://github.com/devinaconley/py-object-factory', 15 | packages=setuptools.find_packages(), 16 | classifiers=( 17 | 'Programming Language :: Python :: 3', 18 | 'License :: OSI Approved :: MIT License', 19 | 'Operating System :: OS Independent', 20 | ), 21 | install_requires=[ 22 | 'marshmallow>=3,<4', 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devinaconley/py-object-factory/13a1fbfcc0982034e2b5364bacca53dc90248549/test/__init__.py -------------------------------------------------------------------------------- /test/test_boolean.py: -------------------------------------------------------------------------------- 1 | """ 2 | module for testing functionality of serializable Boolean field 3 | """ 4 | 5 | # lib 6 | import pytest 7 | import marshmallow 8 | 9 | # src 10 | from objectfactory import Serializable, Boolean 11 | 12 | 13 | class TestBoolean(object): 14 | """ 15 | test case for boolean field type 16 | """ 17 | 18 | def test_definition(self): 19 | """ 20 | test definition of class with boolean field 21 | 22 | expect field to be collected, registered, and included 23 | in schema creation 24 | """ 25 | 26 | class MyTestClass(Serializable): 27 | bool_prop = Boolean() 28 | 29 | assert isinstance(MyTestClass._fields, dict) 30 | assert len(MyTestClass._fields) == 1 31 | assert 'bool_prop' in MyTestClass._fields 32 | assert isinstance(MyTestClass._fields['bool_prop'], Boolean) 33 | 34 | schema = MyTestClass._schema 35 | assert issubclass(schema, marshmallow.Schema) 36 | assert len(schema._declared_fields) == 1 37 | assert 'bool_prop' in schema._declared_fields 38 | 39 | prop = schema._declared_fields['bool_prop'] 40 | assert isinstance(prop, marshmallow.fields.Boolean) 41 | 42 | def test_serialize(self): 43 | """ 44 | test serialize 45 | 46 | expect boolean data to be dumped to json body 47 | """ 48 | 49 | class MyTestClass(Serializable): 50 | bool_prop = Boolean() 51 | 52 | obj = MyTestClass() 53 | obj.bool_prop = True 54 | 55 | body = obj.serialize() 56 | 57 | assert body['_type'] == 'test.test_boolean.MyTestClass' 58 | assert body['bool_prop'] == True 59 | 60 | def test_deserialize(self): 61 | """ 62 | test deserialization 63 | 64 | expect boolean data to be loaded into class 65 | """ 66 | 67 | class MyTestClass(Serializable): 68 | bool_prop = Boolean() 69 | 70 | body = { 71 | '_type': 'MyTestClass', 72 | 'bool_prop': True 73 | } 74 | 75 | obj = MyTestClass() 76 | obj.deserialize(body) 77 | 78 | assert isinstance(obj, MyTestClass) 79 | assert type(obj.bool_prop) == bool 80 | assert obj.bool_prop == True 81 | 82 | def test_deserialize_cast(self): 83 | """ 84 | test deserialization casting 85 | 86 | expect int data to be cast to boolean 87 | """ 88 | 89 | class MyTestClass(Serializable): 90 | bool_prop = Boolean() 91 | 92 | body = { 93 | '_type': 'MyTestClass', 94 | 'bool_prop': 1 95 | } 96 | 97 | obj = MyTestClass() 98 | obj.deserialize(body) 99 | 100 | assert isinstance(obj, MyTestClass) 101 | assert type(obj.bool_prop) == bool 102 | assert obj.bool_prop == True 103 | 104 | body = { 105 | '_type': 'MyTestClass', 106 | 'bool_prop': 0 107 | } 108 | 109 | obj = MyTestClass() 110 | obj.deserialize(body) 111 | 112 | assert isinstance(obj, MyTestClass) 113 | assert type(obj.bool_prop) == bool 114 | assert obj.bool_prop == False 115 | 116 | def test_deserialize_invalid_int(self): 117 | """ 118 | test deserialization validation 119 | 120 | expect validation error to be raised on invalid boolean data from 121 | int not in [0, 1] 122 | """ 123 | 124 | class MyTestClass(Serializable): 125 | bool_prop = Boolean() 126 | 127 | body = { 128 | '_type': 'MyTestClass', 129 | 'bool_prop': 2 130 | } 131 | 132 | obj = MyTestClass() 133 | with pytest.raises(marshmallow.ValidationError): 134 | obj.deserialize(body) 135 | 136 | def test_deserialize_invalid_float(self): 137 | """ 138 | test deserialization validation 139 | 140 | expect validation error to be raised on invalid boolean data from float 141 | """ 142 | 143 | class MyTestClass(Serializable): 144 | bool_prop = Boolean() 145 | 146 | body = { 147 | '_type': 'MyTestClass', 148 | 'bool_prop': 1.1 149 | } 150 | 151 | obj = MyTestClass() 152 | with pytest.raises(marshmallow.ValidationError): 153 | obj.deserialize(body) 154 | 155 | def test_deserialize_invalid_string(self): 156 | """ 157 | test deserialization validation 158 | 159 | expect validation error to be raised on invalid boolean data from string 160 | """ 161 | 162 | class MyTestClass(Serializable): 163 | bool_prop = Boolean() 164 | 165 | body = { 166 | '_type': 'MyTestClass', 167 | 'bool_prop': 'helloworld' 168 | } 169 | 170 | obj = MyTestClass() 171 | with pytest.raises(marshmallow.ValidationError): 172 | obj.deserialize(body) 173 | -------------------------------------------------------------------------------- /test/test_custom_schema.py: -------------------------------------------------------------------------------- 1 | """ 2 | module for testing custom marshmallow schema 3 | """ 4 | 5 | # lib 6 | import datetime 7 | import pytest 8 | import marshmallow 9 | 10 | # src 11 | from objectfactory import Serializable, Field 12 | 13 | 14 | class TestCustomSchema(object): 15 | """ 16 | test group for custom marshmallow serialization schema 17 | """ 18 | 19 | def test_serialize(self): 20 | """ 21 | test serialize 22 | 23 | expect date field to be serialized to a string (YYYY-MM-DD) 24 | """ 25 | 26 | class CustomSchema(marshmallow.Schema): 27 | date = marshmallow.fields.Date() 28 | 29 | class MyTestClass(Serializable, schema=CustomSchema): 30 | date = Field() 31 | 32 | obj = MyTestClass() 33 | obj.date = datetime.date(2012, 3, 4) 34 | 35 | body = obj.serialize() 36 | 37 | assert body['_type'] == 'test.test_custom_schema.MyTestClass' 38 | assert body['date'] == '2012-03-04' 39 | 40 | def test_deserialize(self): 41 | """ 42 | test deserialize 43 | 44 | expect date field to be loaded from date string encoding (YYYY-MM-DD) 45 | """ 46 | 47 | class CustomSchema(marshmallow.Schema): 48 | date = marshmallow.fields.Date() 49 | 50 | class MyTestClass(Serializable, schema=CustomSchema): 51 | date = Field() 52 | 53 | body = { 54 | '_type': 'MyTestClass', 55 | 'date': '2012-03-04' 56 | } 57 | 58 | obj = MyTestClass() 59 | obj.deserialize(body) 60 | 61 | assert isinstance(obj, MyTestClass) 62 | assert obj.date.year == 2012 63 | assert obj.date.month == 3 64 | assert obj.date.day == 4 65 | 66 | def test_deserialize_invalid(self): 67 | """ 68 | test deserialize 69 | 70 | expect exception to be thrown on invalid date string 71 | """ 72 | 73 | class CustomSchema(marshmallow.Schema): 74 | date = marshmallow.fields.Date() 75 | 76 | class MyTestClass(Serializable, schema=CustomSchema): 77 | date = Field() 78 | 79 | body = { 80 | '_type': 'MyTestClass', 81 | 'date': '2012-03-45' 82 | } 83 | 84 | obj = MyTestClass() 85 | with pytest.raises(marshmallow.exceptions.ValidationError): 86 | obj.deserialize(body) 87 | 88 | 89 | class TestCustomField(object): 90 | """ 91 | test group for custom marshmallow field 92 | """ 93 | 94 | def test_serialize(self): 95 | """ 96 | test serialize 97 | 98 | expect property to be serialized to a lowercase string 99 | """ 100 | 101 | class CustomField(marshmallow.fields.Field): 102 | def _serialize(self, value, *args, **kwargs): 103 | return str(value).lower() 104 | 105 | class CustomSchema(marshmallow.Schema): 106 | str_prop = CustomField() 107 | 108 | class MyTestClass(Serializable, schema=CustomSchema): 109 | str_prop = Field() 110 | 111 | obj = MyTestClass() 112 | obj.str_prop = 'HELLO' 113 | 114 | body = obj.serialize() 115 | 116 | assert body['_type'] == 'test.test_custom_schema.MyTestClass' 117 | assert body['str_prop'] == 'hello' 118 | 119 | def test_deserialize(self): 120 | """ 121 | test deserialize 122 | 123 | expect string to be loaded and formatted as all uppercase 124 | """ 125 | 126 | class CustomField(marshmallow.fields.Field): 127 | def _deserialize(self, value, *args, **kwargs): 128 | if len(value) > 8: 129 | raise marshmallow.ValidationError('Field too long') 130 | return str(value).upper() 131 | 132 | class CustomSchema(marshmallow.Schema): 133 | str_prop = CustomField() 134 | 135 | class MyTestClass(Serializable, schema=CustomSchema): 136 | str_prop = Field() 137 | 138 | body = { 139 | '_type': 'MyTestClass', 140 | 'str_prop': 'hello' 141 | } 142 | 143 | obj = MyTestClass() 144 | obj.deserialize(body) 145 | 146 | assert isinstance(obj, MyTestClass) 147 | assert obj.str_prop == 'HELLO' 148 | 149 | def test_deserialize_invalid(self): 150 | """ 151 | test deserialize 152 | 153 | expect exception to be thrown on too long of a string 154 | """ 155 | 156 | class CustomField(marshmallow.fields.Field): 157 | def _deserialize(self, value, *args, **kwargs): 158 | if len(value) > 8: 159 | raise marshmallow.ValidationError('Field too long') 160 | return str(value).upper() 161 | 162 | class CustomSchema(marshmallow.Schema): 163 | str_prop = CustomField() 164 | 165 | class MyTestClass(Serializable, schema=CustomSchema): 166 | str_prop = Field() 167 | 168 | body = { 169 | '_type': 'MyTestClass', 170 | 'str_prop': 'helloworld' 171 | } 172 | 173 | obj = MyTestClass() 174 | with pytest.raises(marshmallow.exceptions.ValidationError): 175 | obj.deserialize(body) 176 | -------------------------------------------------------------------------------- /test/test_datetime.py: -------------------------------------------------------------------------------- 1 | """ 2 | module for testing functionality of serializable datetime field 3 | """ 4 | 5 | # lib 6 | import pytest 7 | import marshmallow 8 | import datetime 9 | import zoneinfo 10 | 11 | # src 12 | from objectfactory import Serializable, DateTime 13 | 14 | 15 | class TestDateTime(object): 16 | """ 17 | test case for datetime field type 18 | """ 19 | 20 | def test_definition(self): 21 | """ 22 | test definition of class with datetime field 23 | 24 | expect field to be collected, registered, and included 25 | in schema creation 26 | """ 27 | 28 | class MyTestClass(Serializable): 29 | datetime_prop = DateTime() 30 | 31 | assert isinstance(MyTestClass._fields, dict) 32 | assert len(MyTestClass._fields) == 1 33 | assert 'datetime_prop' in MyTestClass._fields 34 | assert isinstance(MyTestClass._fields['datetime_prop'], DateTime) 35 | 36 | schema = MyTestClass._schema 37 | assert issubclass(schema, marshmallow.Schema) 38 | assert len(schema._declared_fields) == 1 39 | assert 'datetime_prop' in schema._declared_fields 40 | 41 | prop = schema._declared_fields['datetime_prop'] 42 | assert isinstance(prop, marshmallow.fields.DateTime) 43 | 44 | def test_serialize(self): 45 | """ 46 | test serialize 47 | 48 | expect datetime data to be dumped to json body 49 | """ 50 | 51 | class MyTestClass(Serializable): 52 | datetime_prop = DateTime() 53 | 54 | obj = MyTestClass() 55 | obj.datetime_prop = datetime.datetime(year=2024, month=1, day=14, hour=6, minute=30) 56 | 57 | body = obj.serialize() 58 | 59 | assert body['_type'] == 'test.test_datetime.MyTestClass' 60 | assert body['datetime_prop'] == '2024-01-14T06:30:00' 61 | 62 | def test_deserialize(self): 63 | """ 64 | test deserialization 65 | 66 | expect string datetime data to be loaded into field 67 | """ 68 | 69 | class MyTestClass(Serializable): 70 | datetime_prop = DateTime() 71 | 72 | body = { 73 | '_type': 'MyTestClass', 74 | 'datetime_prop': '2000-01-01T00:00:00' 75 | } 76 | 77 | obj = MyTestClass() 78 | obj.deserialize(body) 79 | 80 | assert isinstance(obj, MyTestClass) 81 | assert type(obj.datetime_prop) == datetime.datetime 82 | assert obj.datetime_prop == datetime.datetime(year=2000, month=1, day=1) 83 | 84 | def test_serialize_custom(self): 85 | """ 86 | test serialize 87 | 88 | expect datetime data to be dumped to json body with custom date string format 89 | """ 90 | 91 | class MyTestClass(Serializable): 92 | datetime_prop = DateTime(date_format='%Y/%m/%d') 93 | 94 | obj = MyTestClass() 95 | obj.datetime_prop = datetime.datetime(year=2024, month=1, day=14, hour=6, minute=30) 96 | 97 | body = obj.serialize() 98 | 99 | assert body['_type'] == 'test.test_datetime.MyTestClass' 100 | assert body['datetime_prop'] == '2024/01/14' 101 | 102 | def test_deserialize_custom(self): 103 | """ 104 | test deserialization casting 105 | 106 | expect datetime data to be loaded from custom date string format 107 | """ 108 | 109 | class MyTestClass(Serializable): 110 | datetime_prop = DateTime(date_format='%Y/%m/%d') 111 | 112 | body = { 113 | '_type': 'MyTestClass', 114 | 'datetime_prop': '2010/10/10' 115 | } 116 | 117 | obj = MyTestClass() 118 | obj.deserialize(body) 119 | 120 | assert isinstance(obj, MyTestClass) 121 | assert type(obj.datetime_prop) == datetime.datetime 122 | assert obj.datetime_prop == datetime.datetime(year=2010, month=10, day=10) 123 | 124 | def test_serialize_tz(self): 125 | """ 126 | test serialize 127 | 128 | expect datetime data to be dumped to json body with timezone 129 | """ 130 | 131 | class MyTestClass(Serializable): 132 | datetime_prop = DateTime() 133 | 134 | obj = MyTestClass() 135 | obj.datetime_prop = datetime.datetime( 136 | year=2024, month=1, day=14, hour=6, minute=30, 137 | tzinfo=zoneinfo.ZoneInfo('EST') 138 | ) 139 | 140 | body = obj.serialize() 141 | 142 | assert body['_type'] == 'test.test_datetime.MyTestClass' 143 | assert body['datetime_prop'] == '2024-01-14T06:30:00-05:00' 144 | -------------------------------------------------------------------------------- /test/test_enum.py: -------------------------------------------------------------------------------- 1 | """ 2 | module for testing functionality of serializable enum field 3 | """ 4 | 5 | # lib 6 | import pytest 7 | import marshmallow 8 | import enum 9 | 10 | # src 11 | from objectfactory import Serializable, Enum 12 | 13 | 14 | class TestEnum(object): 15 | """ 16 | test case for enum field type 17 | """ 18 | 19 | def test_definition(self): 20 | """ 21 | test definition of class with enum field 22 | 23 | expect enum field to be collected, registered, and included 24 | in schema creation 25 | """ 26 | 27 | class Color(enum.Enum): 28 | RED = 1 29 | GREEN = 2 30 | BLUE = 3 31 | 32 | class MyTestClass(Serializable): 33 | enum_prop = Enum(Color) 34 | 35 | assert isinstance(MyTestClass._fields, dict) 36 | assert len(MyTestClass._fields) == 1 37 | assert 'enum_prop' in MyTestClass._fields 38 | assert isinstance(MyTestClass._fields['enum_prop'], Enum) 39 | 40 | schema = MyTestClass._schema 41 | assert issubclass(schema, marshmallow.Schema) 42 | assert len(schema._declared_fields) == 1 43 | assert 'enum_prop' in schema._declared_fields 44 | 45 | prop = schema._declared_fields['enum_prop'] 46 | assert isinstance(prop, marshmallow.fields.Enum) 47 | 48 | def test_serialize(self): 49 | """ 50 | test serialize 51 | expect enum data to be dumped to json body 52 | """ 53 | 54 | class Color(enum.Enum): 55 | RED = 1 56 | GREEN = 2 57 | BLUE = 3 58 | 59 | class MyTestClass(Serializable): 60 | enum_prop = Enum(Color) 61 | 62 | obj = MyTestClass() 63 | obj.enum_prop = Color.GREEN 64 | 65 | body = obj.serialize() 66 | 67 | assert body['_type'] == 'test.test_enum.MyTestClass' 68 | assert body['enum_prop'] == 'GREEN' 69 | 70 | def test_deserialize(self): 71 | """ 72 | test deserialization 73 | expect string enum data to be loaded into field 74 | """ 75 | 76 | class Color(enum.Enum): 77 | RED = 1 78 | GREEN = 2 79 | BLUE = 3 80 | 81 | class MyTestClass(Serializable): 82 | enum_prop = Enum(Color) 83 | 84 | body = { 85 | '_type': 'MyTestClass', 86 | 'enum_prop': 'GREEN' 87 | } 88 | 89 | obj = MyTestClass() 90 | obj.deserialize(body) 91 | 92 | assert isinstance(obj, MyTestClass) 93 | assert type(obj.enum_prop) == Color 94 | assert obj.enum_prop == Color.GREEN 95 | 96 | def test_serialize_by_value(self): 97 | """ 98 | test serialize 99 | 100 | expect enum data to be dumped to json body by name 101 | """ 102 | 103 | class Color(enum.Enum): 104 | RED = 1 105 | GREEN = 2 106 | BLUE = 3 107 | 108 | class MyTestClass(Serializable): 109 | enum_prop = Enum(Color, by_value=True) 110 | other_enum = Enum(Color, by_value=True) 111 | 112 | obj = MyTestClass() 113 | obj.enum_prop = Color.BLUE 114 | obj.other_enum = Color.RED 115 | 116 | body = obj.serialize() 117 | 118 | assert body['_type'] == 'test.test_enum.MyTestClass' 119 | assert body['enum_prop'] == 3 # 'BLUE' 120 | assert body['other_enum'] == 1 # 'RED' 121 | 122 | def test_deserialize_by_value(self): 123 | """ 124 | test deserialization casting 125 | expect enum data to be loaded properly by name 126 | """ 127 | 128 | class Color(enum.Enum): 129 | RED = 1 130 | GREEN = 2 131 | BLUE = 3 132 | 133 | class MyTestClass(Serializable): 134 | enum_prop = Enum(Color, by_value=True) 135 | 136 | body = { 137 | '_type': 'MyTestClass', 138 | 'enum_prop': 3 139 | } 140 | 141 | obj = MyTestClass() 142 | obj.deserialize(body) 143 | 144 | assert isinstance(obj, MyTestClass) 145 | assert type(obj.enum_prop) == Color 146 | assert obj.enum_prop == Color.BLUE 147 | -------------------------------------------------------------------------------- /test/test_factory.py: -------------------------------------------------------------------------------- 1 | """ 2 | module for testing functionality of serializable factory 3 | """ 4 | 5 | # lib 6 | import pytest 7 | 8 | # src 9 | import objectfactory 10 | from objectfactory.factory import _global_factory 11 | from .testmodule.testclasses import MyBasicClass, MyComplexClass 12 | 13 | 14 | class TestFactory(object): 15 | """ 16 | test case for serializable factory 17 | """ 18 | 19 | def test_register_class(self): 20 | """ 21 | validate register_class method 22 | 23 | MyBasicClass should be registered 24 | """ 25 | assert 'MyBasicClass' in _global_factory.registry 26 | assert 'test.testmodule.testclasses.MyBasicClass' in _global_factory.registry 27 | 28 | def test_create_object(self): 29 | """ 30 | validate create object method 31 | 32 | expect object to be deserialized properly 33 | """ 34 | body = { 35 | '_type': 'test.testmodule.testclasses.MyBasicClass', 36 | 'str_prop': 'somestring', 37 | 'int_prop': 42, 38 | } 39 | obj = objectfactory.create(body) 40 | 41 | assert isinstance(obj, MyBasicClass) 42 | assert obj.str_prop == 'somestring' 43 | assert obj.int_prop == 42 44 | 45 | def test_create_object_short_type(self): 46 | """ 47 | validate create object method without fully qualified path 48 | 49 | expect object to be deserialized properly 50 | """ 51 | body = { 52 | '_type': 'MyBasicClass', 53 | 'str_prop': 'somestring', 54 | 'int_prop': 42, 55 | } 56 | obj = objectfactory.create(body) 57 | 58 | assert isinstance(obj, MyBasicClass) 59 | assert obj.str_prop == 'somestring' 60 | assert obj.int_prop == 42 61 | 62 | def test_create_object_other_full_path(self): 63 | """ 64 | validate create object method when full path is altered 65 | 66 | expect object to be deserialized properly based on last path element 67 | """ 68 | body = { 69 | '_type': 'some.other.module.MyBasicClass', 70 | 'str_prop': 'somestring', 71 | 'int_prop': 42, 72 | } 73 | obj = objectfactory.create(body) 74 | 75 | assert isinstance(obj, MyBasicClass) 76 | assert obj.str_prop == 'somestring' 77 | assert obj.int_prop == 42 78 | 79 | def test_create_object_unregistered(self): 80 | """ 81 | validate create object method throws when unregistered 82 | 83 | expect ValueError to be raised indicating that the type is not registered 84 | """ 85 | body = { 86 | '_type': 'MyClassThatDoesNotExist', 87 | 'str_prop': 'somestring', 88 | 'int_prop': 42, 89 | } 90 | with pytest.raises(ValueError, match=r'.*type MyClassThatDoesNotExist not found.*'): 91 | _ = objectfactory.create(body) 92 | 93 | def test_create_object_typed(self): 94 | """ 95 | validate create object method when enforcing type 96 | 97 | expect object to be returned with full type hinting 98 | """ 99 | body = { 100 | '_type': 'test.testmodule.testclasses.MyBasicClass', 101 | 'str_prop': 'somestring', 102 | 'int_prop': 42, 103 | } 104 | obj = objectfactory.create(body, object_type=MyBasicClass) 105 | 106 | assert isinstance(obj, MyBasicClass) 107 | assert obj.str_prop == 'somestring' 108 | assert obj.int_prop == 42 109 | 110 | def test_create_object_typed_invalid(self): 111 | """ 112 | validate create object method throws when type mismatch 113 | 114 | expect TypeError to be raised indicating a type mismatch 115 | """ 116 | body = { 117 | '_type': 'test.testmodule.testclasses.MyBasicClass', 118 | 'str_prop': 'somestring', 119 | 'int_prop': 42, 120 | } 121 | with pytest.raises( 122 | TypeError, 123 | match=r'.*Object type MyBasicClass is not a MyComplexClass.*' 124 | ): 125 | _ = objectfactory.create(body, object_type=MyComplexClass) 126 | -------------------------------------------------------------------------------- /test/test_fields.py: -------------------------------------------------------------------------------- 1 | """ 2 | module for testing serializable List field 3 | """ 4 | 5 | # lib 6 | import pytest 7 | import marshmallow 8 | 9 | # src 10 | from objectfactory import Serializable, Field 11 | 12 | 13 | class TestFieldOptionals(object): 14 | """ 15 | test case for optional params to field arguments 16 | """ 17 | 18 | def test_serialize_default(self): 19 | """ 20 | test serialization 21 | 22 | expect default value to be serialized for unset str_prop 23 | """ 24 | 25 | class MyTestClass(Serializable): 26 | str_prop = Field(default='default_val') 27 | 28 | obj = MyTestClass() 29 | 30 | assert obj.str_prop == 'default_val' 31 | 32 | body = obj.serialize() 33 | 34 | assert body['_type'] == 'test.test_fields.MyTestClass' 35 | assert body['str_prop'] == 'default_val' 36 | 37 | def test_deserialize_default(self): 38 | """ 39 | test deserialization 40 | 41 | expect default value to be loaded into str_prop if not specified, and 42 | int_prop_named to be deserialized into int_prop 43 | """ 44 | 45 | class MyTestClass(Serializable): 46 | str_prop = Field(default='default_val') 47 | 48 | body = { 49 | '_type': 'MyTestClass' 50 | } 51 | 52 | obj = MyTestClass() 53 | obj.deserialize(body) 54 | 55 | assert isinstance(obj, MyTestClass) 56 | assert obj.str_prop == 'default_val' 57 | 58 | def test_serialize_keyed(self): 59 | """ 60 | test serialization 61 | 62 | expect int_prop to be serialized under key int_prop_named 63 | """ 64 | 65 | class MyTestClass(Serializable): 66 | int_prop = Field(key='int_prop_named') 67 | 68 | obj = MyTestClass() 69 | obj.int_prop = 99 70 | 71 | body = obj.serialize() 72 | 73 | assert body['_type'] == 'test.test_fields.MyTestClass' 74 | assert body['int_prop_named'] == 99 75 | 76 | def test_deserialize_keyed(self): 77 | """ 78 | test deserialization 79 | 80 | expect int_prop_named to be deserialized into int_prop 81 | """ 82 | 83 | class MyTestClass(Serializable): 84 | int_prop = Field(key='int_prop_named') 85 | 86 | body = { 87 | '_type': 'MyTestClass', 88 | 'int_prop_named': 99 89 | } 90 | 91 | obj = MyTestClass() 92 | obj.deserialize(body) 93 | 94 | assert isinstance(obj, MyTestClass) 95 | assert obj.int_prop == 99 96 | 97 | def test_deserialize_required(self): 98 | """ 99 | test deserialization of required field 100 | 101 | expect required property to be deserialized into prop 102 | """ 103 | 104 | class MyTestClass(Serializable): 105 | prop = Field(required=True) 106 | 107 | body = { 108 | '_type': 'MyTestClass', 109 | 'prop': 42 110 | } 111 | 112 | obj = MyTestClass() 113 | obj.deserialize(body) 114 | 115 | assert isinstance(obj, MyTestClass) 116 | assert obj.prop == 42 117 | 118 | def test_deserialize_required_missing(self): 119 | """ 120 | test deserialization of required field 121 | 122 | expect exception to be thrown on missing prop field 123 | """ 124 | 125 | class MyTestClass(Serializable): 126 | prop = Field(required=True) 127 | 128 | body = { 129 | '_type': 'MyTestClass' 130 | } 131 | 132 | obj = MyTestClass() 133 | with pytest.raises(marshmallow.exceptions.ValidationError): 134 | obj.deserialize(body) 135 | 136 | def test_deserialize_null(self): 137 | """ 138 | test deserialization validation 139 | 140 | expect null to be deserialized to None 141 | """ 142 | 143 | class MyTestClass(Serializable): 144 | prop = Field() 145 | 146 | body = { 147 | '_type': 'MyTestClass', 148 | 'prop': None 149 | } 150 | 151 | obj = MyTestClass() 152 | obj.deserialize(body) 153 | 154 | assert isinstance(obj, MyTestClass) 155 | assert obj.prop is None 156 | 157 | def test_deserialize_null_disallowed(self): 158 | """ 159 | test deserialization validation 160 | 161 | expect exception to be thrown when null value is not allowed 162 | """ 163 | 164 | class MyTestClass(Serializable): 165 | prop = Field(allow_none=False) 166 | 167 | body = { 168 | '_type': 'MyTestClass', 169 | 'prop': None 170 | } 171 | 172 | obj = MyTestClass() 173 | with pytest.raises(marshmallow.exceptions.ValidationError): 174 | obj.deserialize(body) 175 | -------------------------------------------------------------------------------- /test/test_float.py: -------------------------------------------------------------------------------- 1 | """ 2 | module for testing functionality of serializable Float field 3 | """ 4 | 5 | # lib 6 | import pytest 7 | import marshmallow 8 | 9 | # src 10 | from objectfactory import Serializable, Float 11 | 12 | 13 | class TestFloat(object): 14 | """ 15 | test case for float field type 16 | """ 17 | 18 | def test_definition(self): 19 | """ 20 | test definition of class with float field 21 | 22 | expect field to be collected, registered, and included 23 | in schema creation 24 | """ 25 | 26 | class MyTestClass(Serializable): 27 | float_prop = Float() 28 | 29 | assert isinstance(MyTestClass._fields, dict) 30 | assert len(MyTestClass._fields) == 1 31 | assert 'float_prop' in MyTestClass._fields 32 | assert isinstance(MyTestClass._fields['float_prop'], Float) 33 | 34 | schema = MyTestClass._schema 35 | assert issubclass(schema, marshmallow.Schema) 36 | assert len(schema._declared_fields) == 1 37 | assert 'float_prop' in schema._declared_fields 38 | 39 | prop = schema._declared_fields['float_prop'] 40 | assert isinstance(prop, marshmallow.fields.Float) 41 | 42 | def test_serialize(self): 43 | """ 44 | test serialize 45 | 46 | expect float data to be dumped to json body 47 | """ 48 | 49 | class MyTestClass(Serializable): 50 | float_prop = Float() 51 | 52 | obj = MyTestClass() 53 | obj.float_prop = 99.99 54 | 55 | body = obj.serialize() 56 | 57 | assert body['_type'] == 'test.test_float.MyTestClass' 58 | assert body['float_prop'] == 99.99 59 | 60 | def test_deserialize(self): 61 | """ 62 | test deserialization 63 | 64 | expect float data to be loaded into class 65 | """ 66 | 67 | class MyTestClass(Serializable): 68 | float_prop = Float() 69 | 70 | body = { 71 | '_type': 'MyTestClass', 72 | 'float_prop': 99.99 73 | } 74 | 75 | obj = MyTestClass() 76 | obj.deserialize(body) 77 | 78 | assert isinstance(obj, MyTestClass) 79 | assert type(obj.float_prop) == float 80 | assert obj.float_prop == 99.99 81 | 82 | def test_deserialize_cast(self): 83 | """ 84 | test deserialization casting 85 | 86 | expect int data to be cast to float 87 | """ 88 | 89 | class MyTestClass(Serializable): 90 | float_prop = Float() 91 | 92 | body = { 93 | '_type': 'MyTestClass', 94 | 'float_prop': 99 95 | } 96 | 97 | obj = MyTestClass() 98 | obj.deserialize(body) 99 | 100 | assert isinstance(obj, MyTestClass) 101 | assert type(obj.float_prop) == float 102 | assert obj.float_prop == 99.0 103 | 104 | def test_deserialize_invalid(self): 105 | """ 106 | test deserialization validation 107 | 108 | expect validation error to be raised on invalid float data 109 | """ 110 | 111 | class MyTestClass(Serializable): 112 | float_prop = Float() 113 | 114 | body = { 115 | '_type': 'MyTestClass', 116 | 'float_prop': 'not an float' 117 | } 118 | 119 | obj = MyTestClass() 120 | with pytest.raises(marshmallow.ValidationError): 121 | obj.deserialize(body) 122 | -------------------------------------------------------------------------------- /test/test_integer.py: -------------------------------------------------------------------------------- 1 | """ 2 | module for testing functionality of serializable Integer field 3 | """ 4 | 5 | # lib 6 | import pytest 7 | import marshmallow 8 | 9 | # src 10 | from objectfactory import Serializable, Integer 11 | 12 | 13 | class TestInteger(object): 14 | """ 15 | test case for integer field type 16 | """ 17 | 18 | def test_definition(self): 19 | """ 20 | test definition of class with integer field 21 | 22 | expect field to be collected, registered, and included 23 | in schema creation 24 | """ 25 | 26 | class MyTestClass(Serializable): 27 | int_prop = Integer() 28 | 29 | assert isinstance(MyTestClass._fields, dict) 30 | assert len(MyTestClass._fields) == 1 31 | assert 'int_prop' in MyTestClass._fields 32 | assert isinstance(MyTestClass._fields['int_prop'], Integer) 33 | 34 | schema = MyTestClass._schema 35 | assert issubclass(schema, marshmallow.Schema) 36 | assert len(schema._declared_fields) == 1 37 | assert 'int_prop' in schema._declared_fields 38 | 39 | prop = schema._declared_fields['int_prop'] 40 | assert isinstance(prop, marshmallow.fields.Integer) 41 | 42 | def test_serialize(self): 43 | """ 44 | test serialize 45 | 46 | expect integer data to be dumped to json body 47 | """ 48 | 49 | class MyTestClass(Serializable): 50 | int_prop = Integer() 51 | 52 | obj = MyTestClass() 53 | obj.int_prop = 99 54 | 55 | body = obj.serialize() 56 | 57 | assert body['_type'] == 'test.test_integer.MyTestClass' 58 | assert body['int_prop'] == 99 59 | 60 | def test_deserialize(self): 61 | """ 62 | test deserialization 63 | 64 | expect integer data to be loaded into class 65 | """ 66 | 67 | class MyTestClass(Serializable): 68 | int_prop = Integer() 69 | 70 | body = { 71 | '_type': 'MyTestClass', 72 | 'int_prop': 99 73 | } 74 | 75 | obj = MyTestClass() 76 | obj.deserialize(body) 77 | 78 | assert isinstance(obj, MyTestClass) 79 | assert obj.int_prop == 99 80 | 81 | def test_deserialize_cast(self): 82 | """ 83 | test deserialization casting 84 | 85 | expect float data to be cast to integer 86 | """ 87 | 88 | class MyTestClass(Serializable): 89 | int_prop = Integer() 90 | 91 | body = { 92 | '_type': 'MyTestClass', 93 | 'int_prop': 99.01 94 | } 95 | 96 | obj = MyTestClass() 97 | obj.deserialize(body) 98 | 99 | assert isinstance(obj, MyTestClass) 100 | assert type(obj.int_prop) == int 101 | assert obj.int_prop == 99 102 | 103 | def test_deserialize_invalid(self): 104 | """ 105 | test deserialization validation 106 | 107 | expect validation error to be raised on invalid integer data 108 | """ 109 | 110 | class MyTestClass(Serializable): 111 | int_prop = Integer() 112 | 113 | body = { 114 | '_type': 'MyTestClass', 115 | 'int_prop': 'not an integer' 116 | } 117 | 118 | obj = MyTestClass() 119 | with pytest.raises(marshmallow.ValidationError): 120 | obj.deserialize(body) 121 | -------------------------------------------------------------------------------- /test/test_list.py: -------------------------------------------------------------------------------- 1 | """ 2 | module for testing functionality of serializable list field 3 | """ 4 | 5 | # lib 6 | import pytest 7 | import marshmallow 8 | 9 | # src 10 | import objectfactory 11 | from objectfactory import Serializable, List, String, Integer, register 12 | 13 | 14 | class TestPrimitiveList(object): 15 | """ 16 | test case for serialization of basic lists of primitive strings and integers 17 | """ 18 | 19 | def test_serialize(self): 20 | """ 21 | test serialization 22 | 23 | expect list within a standard field to function as standard list object 24 | and to dump up-to-date list of all primitive values 25 | """ 26 | 27 | class MyTestClass(Serializable): 28 | str_list_prop = List(field_type=String) 29 | int_list_prop = List(field_type=Integer) 30 | 31 | obj = MyTestClass() 32 | obj.str_list_prop = ['hello', 'world'] 33 | obj.int_list_prop = [0, 1, 2, 3, 4] 34 | obj.str_list_prop.append('!') 35 | obj.int_list_prop.append(5) 36 | body = obj.serialize() 37 | 38 | assert body['_type'] == 'test.test_list.MyTestClass' 39 | assert body['str_list_prop'] == ['hello', 'world', '!'] 40 | assert body['int_list_prop'] == [0, 1, 2, 3, 4, 5] 41 | 42 | def test_deserialize(self): 43 | """ 44 | test deserialization 45 | 46 | expect json list of primitives to be loaded properly into 47 | serializable object 48 | """ 49 | 50 | class MyTestClass(Serializable): 51 | str_list_prop = List(field_type=String) 52 | int_list_prop = List(field_type=Integer) 53 | 54 | body = { 55 | '_type': 'MyTestClass', 56 | 'str_list_prop': ['my', 'awesome', 'list', 'of', 'strings'], 57 | 'int_list_prop': [9001, 9002, 9003] 58 | } 59 | 60 | obj = MyTestClass() 61 | obj.deserialize(body) 62 | 63 | assert isinstance(obj, MyTestClass) 64 | assert obj.str_list_prop == ['my', 'awesome', 'list', 'of', 'strings'] 65 | assert obj.int_list_prop == [9001, 9002, 9003] 66 | 67 | def test_deserialize_invalid(self): 68 | """ 69 | test deserialization validation 70 | 71 | expect validation error to be raised on invalid integer data 72 | """ 73 | 74 | class MyTestClass(Serializable): 75 | str_list_prop = List(field_type=String) 76 | int_list_prop = List(field_type=Integer) 77 | 78 | body = { 79 | '_type': 'MyTestClass', 80 | 'str_list_prop': ['my', 'awesome', 'list', 'of', 'strings'], 81 | 'int_list_prop': [9001, 9002, 9003, 'string'] 82 | } 83 | 84 | obj = MyTestClass() 85 | 86 | with pytest.raises(marshmallow.ValidationError): 87 | obj.deserialize(body) 88 | 89 | def test_serialize_deep_copy(self): 90 | """ 91 | test serialization 92 | 93 | expect modification to original list to have no effect on 94 | serialized json body 95 | """ 96 | 97 | class MyTestClass(Serializable): 98 | str_list_prop = List(field_type=String) 99 | int_list_prop = List(field_type=Integer) 100 | 101 | obj = MyTestClass() 102 | obj.str_list_prop = ['hello', 'world'] 103 | obj.int_list_prop = [0, 1, 2, 3, 4] 104 | body = obj.serialize() 105 | 106 | obj.str_list_prop.append('!') 107 | obj.int_list_prop.append(5) 108 | 109 | assert body['_type'] == 'test.test_list.MyTestClass' 110 | assert body['str_list_prop'] == ['hello', 'world'] 111 | assert body['int_list_prop'] == [0, 1, 2, 3, 4] 112 | 113 | def test_deserialize_deep_copy(self): 114 | """ 115 | test deserialization 116 | 117 | expect modification to original json list of primitives to 118 | have not effect on loaded serializable object 119 | """ 120 | 121 | class MyTestClass(Serializable): 122 | str_list_prop = List(field_type=String) 123 | int_list_prop = List(field_type=Integer) 124 | 125 | body = { 126 | '_type': 'MyTestClass', 127 | 'str_list_prop': ['my', 'awesome', 'list', 'of', 'strings'], 128 | 'int_list_prop': [9001, 9002, 9003] 129 | } 130 | 131 | obj = MyTestClass() 132 | obj.deserialize(body) 133 | 134 | body['str_list_prop'].pop() 135 | body['int_list_prop'].pop() 136 | 137 | assert isinstance(obj, MyTestClass) 138 | assert obj.str_list_prop == ['my', 'awesome', 'list', 'of', 'strings'] 139 | assert obj.int_list_prop == [9001, 9002, 9003] 140 | 141 | 142 | class TestNestedList(object): 143 | """ 144 | test case for model containing a list of nested serializable objects 145 | """ 146 | 147 | def setup_method(self, _): 148 | """ 149 | prepare for each test 150 | """ 151 | objectfactory.factory._global_factory.registry.clear() 152 | 153 | def test_serializable(self): 154 | """ 155 | test serialization 156 | 157 | expect all nested instances of MyNestedClass to populate a list 158 | in json body of MyTestClass 159 | """ 160 | 161 | @register 162 | class MyNestedClass(Serializable): 163 | str_prop = String() 164 | int_prop = Integer() 165 | 166 | class MyTestClass(Serializable): 167 | str_prop = String() 168 | nested_list_prop = List() 169 | 170 | obj = MyTestClass() 171 | obj.str_prop = 'object name' 172 | obj.nested_list_prop = [] 173 | 174 | nested_strings = ['some string', 'another string', 'one more string'] 175 | nested_ints = [101, 102, 103] 176 | 177 | for s, n in zip(nested_strings, nested_ints): 178 | temp = MyNestedClass() 179 | temp.str_prop = s 180 | temp.int_prop = n 181 | obj.nested_list_prop.append(temp) 182 | 183 | body = obj.serialize() 184 | 185 | assert body['_type'] == 'test.test_list.MyTestClass' 186 | assert body['str_prop'] == 'object name' 187 | assert len(body['nested_list_prop']) == 3 188 | for i, nested_body in enumerate(body['nested_list_prop']): 189 | assert nested_body['_type'] == 'test.test_list.MyNestedClass' 190 | assert nested_body['str_prop'] == nested_strings[i] 191 | assert nested_body['int_prop'] == nested_ints[i] 192 | 193 | def test_deserialize(self): 194 | """ 195 | test deserialization 196 | 197 | expect list of nested json objects to be deserialized into a list 198 | of MyNestedClass objects that is a member of MyTestClass 199 | """ 200 | 201 | @register 202 | class MyNestedClass(Serializable): 203 | str_prop = String() 204 | int_prop = Integer() 205 | 206 | class MyTestClass(Serializable): 207 | str_prop = String() 208 | nested_list_prop = List() 209 | 210 | body = { 211 | '_type': 'MyTestClass', 212 | 'str_prop': 'really great string property', 213 | 'nested_list_prop': [] 214 | } 215 | nested_strings = ['some string', 'another string', 'one more string'] 216 | nested_ints = [101, 102, 103] 217 | 218 | for s, n in zip(nested_strings, nested_ints): 219 | body['nested_list_prop'].append( 220 | { 221 | '_type': 'MyNestedClass', 222 | 'str_prop': s, 223 | 'int_prop': n 224 | } 225 | ) 226 | 227 | obj = MyTestClass() 228 | obj.deserialize(body) 229 | 230 | assert isinstance(obj, MyTestClass) 231 | assert obj.str_prop == 'really great string property' 232 | assert len(obj.nested_list_prop) == 3 233 | for i, nested_obj in enumerate(obj.nested_list_prop): 234 | assert isinstance(nested_obj, MyNestedClass) 235 | assert nested_obj.str_prop == nested_strings[i] 236 | assert nested_obj.int_prop == nested_ints[i] 237 | 238 | def test_deserialize_typed(self): 239 | """ 240 | test deserialization without _type field 241 | 242 | expect list of nested json objects to be deserialized into a list 243 | of MyNestedClass objects that is a member of MyTestClass, even without 244 | _type field specified 245 | """ 246 | 247 | @register 248 | class MyNestedClass(Serializable): 249 | str_prop = String() 250 | int_prop = Integer() 251 | 252 | class MyTestClass(Serializable): 253 | str_prop = String() 254 | nested_list_prop = List(field_type=MyNestedClass) 255 | 256 | body = { 257 | '_type': 'MyTestClass', 258 | 'str_prop': 'really great string property', 259 | 'nested_list_prop': [] 260 | } 261 | nested_strings = ['some string', 'another string', 'one more string'] 262 | nested_ints = [101, 102, 103] 263 | 264 | for s, n in zip(nested_strings, nested_ints): 265 | body['nested_list_prop'].append( 266 | { 267 | 'str_prop': s, 268 | 'int_prop': n 269 | } 270 | ) 271 | 272 | obj = MyTestClass() 273 | obj.deserialize(body) 274 | 275 | assert isinstance(obj, MyTestClass) 276 | assert obj.str_prop == 'really great string property' 277 | assert len(obj.nested_list_prop) == 3 278 | for i, nested_obj in enumerate(obj.nested_list_prop): 279 | assert isinstance(nested_obj, MyNestedClass) 280 | assert nested_obj.str_prop == nested_strings[i] 281 | assert nested_obj.int_prop == nested_ints[i] 282 | 283 | def test_deserialize_typed_invalid(self): 284 | """ 285 | test deserialization validation 286 | 287 | expect validation error to be raised on invalid nested object type 288 | """ 289 | 290 | @register 291 | class MyNestedClass(Serializable): 292 | str_prop = String() 293 | 294 | @register 295 | class OtherClass(Serializable): 296 | str_prop = String() 297 | 298 | class MyTestClass(Serializable): 299 | str_prop = String() 300 | nested_list_prop = List(field_type=MyNestedClass) 301 | 302 | body = { 303 | '_type': 'MyTestClass', 304 | 'nested_list_prop': [ 305 | { 306 | '_type': 'OtherClass', 307 | 'str_prop': 'some string', 308 | } 309 | ] 310 | } 311 | 312 | obj = MyTestClass() 313 | 314 | with pytest.raises(ValueError): 315 | obj.deserialize(body) 316 | 317 | def test_default_unique(self): 318 | """ 319 | test default nested list field is unique between instances 320 | 321 | expect the default value to be replicated for each instance 322 | of the parent class, to avoid unintentional memory sharing 323 | """ 324 | 325 | @register 326 | class MyNestedClass(Serializable): 327 | str_prop = String() 328 | int_prop = Integer() 329 | 330 | class MyTestClass(Serializable): 331 | str_prop = Integer() 332 | nested_list_prop = List() 333 | 334 | obj_a = MyTestClass() 335 | obj_b = MyTestClass() 336 | 337 | assert len(obj_a.nested_list_prop) == 0 338 | assert len(obj_b.nested_list_prop) == 0 339 | 340 | obj_a.nested_list_prop.append(MyNestedClass.from_kwargs(str_prop='x')) 341 | 342 | assert len(obj_a.nested_list_prop) == 1 343 | assert obj_a.nested_list_prop[0].str_prop == 'x' 344 | assert len(obj_b.nested_list_prop) == 0 345 | 346 | obj_a.nested_list_prop.append(MyNestedClass.from_kwargs(str_prop='y')) 347 | obj_b.nested_list_prop.append(MyNestedClass.from_kwargs(str_prop='z')) 348 | 349 | assert len(obj_a.nested_list_prop) == 2 350 | assert obj_a.nested_list_prop[0].str_prop == 'x' 351 | assert obj_a.nested_list_prop[1].str_prop == 'y' 352 | assert len(obj_b.nested_list_prop) == 1 353 | assert obj_b.nested_list_prop[0].str_prop == 'z' 354 | -------------------------------------------------------------------------------- /test/test_nested.py: -------------------------------------------------------------------------------- 1 | """ 2 | module for testing serializable Nested field 3 | """ 4 | 5 | # lib 6 | import pytest 7 | import marshmallow 8 | 9 | # src 10 | import objectfactory 11 | from objectfactory import Serializable, Nested, String, register 12 | from objectfactory.nested import NestedFactoryField 13 | 14 | 15 | class TestNested(object): 16 | """ 17 | test case for nested object field type 18 | """ 19 | 20 | def setup_method(self, _): 21 | """ 22 | prepare for each test 23 | """ 24 | objectfactory.factory._global_factory.registry.clear() 25 | 26 | def test_definition(self): 27 | """ 28 | test definition of class with nested field 29 | 30 | expect field to be collected, registered, and included 31 | in schema creation 32 | """ 33 | 34 | class MyTestClass(Serializable): 35 | nested = Nested() 36 | 37 | assert isinstance(MyTestClass._fields, dict) 38 | assert len(MyTestClass._fields) == 1 39 | assert 'nested' in MyTestClass._fields 40 | assert isinstance(MyTestClass._fields['nested'], Nested) 41 | 42 | schema = MyTestClass._schema 43 | assert issubclass(schema, marshmallow.Schema) 44 | assert len(schema._declared_fields) == 1 45 | assert 'nested' in schema._declared_fields 46 | 47 | prop = schema._declared_fields['nested'] 48 | assert isinstance(prop, NestedFactoryField) 49 | 50 | def test_serialize(self): 51 | """ 52 | test serialize 53 | 54 | expect nested object to be dumped to nested dict in json body 55 | """ 56 | 57 | class MyNestedClass(Serializable): 58 | str_prop = String() 59 | 60 | class MyTestClass(Serializable): 61 | nested = Nested() 62 | 63 | obj = MyTestClass() 64 | obj.nested = MyNestedClass() 65 | obj.nested.str_prop = 'some string' 66 | 67 | body = obj.serialize() 68 | 69 | assert body['_type'] == 'test.test_nested.MyTestClass' 70 | assert isinstance(body['nested'], dict) 71 | assert body['nested']['_type'] == 'test.test_nested.MyNestedClass' 72 | assert body['nested']['str_prop'] == 'some string' 73 | 74 | def test_deserialize(self): 75 | """ 76 | test deserialization 77 | 78 | expect nested object to be created and data to be loaded 79 | """ 80 | 81 | @register 82 | class MyNestedClass(Serializable): 83 | str_prop = String() 84 | 85 | class MyTestClass(Serializable): 86 | nested = Nested() 87 | 88 | body = { 89 | '_type': 'MyTestClass', 90 | 'nested': { 91 | '_type': 'MyNestedClass', 92 | 'str_prop': 'some string' 93 | } 94 | } 95 | 96 | obj = MyTestClass() 97 | obj.deserialize(body) 98 | 99 | assert isinstance(obj, MyTestClass) 100 | assert isinstance(obj.nested, MyNestedClass) 101 | assert obj.nested.str_prop == 'some string' 102 | 103 | def test_deserialize_typed(self): 104 | """ 105 | test deserialization with typed nested field 106 | 107 | expect nested object to be created and data to be loaded based 108 | on specified field type, despite lack of type info or registration 109 | """ 110 | 111 | class MyNestedClass(Serializable): 112 | str_prop = String() 113 | 114 | class MyTestClass(Serializable): 115 | nested = Nested(field_type=MyNestedClass) 116 | 117 | body = { 118 | '_type': 'MyTestClass', 119 | 'nested': { 120 | 'str_prop': 'some string' 121 | } 122 | } 123 | 124 | obj = MyTestClass() 125 | obj.deserialize(body) 126 | 127 | assert isinstance(obj, MyTestClass) 128 | assert isinstance(obj.nested, MyNestedClass) 129 | assert obj.nested.str_prop == 'some string' 130 | 131 | def test_deserialize_enforce_typed(self): 132 | """ 133 | test deserialization enforcing field type 134 | 135 | expect an error to be thrown on deserialization because the nested 136 | field is of the incorrect type 137 | """ 138 | 139 | @register 140 | class MyNestedClass(Serializable): 141 | str_prop = String() 142 | 143 | @register 144 | class OtherClass(Serializable): 145 | str_prop = String() 146 | 147 | class MyTestClass(Serializable): 148 | nested = Nested(field_type=MyNestedClass) 149 | 150 | body = { 151 | '_type': 'MyTestClass', 152 | 'nested': { 153 | '_type': 'OtherClass', 154 | 'str_prop': 'some string', 155 | } 156 | } 157 | 158 | obj = MyTestClass() 159 | with pytest.raises(ValueError): 160 | obj.deserialize(body) 161 | 162 | def test_serialize_nested_optional(self): 163 | """ 164 | test serialize with nested optional 165 | 166 | expect nested object to be dumped to nested dict under specified key 167 | """ 168 | 169 | class MyNestedClass(Serializable): 170 | str_prop = String(key='string_property') 171 | 172 | class MyTestClass(Serializable): 173 | nested = Nested() 174 | 175 | obj = MyTestClass() 176 | obj.nested = MyNestedClass() 177 | obj.nested.str_prop = 'some string' 178 | 179 | body = obj.serialize() 180 | 181 | assert body['_type'] == 'test.test_nested.MyTestClass' 182 | assert isinstance(body['nested'], dict) 183 | assert body['nested']['_type'] == 'test.test_nested.MyNestedClass' 184 | assert 'str_prop' not in body['nested'] 185 | assert body['nested']['string_property'] == 'some string' 186 | 187 | def test_deserialize_nested_optional(self): 188 | """ 189 | test deserialization with nested optional 190 | 191 | expect nested object to be created and data to be loaded from specified key 192 | """ 193 | 194 | @register 195 | class MyNestedClass(Serializable): 196 | str_prop = String(key='string_property') 197 | 198 | class MyTestClass(Serializable): 199 | nested = Nested() 200 | 201 | body = { 202 | '_type': 'MyTestClass', 203 | 'nested': { 204 | '_type': 'MyNestedClass', 205 | 'string_property': 'some string' 206 | } 207 | } 208 | 209 | obj = MyTestClass() 210 | obj.deserialize(body) 211 | 212 | obj = MyTestClass.from_dict(body) 213 | 214 | assert isinstance(obj, MyTestClass) 215 | assert isinstance(obj.nested, MyNestedClass) 216 | assert obj.nested.str_prop == 'some string' 217 | -------------------------------------------------------------------------------- /test/test_serializable.py: -------------------------------------------------------------------------------- 1 | """ 2 | module for testing functionality of serializable objects 3 | """ 4 | 5 | # lib 6 | import marshmallow 7 | 8 | # src 9 | from objectfactory import Serializable, Field 10 | from .testmodule.testclasses import MyBasicClass, MySubClass 11 | 12 | 13 | class TestClassDefinition(object): 14 | """ 15 | test group for definition of serializable object 16 | """ 17 | 18 | def test_fields_collected(self): 19 | """ 20 | test collection of field descriptors 21 | 22 | expect each field defined in serializable class to be detected 23 | and indexed under the _fields parameter 24 | """ 25 | 26 | class MyClass(Serializable): 27 | some_field = Field() 28 | another_field = Field() 29 | not_a_field = 'some class attribute' 30 | 31 | assert isinstance(MyClass._fields, dict) 32 | assert len(MyClass._fields) == 2 33 | assert 'some_field' in MyClass._fields 34 | assert isinstance(MyClass._fields['some_field'], Field) 35 | assert 'another_field' in MyClass._fields 36 | assert isinstance(MyClass._fields['another_field'], Field) 37 | assert 'not_a_field' not in MyClass._fields 38 | 39 | def test_schema_creation(self): 40 | """ 41 | test creation of marshmallow schema 42 | 43 | expect schema to contain each field defined in serializable class 44 | """ 45 | 46 | class MyClass(Serializable): 47 | some_field = Field() 48 | another_field = Field() 49 | 50 | schema = MyClass._schema 51 | assert issubclass(schema, marshmallow.Schema) 52 | assert len(schema._declared_fields) == 2 53 | assert 'some_field' in schema._declared_fields 54 | assert isinstance(schema._declared_fields['some_field'], marshmallow.fields.Field) 55 | assert 'another_field' in schema._declared_fields 56 | assert isinstance(schema._declared_fields['another_field'], marshmallow.fields.Field) 57 | 58 | 59 | class TestSerializableObject(object): 60 | """ 61 | test group for normal python usage of serializable object 62 | """ 63 | 64 | def test_init_keywords(self): 65 | """ 66 | test initializing class with keywords based on fields 67 | 68 | expect any fields to pass through as a keyword arg to init 69 | """ 70 | obj = MyBasicClass.from_kwargs( 71 | str_prop='some string', 72 | int_prop=12 73 | ) 74 | 75 | assert obj.str_prop == 'some string' 76 | assert obj.int_prop == 12 77 | 78 | def test_init_dictionary(self): 79 | """ 80 | test initializing class with dictionary 81 | 82 | expect any dictionary data fields to pass through to init 83 | """ 84 | obj = MyBasicClass.from_dict( 85 | { 86 | 'str_prop': 'some string', 87 | 'int_prop': 12 88 | } 89 | ) 90 | 91 | assert obj.str_prop == 'some string' 92 | assert obj.int_prop == 12 93 | 94 | 95 | class TestSerialization(object): 96 | """ 97 | test group for serialization of basic object with primitive fields 98 | """ 99 | 100 | def test_serialize(self): 101 | """ 102 | test serialization 103 | """ 104 | obj = MyBasicClass() 105 | obj.str_prop = 'my awesome string' 106 | obj.int_prop = 1234 107 | body = obj.serialize() 108 | 109 | assert body['_type'] == 'test.testmodule.testclasses.MyBasicClass' 110 | assert body['str_prop'] == 'my awesome string' 111 | assert body['int_prop'] == 1234 112 | 113 | def test_deserialize(self): 114 | """ 115 | test deserialization 116 | """ 117 | body = { 118 | '_type': 'MyBasicClass', 119 | 'str_prop': 'another great string', 120 | 'int_prop': 9001 121 | } 122 | 123 | obj = MyBasicClass() 124 | obj.deserialize(body) 125 | 126 | assert isinstance(obj, MyBasicClass) 127 | assert obj.str_prop == 'another great string' 128 | assert obj.int_prop == 9001 129 | 130 | def test_multiple(self): 131 | """ 132 | test deserializing multiple objects of same class 133 | 134 | validate there is no conflict in values of class level descriptors 135 | """ 136 | body1 = { 137 | '_type': 'MyBasicClass', 138 | 'str_prop': 'string1', 139 | 'int_prop': 9001 140 | } 141 | body2 = { 142 | '_type': 'MyBasicClass', 143 | 'str_prop': 'string2', 144 | 'int_prop': 9002 145 | } 146 | obj1 = MyBasicClass() 147 | obj1.deserialize(body1) 148 | obj2 = MyBasicClass() 149 | obj2.deserialize(body2) 150 | 151 | assert isinstance(obj1, MyBasicClass) 152 | assert obj1.str_prop == 'string1' 153 | assert obj1.int_prop == 9001 154 | 155 | assert isinstance(obj2, MyBasicClass) 156 | assert obj2.str_prop == 'string2' 157 | assert obj2.int_prop == 9002 158 | 159 | def test_serialize_short_type(self): 160 | """ 161 | test serialization without fully qualified path 162 | 163 | expect short name to be set as value in type field 164 | """ 165 | obj = MyBasicClass() 166 | obj.str_prop = 'my awesome string' 167 | obj.int_prop = 1234 168 | body = obj.serialize(use_full_type=False) 169 | 170 | assert body['_type'] == 'MyBasicClass' 171 | assert body['str_prop'] == 'my awesome string' 172 | assert body['int_prop'] == 1234 173 | 174 | def test_serialize_no_type(self): 175 | """ 176 | test serialization without type info 177 | 178 | expect _type key to be excluded 179 | """ 180 | obj = MyBasicClass() 181 | obj.str_prop = 'my awesome string' 182 | obj.int_prop = 1234 183 | body = obj.serialize(include_type=False) 184 | 185 | assert '_type' not in body 186 | assert body['str_prop'] == 'my awesome string' 187 | assert body['int_prop'] == 1234 188 | 189 | 190 | class TestSubClass(object): 191 | """ 192 | test group for sub-classing another serializable model 193 | """ 194 | 195 | def test_serialize(self): 196 | """ 197 | test serialization 198 | 199 | expect members of both parent and sub-class to be serialized, _type string 200 | should be MySubClass, and override should not cause conflict 201 | """ 202 | obj = MySubClass() 203 | obj.str_prop = 'parent_class_string' 204 | obj.int_prop = 99 205 | obj.str_prop_sub = 'sub_class_string' 206 | 207 | body = obj.serialize() 208 | 209 | assert body['_type'] == 'test.testmodule.testclasses.MySubClass' 210 | assert body['str_prop'] == 'parent_class_string' 211 | assert body['int_prop'] == 99 212 | assert body['str_prop_sub'] == 'sub_class_string' 213 | 214 | def test_deserialize(self): 215 | """ 216 | test deserialization 217 | 218 | expect both parent and sub-class members to be properly loaded, obj type 219 | should be MySubClass, and override should not cause conflict 220 | """ 221 | body = { 222 | '_type': 'MySubClass', 223 | 'str_prop': 'parent_class_string', 224 | 'int_prop': 99, 225 | 'str_prop_sub': 'sub_class_string' 226 | } 227 | 228 | obj = MySubClass() 229 | obj.deserialize(body) 230 | 231 | assert isinstance(obj, MySubClass) 232 | assert obj.str_prop == 'parent_class_string' 233 | assert obj.int_prop == 99 234 | assert obj.str_prop_sub == 'sub_class_string' 235 | -------------------------------------------------------------------------------- /test/test_string.py: -------------------------------------------------------------------------------- 1 | """ 2 | module for testing functionality of serializable String field 3 | """ 4 | 5 | # lib 6 | import pytest 7 | import marshmallow 8 | 9 | # src 10 | from objectfactory import Serializable, String 11 | 12 | 13 | class TestString(object): 14 | """ 15 | test case for string field type 16 | """ 17 | 18 | def test_definition(self): 19 | """ 20 | test definition of class with string field 21 | 22 | expect field to be collected, registered, and included 23 | in schema creation 24 | """ 25 | 26 | class MyTestClass(Serializable): 27 | str_prop = String() 28 | 29 | assert isinstance(MyTestClass._fields, dict) 30 | assert len(MyTestClass._fields) == 1 31 | assert 'str_prop' in MyTestClass._fields 32 | assert isinstance(MyTestClass._fields['str_prop'], String) 33 | 34 | schema = MyTestClass._schema 35 | assert issubclass(schema, marshmallow.Schema) 36 | assert len(schema._declared_fields) == 1 37 | assert 'str_prop' in schema._declared_fields 38 | 39 | prop = schema._declared_fields['str_prop'] 40 | assert isinstance(prop, marshmallow.fields.String) 41 | 42 | def test_serialize(self): 43 | """ 44 | test serialize 45 | 46 | expect string data to be dumped to json body 47 | """ 48 | 49 | class MyTestClass(Serializable): 50 | str_prop = String() 51 | 52 | obj = MyTestClass() 53 | obj.str_prop = 'some string' 54 | 55 | body = obj.serialize() 56 | 57 | assert body['_type'] == 'test.test_string.MyTestClass' 58 | assert body['str_prop'] == 'some string' 59 | 60 | def test_deserialize(self): 61 | """ 62 | test deserialization 63 | 64 | expect string data to be loaded into class 65 | """ 66 | 67 | class MyTestClass(Serializable): 68 | str_prop = String() 69 | 70 | body = { 71 | '_type': 'MyTestClass', 72 | 'str_prop': 'another string' 73 | } 74 | 75 | obj = MyTestClass() 76 | obj.deserialize(body) 77 | 78 | assert isinstance(obj, MyTestClass) 79 | assert type(obj.str_prop) == str 80 | assert obj.str_prop == 'another string' 81 | 82 | def test_deserialize_invalid(self): 83 | """ 84 | test deserialization validation 85 | 86 | expect validation error to be raised on invalid string data 87 | """ 88 | 89 | class MyTestClass(Serializable): 90 | str_prop = String() 91 | 92 | body = { 93 | '_type': 'MyTestClass', 94 | 'str_prop': 1000 95 | } 96 | 97 | obj = MyTestClass() 98 | with pytest.raises(marshmallow.ValidationError): 99 | obj.deserialize(body) 100 | -------------------------------------------------------------------------------- /test/testmodule/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devinaconley/py-object-factory/13a1fbfcc0982034e2b5364bacca53dc90248549/test/testmodule/__init__.py -------------------------------------------------------------------------------- /test/testmodule/testclasses.py: -------------------------------------------------------------------------------- 1 | """ 2 | module to implement various dummy classes for use during testing 3 | """ 4 | 5 | # src 6 | from objectfactory import register, Serializable, Field, Nested, List 7 | 8 | 9 | @register 10 | class MyBasicClass(Serializable): 11 | """ 12 | basic class to be used for testing serialization 13 | """ 14 | str_prop = Field() 15 | int_prop = Field() 16 | 17 | 18 | @register 19 | class MySubClass(MyBasicClass): 20 | """ 21 | sub class to be used for testing inheritance and serialization 22 | """ 23 | int_prop = Field() 24 | str_prop_sub = Field() 25 | 26 | 27 | @register 28 | class MyComplexClass(Serializable): 29 | """ 30 | complex class to test hierarchical serialization 31 | """ 32 | nested = Nested(MyBasicClass) 33 | prop = Field() 34 | --------------------------------------------------------------------------------