|
| 1 | +# Basic Usage |
| 2 | + |
| 3 | +## Implementing a custom converter |
| 4 | + |
| 5 | +Implementing your own serialization functions is easy. It's often needed, not only when implementing a new |
| 6 | +serialization format next to the `databind.json` module, but also to extend an existing serialization format. For |
| 7 | +example, you may want to add support for serializing your `URI` custom type to and from a string. You can do this |
| 8 | +by implementing a custom {@pylink databind.core.converter.Converter}. |
| 9 | + |
| 10 | +```python |
| 11 | +from dataclasses import dataclass |
| 12 | +from urllib.parse import urlparse |
| 13 | +from typing import Any |
| 14 | + |
| 15 | +from databind.core.context import Context |
| 16 | +from databind.core.converter import Converter |
| 17 | + |
| 18 | + |
| 19 | +@dataclass |
| 20 | +class URI: |
| 21 | + scheme: str |
| 22 | + host: str |
| 23 | + path: str |
| 24 | + |
| 25 | + def __str__(self) -> str: |
| 26 | + return f'{self.scheme}://{self.host}{self.path}' |
| 27 | + |
| 28 | + class URIConverter(Converter): |
| 29 | + |
| 30 | + def convert(self, ctx: Context) -> Any: |
| 31 | + if not isinstance(ctx.datatype, ClassTypeHint) or not issubclass(ctx.datatype.type, URI): |
| 32 | + raise NotImplementedError |
| 33 | + if ctx.direction.is_serialize(): |
| 34 | + return str(ctx.value) # Always serialize into string |
| 35 | + elif ctx.direction.is_deserialize(): |
| 36 | + if isinstance(ctx.value, str): |
| 37 | + parsed = urlparse(ctx.value) |
| 38 | + return URI(parsed.scheme, parsed.hostname, parsed.path) |
| 39 | + # Fall back to other converters, such as default implementation for dataclasses |
| 40 | + raise NotImplementedError |
| 41 | + assert False, 'invalid direction' |
| 42 | +``` |
| 43 | + |
| 44 | +To use this new converter, you need to register it to an {@pylink databind.core.mapper.ObjectMapper} instance. |
| 45 | + |
| 46 | +```python |
| 47 | +from databind.core.mapper import ObjectMapper |
| 48 | + |
| 49 | +mapper = ObjectMapper() |
| 50 | +mapper.module.register(URI.URIConverter()) |
| 51 | + |
| 52 | +assert mapper.deserialize('https://example.com/foo', URI) == URI('https', 'example.com', '/foo') |
| 53 | +assert mapper.serialize(URI('https', 'example.com', '/foo'), URI) == 'https://example.com/foo' |
| 54 | +``` |
| 55 | + |
| 56 | +## Supporting settings in your converter |
| 57 | + |
| 58 | +!!! info "What are settings?" |
| 59 | + |
| 60 | + "Settings" are Python objects that are associated with types in the serialization process that can alter the behavior |
| 61 | + of the converter. Go to [Settings](settings.md) to read more about it. |
| 62 | + |
| 63 | +Consuming settings in a converter is straight forward. The {@pylink databind.core.context.Context} class provides |
| 64 | +convenient methods to access settings that are relevant for the current value being processed. |
| 65 | + |
| 66 | +```python |
| 67 | +# ... |
| 68 | +from databind.core.settings import BooleanSetting |
| 69 | + |
| 70 | + |
| 71 | +@dataclass |
| 72 | +class URI: |
| 73 | + # ... |
| 74 | + |
| 75 | + class SerializeAsString(BooleanSetting): |
| 76 | + """ |
| 77 | + Specifies whether the URI should be serialized to a string. |
| 78 | + """ |
| 79 | + |
| 80 | + class URIConverter(Converter): |
| 81 | + |
| 82 | + def convert(self, ctx: Context) -> Any: |
| 83 | + if not isinstance(ctx.datatype, ClassTypeHint) or not issubclass(ctx.datatype.type, URI): |
| 84 | + raise NotImplementedError |
| 85 | + if ctx.direction.is_serialize(): |
| 86 | + serialize_as_string = ctx.get_setting(URI.SerializeAsString).enabled |
| 87 | + if serialize_as_string: |
| 88 | + return str(ctx.value) |
| 89 | + raise NotImplementedError |
| 90 | + elif ctx.direction.is_deserialize(): |
| 91 | + if isinstance(ctx.value, str): |
| 92 | + parsed = urlparse(ctx.value) |
| 93 | + return URI(parsed.scheme, parsed.hostname, parsed.path) |
| 94 | + raise NotImplementedError |
| 95 | + assert False, 'invalid direction' |
| 96 | +``` |
| 97 | + |
| 98 | +The setting can now be specified when serializing a `URI` instance as a global setting: |
| 99 | + |
| 100 | +```python |
| 101 | +# ... |
| 102 | + |
| 103 | +from databind.core.converter import NoMatchingConverter |
| 104 | +from pytest import raises |
| 105 | + |
| 106 | +# When the setting is not enabled, the converter raises a NotImplementedError, having databind search for |
| 107 | +# another applicable converter. Since none exists with an otherwise empty ObjectMapper, this raises a |
| 108 | +# NoMatchingConverter exception. |
| 109 | +with raises(NoMatchingConverter): |
| 110 | + mapper.serialize(URI('https', 'example.com', '/foo'), URI) |
| 111 | + |
| 112 | +# Using a global setting, affecting all URI instances being serialized unless a more local setting is specified. |
| 113 | +assert mapper.serialize( |
| 114 | + URI('htps', 'example.com', '/foo'), URI, settings=[URI.SerializeAsString(True)]) == 'https://example.com/foo' |
| 115 | +``` |
| 116 | + |
| 117 | +## Supporting `typing.Annotated` type hints |
| 118 | + |
| 119 | +Converters must explicitly support `typing.Annotated` type hints. They are often useful to associate settings with |
| 120 | +a type in a particular case only. There may also be other reasons that a user may want to use an `Annotated` type |
| 121 | +hint. |
| 122 | + |
| 123 | +```python |
| 124 | +class URI: |
| 125 | + # ... |
| 126 | + |
| 127 | + class URIConverter(Converter): |
| 128 | + |
| 129 | + def convert(self, ctx: Context) -> Any: |
| 130 | + # Check if the type to be converted is supposed to be a URI. |
| 131 | + datatype = ctx.datatype |
| 132 | + if isinstance(datatype, AnnotatedTypeHint): |
| 133 | + datatype = datatype[0] |
| 134 | + if not isinstance(datatype, ClassTypeHint) or not issubclass(datatype.type, URI): |
| 135 | + raise NotImplementedError |
| 136 | + |
| 137 | + # ... |
| 138 | +``` |
| 139 | + |
| 140 | +Now the setting can be specified as an `Annotated` type hint: |
| 141 | + |
| 142 | +```python |
| 143 | +# Using the Annotated type hint to associate the setting with the type. |
| 144 | +assert mapper.serialize( |
| 145 | + URI('https', 'example.com', '/foo'), Annoated[URI, URI.SerializeAsString(True)]) == 'https://example.com/foo' |
| 146 | +``` |
| 147 | + |
| 148 | +## Class-decorator settings |
| 149 | + |
| 150 | +There is also a special class called {@pylink databind.core.settings.ClassDecoratorSetting}, which can be used to |
| 151 | +create setting types that can decorate classes. The `Context.get_settings()` method will automatically understand |
| 152 | +that setting as well. |
| 153 | + |
| 154 | +## Simplifying custom converts for users |
| 155 | + |
| 156 | +Implementing custom converters, especially to convert between strings and custom types, can be a bit tedious. Given |
| 157 | +that it is quite a common use case, it is usually recommended that a Databind serialization library provide specific |
| 158 | +settings to simplify the process for users. |
| 159 | + |
| 160 | +For example, the `databind.json` package provides a {@pylink databind.json.settings.JsonConverter} setting that users |
| 161 | +can use to very easily support the serialization of their custom types to and from strings in a JSON context. |
| 162 | + |
| 163 | +```python |
| 164 | +from databind.json.settings import JsonConverter |
| 165 | + |
| 166 | +@JsonConverter.using_classmethods(serialize="__str__", deserialize="of") |
| 167 | +class MyCustomType: |
| 168 | + |
| 169 | + def __str__(self) -> str: |
| 170 | + ... |
| 171 | + |
| 172 | + @staticmethod |
| 173 | + def of(s: str) -> MyCustomType: |
| 174 | + ... |
| 175 | +``` |
0 commit comments