From 4234780aa727a5cec121cf650200d0b4ac74e500 Mon Sep 17 00:00:00 2001 From: anikolaienko Date: Wed, 5 Jan 2022 13:57:55 +0200 Subject: [PATCH 1/4] improving documentation --- README.md | 72 +++++++++++++++++++++++++++++++++++++++++++++++------- logo.png | Bin 0 -> 35951 bytes 2 files changed, 63 insertions(+), 9 deletions(-) create mode 100644 logo.png diff --git a/README.md b/README.md index c8c611c..f4683dd 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,77 @@ + + # py-automapper -Python object auto mapper -Current mapper can be useful for multilayer architecture which requires constant mapping between objects from separate layers (data layer, presentation layer, etc). +**Version** +1.1.0 + +**Author** +anikolaienko + +**Copyright** +anikolaienko + +**License** +The MIT License (MIT) + +**Last updated** +5 Jan 2022 + +**Package Download** +https://pypi.python.org/pypi/py-automapper + +**Build Status** +TODO + +--- + +## Versions +Check [CHANGELOG.md](/CHANGELOG.md) + +## About + +**Python auto mapper** is useful for multilayer architecture which requires constant mapping between objects from separate layers (data layer, presentation layer, etc). -For more information read the [documentation](https://anikolaienko.github.io/py-automapper). +Inspired by: [object-mapper](https://github.com/marazt/object-mapper) -## Usage example: +The major advantage of py-automapper is its extensibility, that allows it to map practically any type, discover custom class fields and customize mapping rules. Read more in [documentation](https://anikolaienko.github.io/py-automapper). + +## Usage +Install package: +```bash +pip install py-automapper +``` + +Simple mapping: ```python from automapper import mapper -# Add automatic mappings +class SourceClass: + name: str + age: int + profession: str + +class TargetClass: + name: str + age: int + +# Register mapping mapper.add(SourceClass, TargetClass) -# Map object of SourceClass to output object of TargetClass -mapper.map(obj) +source_obj = SourceClass("Andrii", 30, "software developer") -# Map object to AnotherTargetClass not added to mapping collection -mapper.to(AnotherTargetClass).map(obj) +# Map object +target_obj = mapper.map(obj) +print(target_obj) +``` + +One time mapping without registering in mapper: +```python +target_obj = mapper.to(TargetClass).map(source_obj) +``` + +```python # Override specific fields or provide missing ones mapper.map(obj, field1=value1, field2=value2) diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..e8991770498badf9463cf1b8338be15ed36751ef GIT binary patch literal 35951 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 literal 0 HcmV?d00001 From d9e223c052b3d86d6ec11369286168ca0ff57377 Mon Sep 17 00:00:00 2001 From: anikolaienko Date: Wed, 5 Jan 2022 16:15:16 +0200 Subject: [PATCH 2/4] release ready functionality --- CHANGELOG.md | 3 + README.md | 164 +++++++++++++++++++++++++++++++++---------- automapper/mapper.py | 29 ++++++-- docs/index.md | 8 +-- poetry.lock | 62 +++++++++++++++- pyproject.toml | 5 +- 6 files changed, 217 insertions(+), 54 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9aa967a..5c221d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,2 +1,5 @@ +1.0.0 - 2022/01/05 +* Finalized documentation, fixed defects + 0.0.1 - 2021/06/23 * Initial version \ No newline at end of file diff --git a/README.md b/README.md index f4683dd..18b0a35 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # py-automapper **Version** -1.1.0 +1.0.0 **Author** anikolaienko @@ -47,13 +47,15 @@ Simple mapping: from automapper import mapper class SourceClass: - name: str - age: int - profession: str + def __init__(self, name: str, age: int, profession: str): + self.name = name + self.age = age + self.profession = profession class TargetClass: - name: str - age: int + def __init__(self, name: str, age: int): + self.name = name + self.age = age # Register mapping mapper.add(SourceClass, TargetClass) @@ -61,57 +63,141 @@ mapper.add(SourceClass, TargetClass) source_obj = SourceClass("Andrii", 30, "software developer") # Map object -target_obj = mapper.map(obj) +target_obj = mapper.map(source_obj) -print(target_obj) +# or one time mapping without registering in mapper +target_obj = mapper.to(TargetClass).map(source_obj) + +print(f"Name: {target_obj.name}; Age: {target_obj.age}; has profession: {hasattr(target_obj, 'profession')}") + +# Output: +# Name: Andrii; age: 30; has profession: False ``` -One time mapping without registering in mapper: +## Override fields +If you want to override some field and/or add mapping for field not existing in SourceClass: ```python -target_obj = mapper.to(TargetClass).map(source_obj) +from typing import List +from automapper import mapper + +class SourceClass: + def __init__(self, name: str, age: int): + self.name = name + self.age = age + +class TargetClass: + def __init__(self, name: str, age: int, hobbies: List[str]): + self.name = name + self.age = age + self.hobbies = hobbies + +mapper.add(SourceClass, TargetClass) + +source_obj = SourceClass("Andrii", 30) +hobbies = ["Diving", "Languages", "Sports"] + +# Override `age` and provide missing field `hobbies` +target_obj = mapper.map(source_obj, age=25, hobbies=hobbies) + +print(f"Name: {target_obj.name}; Age: {target_obj.age}; hobbies: {target_obj.hobbies}") +# Output: +# Name: Andrii; Age: 25; hobbies: ['Diving', 'Languages', 'Sports'] + +# Modifying initial `hobbies` object will not modify `target_obj` +hobbies.pop() + +print(f"Hobbies: {hobbies}") +print(f"Target hobbies: {target_obj.hobbies}") + +# Output: +# Hobbies: ['Diving', 'Languages'] +# Target hobbies: ['Diving', 'Languages', 'Sports'] ``` +## Extensions +`py-automapper` has few predefined extensions for mapping to classes for frameworks: +* [FastAPI](https://github.com/tiangolo/fastapi) and [Pydantic](https://github.com/samuelcolvin/pydantic) +* [TortoiseORM](https://github.com/tortoise/tortoise-orm) + +When you first time import `mapper` from `automapper` it checks default extensions and if modules are found for these extensions, then they will be automatically loaded for default `mapper` object. + +What does extension do? To know what fields in Target class are available for mapping `py-automapper` need to extract the list of these fields. There is no generic way to do that for all Python objects. For this purpose `py-automapper` uses extensions. + +List of default extensions can be found in [/automapper/extensions](/automapper/extensions) folder. You can take a look how it's done for a class with `__init__` method or for Pydantic or TortoiseORM models. + +You can create your own extension and register in `mapper`: ```python -# Override specific fields or provide missing ones -mapper.map(obj, field1=value1, field2=value2) +from automapper import mapper -# Don't map None values to target object -mapper.map(obj, skip_none_values = True) +class TargetClass: + def __init__(self, **kwargs): + self.name = kwargs["name"] + self.age = kwargs["age"] + + @staticmethod + def get_fields(cls): + return ["name", "age"] + +source_obj = {"name": "Andrii", "age": 30} + +try: + # Map object + target_obj = mapper.to(TargetClass).map(source_obj) +except Exception as e: + print(f"Exception: {repr(e)}") + # Output: + # Exception: KeyError('name') + + # mapper could not find list of fields from BaseClass + # let's register extension for class BaseClass and all inherited ones + mapper.add_spec(TargetClass, TargetClass.get_fields) + target_obj = mapper.to(TargetClass).map(source_obj) + + print(f"Name: {target_obj.name}; Age: {target_obj.age}") ``` -## Advanced features +You can also create your own clean Mapper without any extensions and define extension for very specific classes, e.g. if class accepts `kwargs` parameter in `__init__` method and you want to copy only specific fields. Next example is a bit complex but probably rarely will be needed: ```python +from typing import Type, TypeVar + from automapper import Mapper # Create your own Mapper object without any predefined extensions mapper = Mapper() -# Add your own extension for extracting list of fields from class -# for all classes inherited from base class -mapper.add_spec( - BaseClass, - lambda child_class: child_class.get_fields_function() -) - -# Add your own extension for extracting list of fields from class -# for all classes that can be identified in verification function -mapper.add_spec( - lambda cls: hasattr(cls, "get_fields_function"), - lambda cls: cls.get_fields_function() -) -``` -For more information about extensions check out existing extensions in `automapper/extensions` folder +class TargetClass: + def __init__(self, **kwargs): + self.data = kwargs.copy() -## Not yet implemented features -```python + @classmethod + def fields(cls): + return ["name", "age", "profession"] -# TODO: multiple from classes -mapper.add(FromClassA, FromClassB, ToClassC) +source_obj = {"name": "Andrii", "age": 30, "profession": None} -# TODO: add custom mappings for fields -mapper.add(ClassA, ClassB, {"Afield1": "Bfield1", "Afield2": "Bfield2"}) +try: + target_obj = mapper.to(TargetClass).map(source_obj) +except Exception as e: + print(f"Exception: {repr(e)}") + # Output: + # Exception: MappingError("No spec function is added for base class of ") -# TODO: Advanced: map multiple objects to output type -mapper.multimap(obj1, obj2) -mapper.to(TargetType).multimap(obj1, obj2) +# Instead of using base class, we define spec for all classes that have `fields` property +T = TypeVar("T") + +def class_has_fields_property(target_cls: Type[T]) -> bool: + return callable(getattr(target_cls, "fields", None)) + +mapper.add_spec(class_has_fields_property, lambda t: getattr(t, "fields")()) + +target_obj = mapper.to(TargetClass).map(source_obj) +print(f"Name: {target_obj.data['name']}; Age: {target_obj.data['age']}; Profession: {target_obj.data['profession']}") +# Output: +# Name: Andrii; Age: 30; Profession: None + +# Skip `None` value +target_obj = mapper.to(TargetClass).map(source_obj, skip_none_values=True) +print(f"Name: {target_obj.data['name']}; Age: {target_obj.data['age']}; Has profession: {hasattr(target_obj, 'profession')}") +# Output: +# Name: Andrii; Age: 30; Has profession: False ``` diff --git a/automapper/mapper.py b/automapper/mapper.py index 4bd71d3..331375d 100644 --- a/automapper/mapper.py +++ b/automapper/mapper.py @@ -97,22 +97,32 @@ def add_spec( else: raise ValueError("Incorrect type of the classifier argument") - def add(self, source_cls: Type[S], target_cls: Type[T]) -> None: - """Adds mapping between object of source class to form an object of target class""" - if source_cls in self._mappings: + def add(self, source_cls: Type[S], target_cls: Type[T], override: bool = False) -> None: + """Adds mapping between object of `source class` to an object of `target class`. + + Parameters + ---------- + source_cls : Type + Source class to map from + target_cls : Type + Target class to map to + override : bool, optional + Override existing `source class` mapping to use new `target class` + """ + if source_cls in self._mappings and not override: raise DuplicatedRegistrationError( f"source_cls {source_cls} was already added for mapping" ) self._mappings[source_cls] = target_cls - def map(self, obj: object, skip_none_values: bool = False) -> T: + def map(self, obj: object, skip_none_values: bool = False, **kwargs: Any) -> T: """Produces output object mapped from source object and custom arguments""" obj_type = type(obj) if obj_type not in self._mappings: raise MappingError(f"Missing mapping type for input type {obj_type}") return self._map_common( - obj, self._mappings[obj_type], set(), skip_none_values=skip_none_values + obj, self._mappings[obj_type], set(), skip_none_values=skip_none_values, **kwargs ) def _get_fields(self, target_cls: Type[T]) -> Iterable[str]: @@ -191,8 +201,13 @@ def _map_common( mapped_values: Dict[str, Any] = {} for field_name in target_cls_fields: - if field_name in kwargs or hasattr(obj, field_name): - value = kwargs[field_name] if field_name in kwargs else getattr(obj, field_name) + if field_name in kwargs or hasattr(obj, field_name) or field_name in obj: + if field_name in kwargs: + value = kwargs[field_name] + elif hasattr(obj, field_name): + value = getattr(obj, field_name) + else: + value = obj[field_name] if value is not None: mapped_values[field_name] = self._map_subobject( diff --git a/docs/index.md b/docs/index.md index fa85322..ad40eb5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,11 +1,9 @@ -## Py-automapper +## py-automapper The idea came from a need on the project to map types of "different nature" between each other. Namely, if we take TortoiseORM Model class and Python dataclass there's no, known to me, single method to retrieve list of fields required by the class. Current autommapper allows to create mappings between these two classes and allows to write extensions for supporting other classes as well. -For now check out [README.md](https://github.com/anikolaienko/py-automapper) file in the repository for more information +For now check out [README.md](https://github.com/anikolaienko/py-automapper) for documentation. -For more details see [GitHub Flavored Markdown](https://guides.github.com/features/mastering-markdown/). - -### Support or Contact +### Support/Contribute If you have any questions or ideas check out existing issues list or create a new one on [Issues page](https://github.com/anikolaienko/py-automapper/issues). diff --git a/poetry.lock b/poetry.lock index 6ba5bc9..8251a3e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -81,6 +81,17 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "coverage" +version = "6.2" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.extras] +toml = ["tomli"] + [[package]] name = "flake8" version = "3.9.2" @@ -347,7 +358,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "2e976363d99e3f342a3252fd6b348b9b18790b9f18468e777d4df6da8a5d7e24" +content-hash = "b95c90cac364b4c6a6cd6197056c97d5adeb64495fc6a5c5216b92509e0d5a7f" [metadata.files] aiosqlite = [ @@ -377,6 +388,55 @@ colorama = [ {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, ] +coverage = [ + {file = "coverage-6.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6dbc1536e105adda7a6312c778f15aaabe583b0e9a0b0a324990334fd458c94b"}, + {file = "coverage-6.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:174cf9b4bef0db2e8244f82059a5a72bd47e1d40e71c68ab055425172b16b7d0"}, + {file = "coverage-6.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:92b8c845527eae547a2a6617d336adc56394050c3ed8a6918683646328fbb6da"}, + {file = "coverage-6.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c7912d1526299cb04c88288e148c6c87c0df600eca76efd99d84396cfe00ef1d"}, + {file = "coverage-6.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d5d2033d5db1d58ae2d62f095e1aefb6988af65b4b12cb8987af409587cc0739"}, + {file = "coverage-6.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:3feac4084291642165c3a0d9eaebedf19ffa505016c4d3db15bfe235718d4971"}, + {file = "coverage-6.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:276651978c94a8c5672ea60a2656e95a3cce2a3f31e9fb2d5ebd4c215d095840"}, + {file = "coverage-6.2-cp310-cp310-win32.whl", hash = "sha256:f506af4f27def639ba45789fa6fde45f9a217da0be05f8910458e4557eed020c"}, + {file = "coverage-6.2-cp310-cp310-win_amd64.whl", hash = "sha256:3f7c17209eef285c86f819ff04a6d4cbee9b33ef05cbcaae4c0b4e8e06b3ec8f"}, + {file = "coverage-6.2-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:13362889b2d46e8d9f97c421539c97c963e34031ab0cb89e8ca83a10cc71ac76"}, + {file = "coverage-6.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:22e60a3ca5acba37d1d4a2ee66e051f5b0e1b9ac950b5b0cf4aa5366eda41d47"}, + {file = "coverage-6.2-cp311-cp311-win_amd64.whl", hash = "sha256:b637c57fdb8be84e91fac60d9325a66a5981f8086c954ea2772efe28425eaf64"}, + {file = "coverage-6.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f467bbb837691ab5a8ca359199d3429a11a01e6dfb3d9dcc676dc035ca93c0a9"}, + {file = "coverage-6.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2641f803ee9f95b1f387f3e8f3bf28d83d9b69a39e9911e5bfee832bea75240d"}, + {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1219d760ccfafc03c0822ae2e06e3b1248a8e6d1a70928966bafc6838d3c9e48"}, + {file = "coverage-6.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9a2b5b52be0a8626fcbffd7e689781bf8c2ac01613e77feda93d96184949a98e"}, + {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8e2c35a4c1f269704e90888e56f794e2d9c0262fb0c1b1c8c4ee44d9b9e77b5d"}, + {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5d6b09c972ce9200264c35a1d53d43ca55ef61836d9ec60f0d44273a31aa9f17"}, + {file = "coverage-6.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:e3db840a4dee542e37e09f30859f1612da90e1c5239a6a2498c473183a50e781"}, + {file = "coverage-6.2-cp36-cp36m-win32.whl", hash = "sha256:4e547122ca2d244f7c090fe3f4b5a5861255ff66b7ab6d98f44a0222aaf8671a"}, + {file = "coverage-6.2-cp36-cp36m-win_amd64.whl", hash = "sha256:01774a2c2c729619760320270e42cd9e797427ecfddd32c2a7b639cdc481f3c0"}, + {file = "coverage-6.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb8b8ee99b3fffe4fd86f4c81b35a6bf7e4462cba019997af2fe679365db0c49"}, + {file = "coverage-6.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:619346d57c7126ae49ac95b11b0dc8e36c1dd49d148477461bb66c8cf13bb521"}, + {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0a7726f74ff63f41e95ed3a89fef002916c828bb5fcae83b505b49d81a066884"}, + {file = "coverage-6.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cfd9386c1d6f13b37e05a91a8583e802f8059bebfccde61a418c5808dea6bbfa"}, + {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:17e6c11038d4ed6e8af1407d9e89a2904d573be29d51515f14262d7f10ef0a64"}, + {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c254b03032d5a06de049ce8bca8338a5185f07fb76600afff3c161e053d88617"}, + {file = "coverage-6.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:dca38a21e4423f3edb821292e97cec7ad38086f84313462098568baedf4331f8"}, + {file = "coverage-6.2-cp37-cp37m-win32.whl", hash = "sha256:600617008aa82032ddeace2535626d1bc212dfff32b43989539deda63b3f36e4"}, + {file = "coverage-6.2-cp37-cp37m-win_amd64.whl", hash = "sha256:bf154ba7ee2fd613eb541c2bc03d3d9ac667080a737449d1a3fb342740eb1a74"}, + {file = "coverage-6.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f9afb5b746781fc2abce26193d1c817b7eb0e11459510fba65d2bd77fe161d9e"}, + {file = "coverage-6.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:edcada2e24ed68f019175c2b2af2a8b481d3d084798b8c20d15d34f5c733fa58"}, + {file = "coverage-6.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9c8c4283e17690ff1a7427123ffb428ad6a52ed720d550e299e8291e33184dc"}, + {file = "coverage-6.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f614fc9956d76d8a88a88bb41ddc12709caa755666f580af3a688899721efecd"}, + {file = "coverage-6.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9365ed5cce5d0cf2c10afc6add145c5037d3148585b8ae0e77cc1efdd6aa2953"}, + {file = "coverage-6.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8bdfe9ff3a4ea37d17f172ac0dff1e1c383aec17a636b9b35906babc9f0f5475"}, + {file = "coverage-6.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:63c424e6f5b4ab1cf1e23a43b12f542b0ec2e54f99ec9f11b75382152981df57"}, + {file = "coverage-6.2-cp38-cp38-win32.whl", hash = "sha256:49dbff64961bc9bdd2289a2bda6a3a5a331964ba5497f694e2cbd540d656dc1c"}, + {file = "coverage-6.2-cp38-cp38-win_amd64.whl", hash = "sha256:9a29311bd6429be317c1f3fe4bc06c4c5ee45e2fa61b2a19d4d1d6111cb94af2"}, + {file = "coverage-6.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:03b20e52b7d31be571c9c06b74746746d4eb82fc260e594dc662ed48145e9efd"}, + {file = "coverage-6.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:215f8afcc02a24c2d9a10d3790b21054b58d71f4b3c6f055d4bb1b15cecce685"}, + {file = "coverage-6.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a4bdeb0a52d1d04123b41d90a4390b096f3ef38eee35e11f0b22c2d031222c6c"}, + {file = "coverage-6.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c332d8f8d448ded473b97fefe4a0983265af21917d8b0cdcb8bb06b2afe632c3"}, + {file = "coverage-6.2-cp39-cp39-win32.whl", hash = "sha256:6e1394d24d5938e561fbeaa0cd3d356207579c28bd1792f25a068743f2d5b282"}, + {file = "coverage-6.2-cp39-cp39-win_amd64.whl", hash = "sha256:86f2e78b1eff847609b1ca8050c9e1fa3bd44ce755b2ec30e70f2d3ba3844644"}, + {file = "coverage-6.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de"}, + {file = "coverage-6.2.tar.gz", hash = "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8"}, +] flake8 = [ {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, diff --git a/pyproject.toml b/pyproject.toml index cfb67bf..609bc91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "py-automapper" -version = "0.0.1" +version = "1.0.0" description = "Library for automatically mapping one object to another" authors = ["Andrii Nikolaienko "] license = "MIT" @@ -14,7 +14,7 @@ packages = [ include = ["CHANGELOG.md", "LICENSE", "README.md"] classifiers = [ "License :: OSI Approved :: MIT License", - "Development Status :: 2 - Pre-Alpha", + "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", @@ -36,6 +36,7 @@ black = "^20.8b1" mypy = "^0.910" tortoise-orm = "^0.17.4" pydantic = "^1.8.2" +coverage = "^6.2" [build-system] requires = ["poetry_core>=1.0.0"] From 67012ece0bde2b81a7260a8121924612b21fda75 Mon Sep 17 00:00:00 2001 From: anikolaienko Date: Wed, 5 Jan 2022 16:43:37 +0200 Subject: [PATCH 3/4] using black, flake8 and mypy --- automapper/__init__.py | 6 ++- automapper/exceptions.py | 4 +- automapper/extensions/default.py | 4 +- automapper/mapper.py | 67 ++++++++++++++++++++++++++------ 4 files changed, 66 insertions(+), 15 deletions(-) diff --git a/automapper/__init__.py b/automapper/__init__.py index 29cfafb..ad75b9e 100644 --- a/automapper/__init__.py +++ b/automapper/__init__.py @@ -1,7 +1,11 @@ # flake8: noqa: F401 from .mapper import Mapper -from .exceptions import DuplicatedRegistrationError, MappingError, CircularReferenceError +from .exceptions import ( + DuplicatedRegistrationError, + MappingError, + CircularReferenceError, +) from .mapper_initializer import create_mapper diff --git a/automapper/exceptions.py b/automapper/exceptions.py index c041757..b2d6595 100644 --- a/automapper/exceptions.py +++ b/automapper/exceptions.py @@ -8,4 +8,6 @@ class MappingError(Exception): class CircularReferenceError(Exception): def __init__(self, *args: object) -> None: - super().__init__("Mapper does not support objects with circular references yet", *args) + super().__init__( + "Mapper does not support objects with circular references yet", *args + ) diff --git a/automapper/extensions/default.py b/automapper/extensions/default.py index 95eaf71..d954a6a 100644 --- a/automapper/extensions/default.py +++ b/automapper/extensions/default.py @@ -12,7 +12,9 @@ def __init_method_classifier__(target_cls: Type[T]) -> bool: return ( hasattr(target_cls, "__init__") and hasattr(getattr(target_cls, "__init__"), "__annotations__") - and isinstance(getattr(getattr(target_cls, "__init__"), "__annotations__"), dict) + and isinstance( + getattr(getattr(target_cls, "__init__"), "__annotations__"), dict + ) ) diff --git a/automapper/mapper.py b/automapper/mapper.py index 331375d..5e0721a 100644 --- a/automapper/mapper.py +++ b/automapper/mapper.py @@ -1,8 +1,24 @@ -from typing import Any, Union, Type, TypeVar, Dict, Set, Callable, Iterable, Generic, overload, cast +from typing import ( + Any, + Union, + Type, + TypeVar, + Dict, + Set, + Callable, + Iterable, + Generic, + overload, + cast, +) from copy import deepcopy import inspect -from .exceptions import CircularReferenceError, DuplicatedRegistrationError, MappingError +from .exceptions import ( + CircularReferenceError, + DuplicatedRegistrationError, + MappingError, +) # Custom Types S = TypeVar("S") @@ -14,10 +30,15 @@ def is_sequence(obj: Any) -> bool: - """Check if object is iteratable""" + """Check if object implements `__iter__` method""" return hasattr(obj, "__iter__") +def is_subscriptable(obj: Any) -> bool: + """Check if object implements `__get_item__` method""" + return hasattr(obj, "__get_item__") + + def is_primitive(obj: Any) -> bool: """Check if object type is primitive""" return type(obj) in __PRIMITIVE_TYPES @@ -68,7 +89,9 @@ def add_spec(self, classifier: Type[T], spec_func: SpecFunction[T]) -> None: ... @overload - def add_spec(self, classifier: ClassifierFunction[T], spec_func: SpecFunction[T]) -> None: + def add_spec( + self, classifier: ClassifierFunction[T], spec_func: SpecFunction[T] + ) -> None: """Add a spec function for all classes identified by classifier function. Parameters: @@ -80,7 +103,9 @@ def add_spec(self, classifier: ClassifierFunction[T], spec_func: SpecFunction[T] ... def add_spec( - self, classifier: Union[Type[T], ClassifierFunction[T]], spec_func: SpecFunction[T] + self, + classifier: Union[Type[T], ClassifierFunction[T]], + spec_func: SpecFunction[T], ) -> None: if inspect.isclass(classifier): if classifier in self._class_specs: @@ -97,7 +122,9 @@ def add_spec( else: raise ValueError("Incorrect type of the classifier argument") - def add(self, source_cls: Type[S], target_cls: Type[T], override: bool = False) -> None: + def add( + self, source_cls: Type[S], target_cls: Type[T], override: bool = False + ) -> None: """Adds mapping between object of `source class` to an object of `target class`. Parameters @@ -122,7 +149,11 @@ def map(self, obj: object, skip_none_values: bool = False, **kwargs: Any) -> T: raise MappingError(f"Missing mapping type for input type {obj_type}") return self._map_common( - obj, self._mappings[obj_type], set(), skip_none_values=skip_none_values, **kwargs + obj, + self._mappings[obj_type], + set(), + skip_none_values=skip_none_values, + **kwargs, ) def _get_fields(self, target_cls: Type[T]) -> Iterable[str]: @@ -135,7 +166,9 @@ def _get_fields(self, target_cls: Type[T]) -> Iterable[str]: if classifier(target_cls): return self._classifier_specs[classifier](target_cls) - raise MappingError(f"No spec function is added for base class of {type(target_cls)}") + raise MappingError( + f"No spec function is added for base class of {type(target_cls)}" + ) def _map_subobject( self, obj: S, _visited_stack: Set[int], skip_none_values: bool = False @@ -150,7 +183,10 @@ def _map_subobject( if type(obj) in self._mappings: result = self._map_common( - obj, self._mappings[type(obj)], _visited_stack, skip_none_values=skip_none_values + obj, + self._mappings[type(obj)], + _visited_stack, + skip_none_values=skip_none_values, ) else: _visited_stack.add(obj_id) @@ -158,7 +194,9 @@ def _map_subobject( if is_sequence(obj): if isinstance(obj, dict): result = { - k: self._map_subobject(v, _visited_stack, skip_none_values=skip_none_values) + k: self._map_subobject( + v, _visited_stack, skip_none_values=skip_none_values + ) for k, v in obj } else: @@ -200,14 +238,19 @@ def _map_common( target_cls_fields = self._get_fields(target_cls) mapped_values: Dict[str, Any] = {} + is_obj_subscriptable = is_subscriptable(obj) for field_name in target_cls_fields: - if field_name in kwargs or hasattr(obj, field_name) or field_name in obj: + if ( + field_name in kwargs + or hasattr(obj, field_name) + or (is_obj_subscriptable and field_name in obj) # type: ignore [operator] + ): if field_name in kwargs: value = kwargs[field_name] elif hasattr(obj, field_name): value = getattr(obj, field_name) else: - value = obj[field_name] + value = obj[field_name] # type: ignore [index] if value is not None: mapped_values[field_name] = self._map_subobject( From a9868bc3432d54eb59d5a965a4cd5e32668c9d65 Mon Sep 17 00:00:00 2001 From: anikolaienko Date: Wed, 5 Jan 2022 16:48:58 +0200 Subject: [PATCH 4/4] using pre-commit --- .pre-commit-config.yaml | 26 +++++ poetry.lock | 184 +++++++++++++++++++++++++++++++++- pyproject.toml | 1 + tests/test_automapper.py | 16 ++- tests/test_for_complex_obj.py | 8 +- 5 files changed, 228 insertions(+), 7 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..88c3940 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,26 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: local + hooks: + - id: black + name: black + description: "Black: The uncompromising Python code formatter" + entry: black + language: python + require_serial: true + types_or: [python, pyi] + - id: flake8 + name: flake8 + description: '`flake8` is a command-line utility for enforcing style consistency across Python projects.' + entry: flake8 + language: python + types: [python] + require_serial: true + - id: mypy + name: mypy + description: 'Mypy is a static type checker for Python 3' + entry: mypy + language: python + types: [python] + require_serial: true diff --git a/poetry.lock b/poetry.lock index 8251a3e..8dcdd1e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -61,6 +61,14 @@ typing-extensions = ">=3.7.4" colorama = ["colorama (>=0.4.3)"] d = ["aiohttp (>=3.3.2)", "aiohttp-cors"] +[[package]] +name = "cfgv" +version = "3.3.1" +description = "Validate configuration and produce human readable error messages." +category = "dev" +optional = false +python-versions = ">=3.6.1" + [[package]] name = "click" version = "8.0.1" @@ -92,6 +100,26 @@ python-versions = ">=3.6" [package.extras] toml = ["tomli"] +[[package]] +name = "distlib" +version = "0.3.4" +description = "Distribution utilities" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "filelock" +version = "3.4.2" +description = "A platform independent file lock." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] +testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] + [[package]] name = "flake8" version = "3.9.2" @@ -106,6 +134,17 @@ mccabe = ">=0.6.0,<0.7.0" pycodestyle = ">=2.7.0,<2.8.0" pyflakes = ">=2.3.0,<2.4.0" +[[package]] +name = "identify" +version = "2.4.1" +description = "File identification library for Python" +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[package.extras] +license = ["ukkonen"] + [[package]] name = "importlib-metadata" version = "4.5.0" @@ -172,6 +211,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "nodeenv" +version = "1.6.0" +description = "Node.js virtual environment builder" +category = "dev" +optional = false +python-versions = "*" + [[package]] name = "packaging" version = "20.9" @@ -191,6 +238,18 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +[[package]] +name = "platformdirs" +version = "2.4.1" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] + [[package]] name = "pluggy" version = "0.13.1" @@ -205,6 +264,23 @@ importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] dev = ["pre-commit", "tox"] +[[package]] +name = "pre-commit" +version = "2.16.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +category = "dev" +optional = false +python-versions = ">=3.6.1" + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +toml = "*" +virtualenv = ">=20.0.8" + [[package]] name = "py" version = "1.10.0" @@ -290,6 +366,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "pyyaml" +version = "6.0" +description = "YAML parser and emitter for Python" +category = "dev" +optional = false +python-versions = ">=3.6" + [[package]] name = "regex" version = "2021.4.4" @@ -298,6 +382,14 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + [[package]] name = "toml" version = "0.10.2" @@ -343,6 +435,25 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "virtualenv" +version = "20.13.0" +description = "Virtual Python Environment builder" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +distlib = ">=0.3.1,<1" +filelock = ">=3.2,<4" +importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +platformdirs = ">=2,<3" +six = ">=1.9.0,<2" + +[package.extras] +docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] +testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] + [[package]] name = "zipp" version = "3.4.1" @@ -358,7 +469,7 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=1.2.3)", "pytest-flake8", "pyt [metadata] lock-version = "1.1" python-versions = "^3.7" -content-hash = "b95c90cac364b4c6a6cd6197056c97d5adeb64495fc6a5c5216b92509e0d5a7f" +content-hash = "0022e88d8941d6f78ad0ceaabf3f24b57d5447fd037b39f3ca121df3eb35826b" [metadata.files] aiosqlite = [ @@ -380,6 +491,10 @@ attrs = [ black = [ {file = "black-20.8b1.tar.gz", hash = "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"}, ] +cfgv = [ + {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, + {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, +] click = [ {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, @@ -437,10 +552,22 @@ coverage = [ {file = "coverage-6.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:5829192582c0ec8ca4a2532407bc14c2f338d9878a10442f5d03804a95fac9de"}, {file = "coverage-6.2.tar.gz", hash = "sha256:e2cad8093172b7d1595b4ad66f24270808658e11acf43a8f95b41276162eb5b8"}, ] +distlib = [ + {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, + {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, +] +filelock = [ + {file = "filelock-3.4.2-py3-none-any.whl", hash = "sha256:cf0fc6a2f8d26bd900f19bf33915ca70ba4dd8c56903eeb14e1e7a2fd7590146"}, + {file = "filelock-3.4.2.tar.gz", hash = "sha256:38b4f4c989f9d06d44524df1b24bd19e167d851f19b50bf3e3559952dddc5b80"}, +] flake8 = [ {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, ] +identify = [ + {file = "identify-2.4.1-py2.py3-none-any.whl", hash = "sha256:0192893ff68b03d37fed553e261d4a22f94ea974093aefb33b29df2ff35fed3c"}, + {file = "identify-2.4.1.tar.gz", hash = "sha256:64d4885e539f505dd8ffb5e93c142a1db45480452b1594cacd3e91dca9a984e9"}, +] importlib-metadata = [ {file = "importlib_metadata-4.5.0-py3-none-any.whl", hash = "sha256:833b26fb89d5de469b24a390e9df088d4e52e4ba33b01dc5e0e4f41b81a16c00"}, {file = "importlib_metadata-4.5.0.tar.gz", hash = "sha256:b142cc1dd1342f31ff04bb7d022492b09920cb64fed867cd3ea6f80fe3ebd139"}, @@ -486,6 +613,10 @@ mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, ] +nodeenv = [ + {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, + {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, +] packaging = [ {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, @@ -494,10 +625,18 @@ pathspec = [ {file = "pathspec-0.8.1-py2.py3-none-any.whl", hash = "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"}, {file = "pathspec-0.8.1.tar.gz", hash = "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd"}, ] +platformdirs = [ + {file = "platformdirs-2.4.1-py3-none-any.whl", hash = "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca"}, + {file = "platformdirs-2.4.1.tar.gz", hash = "sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda"}, +] pluggy = [ {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, ] +pre-commit = [ + {file = "pre_commit-2.16.0-py2.py3-none-any.whl", hash = "sha256:758d1dc9b62c2ed8881585c254976d66eae0889919ab9b859064fc2fe3c7743e"}, + {file = "pre_commit-2.16.0.tar.gz", hash = "sha256:fe9897cac830aa7164dbd02a4e7b90cae49630451ce88464bca73db486ba9f65"}, +] py = [ {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, @@ -550,6 +689,41 @@ pytz = [ {file = "pytz-2021.1-py2.py3-none-any.whl", hash = "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798"}, {file = "pytz-2021.1.tar.gz", hash = "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da"}, ] +pyyaml = [ + {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, + {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, + {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, + {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, + {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, + {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, + {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, + {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, + {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, + {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, + {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, + {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, + {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, + {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, + {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, + {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, + {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, + {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, + {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, + {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, + {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, +] regex = [ {file = "regex-2021.4.4-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000"}, {file = "regex-2021.4.4-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:47bf5bf60cf04d72bf6055ae5927a0bd9016096bf3d742fa50d9bf9f45aa0711"}, @@ -593,6 +767,10 @@ regex = [ {file = "regex-2021.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:97f29f57d5b84e73fbaf99ab3e26134e6687348e95ef6b48cfd2c06807005a07"}, {file = "regex-2021.4.4.tar.gz", hash = "sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb"}, ] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] toml = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, @@ -638,6 +816,10 @@ typing-extensions = [ {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, ] +virtualenv = [ + {file = "virtualenv-20.13.0-py2.py3-none-any.whl", hash = "sha256:339f16c4a86b44240ba7223d0f93a7887c3ca04b5f9c8129da7958447d079b09"}, + {file = "virtualenv-20.13.0.tar.gz", hash = "sha256:d8458cf8d59d0ea495ad9b34c2599487f8a7772d796f9910858376d1600dd2dd"}, +] zipp = [ {file = "zipp-3.4.1-py3-none-any.whl", hash = "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"}, {file = "zipp-3.4.1.tar.gz", hash = "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76"}, diff --git a/pyproject.toml b/pyproject.toml index 609bc91..762f275 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,6 +37,7 @@ mypy = "^0.910" tortoise-orm = "^0.17.4" pydantic = "^1.8.2" coverage = "^6.2" +pre-commit = "^2.16.0" [build-system] requires = ["poetry_core>=1.0.0"] diff --git a/tests/test_automapper.py b/tests/test_automapper.py index 463dcee..6f6b886 100644 --- a/tests/test_automapper.py +++ b/tests/test_automapper.py @@ -22,7 +22,9 @@ def __init__(self, num: int, text: str, flag: bool) -> None: @classmethod def fields(cls) -> Iterable[str]: - return (field for field in cls.__init__.__annotations__.keys() if field != "return") + return ( + field for field in cls.__init__.__annotations__.keys() if field != "return" + ) class AnotherClass: @@ -68,12 +70,16 @@ def setUp(self): def test_add_spec__adds_to_internal_collection(self): self.mapper.add_spec(ParentClass, custom_spec_func) assert ParentClass in self.mapper._class_specs - assert ["num", "text", "flag"] == self.mapper._class_specs[ParentClass](ChildClass) + assert ["num", "text", "flag"] == self.mapper._class_specs[ParentClass]( + ChildClass + ) def test_add_spec__error_on_adding_same_class_spec(self): self.mapper.add_spec(ParentClass, custom_spec_func) with pytest.raises(DuplicatedRegistrationError): - self.mapper.add_spec(ParentClass, lambda concrete_type: ["field1", "field2"]) + self.mapper.add_spec( + ParentClass, lambda concrete_type: ["field1", "field2"] + ) def test_add_spec__adds_to_internal_collection_for_classifier(self): self.mapper.add_spec(classifier_func, spec_func) @@ -85,7 +91,9 @@ def test_add_spec__adds_to_internal_collection_for_classifier(self): def test_add_spec__error_on_duplicated_registration(self): self.mapper.add_spec(classifier_func, spec_func) with pytest.raises(DuplicatedRegistrationError): - self.mapper.add_spec(classifier_func, lambda concrete_type: ["field1", "field2"]) + self.mapper.add_spec( + classifier_func, lambda concrete_type: ["field1", "field2"] + ) def test_add__appends_class_to_class_mapping(self): with pytest.raises(MappingError): diff --git a/tests/test_for_complex_obj.py b/tests/test_for_complex_obj.py index 38a31ae..5225d66 100644 --- a/tests/test_for_complex_obj.py +++ b/tests/test_for_complex_obj.py @@ -22,7 +22,9 @@ def __init__(self, num: int, text: str, flag: bool) -> None: @classmethod def fields(cls) -> Iterable[str]: - return (field for field in cls.__init__.__annotations__.keys() if field != "return") + return ( + field for field in cls.__init__.__annotations__.keys() if field != "return" + ) class AnotherClass: @@ -73,7 +75,9 @@ def setUp(self): self.mapper = create_mapper() def test_map__complext_obj(self): - complex_obj = ComplexClass(obj=ChildClass(15, "nested_obj_msg", True), text="obj_msg") + complex_obj = ComplexClass( + obj=ChildClass(15, "nested_obj_msg", True), text="obj_msg" + ) self.mapper.add(ChildClass, AnotherClass) self.mapper.add(ComplexClass, AnotherComplexClass)