Skip to content

Commit 63fbb92

Browse files
Restructure API docs and add usage guide to CORE
1 parent f43d59d commit 63fbb92

18 files changed

+478
-258
lines changed

databind.core/README.md

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,8 @@
1-
# databind.core
1+
# `databind.core`
22

3-
`databind.core` provides a jackson-databind inspired framework for data de-/serialization in Python. Unless you
4-
are looking to implement support for de-/serializing new data formats, the `databind.core` package alone might
5-
not be what you are looking for (unless you want to use `databind.core.dataclasses` as a drop-in replacement to
6-
the standard library `dataclasses` module, for that check out the section at the bottom).
7-
8-
### Known implementations
9-
10-
* [databind.json](https://pypi.org/project/databind.json)
11-
12-
### Dataclass extension
13-
14-
The standard library `dataclasses` module does not allow to define non-default arguments after default arguments.
15-
You can use `databind.core.dataclasses` as a drop-in replacement to get this feature. It behaves exactly like the
16-
standard library, only that non-default arguments may follow default arguments. Such arguments can be passed to
17-
the constructor as positional or keyword arguments.
18-
19-
```py
20-
from databind.core import dataclasses
21-
22-
@dataclasses.dataclass
23-
class A:
24-
value1: int = 42
25-
26-
@dataclasses.dataclass
27-
class B(A):
28-
value2: str
29-
30-
print(B(0, 'Hello, World!'))
31-
print(B(value2='Answer to the universe'))
32-
```
3+
This library provides the core functionality to implement serialization functions to and from Python objects, with
4+
a great support for many features of the Python type system. A JSON implementation is provided by the `databind.json`
5+
package.
336

347
---
358

docs/build.novella

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,35 @@
11

22
template "mkdocs"
33

4-
def databind_modules = [
4+
def databind_core_modules = [
5+
'databind.core',
56
'databind.core.context',
67
'databind.core.converter',
78
'databind.core.dataclasses',
89
'databind.core.mapper',
910
'databind.core.schema',
1011
'databind.core.settings',
1112
'databind.core.union',
13+
]
14+
15+
def databind_json_modules = [
1216
'databind.json',
1317
'databind.json.converters',
1418
'databind.json.direction',
1519
'databind.json.module',
20+
'databind.json.settings',
1621
]
1722

1823
action "mkdocs-update-config" {
1924
site_name = "python-databind"
2025
update '$.theme.features' add: []
21-
update '$.theme.palette' set: {'scheme': 'slate', 'primary': 'blue', 'accent': 'amber'}
26+
update '$.theme.palette' set: {'primary': 'blue', 'accent': 'amber'}
2227
update_with config -> {
23-
for module in databind_modules:
24-
config['nav'][-2]['API'].append('api/' + module + '.md')
28+
print(json.dumps(config, indent=2))
29+
for module in databind_core_modules:
30+
config['nav'][1]['CORE'][-1]['API'].append('core/api/' + module + '.md')
31+
for module in databind_json_modules:
32+
config['nav'][2]['JSON'][-1]['API'].append('json/api/' + module + '.md')
2533
}
2634
}
2735

@@ -38,8 +46,12 @@ do
3846
precedes "preprocess-markdown"
3947
}
4048
action: {
41-
for module in databind_modules:
42-
def filename = directory / 'content' / 'api'/ (module + '.md')
49+
for module in databind_core_modules:
50+
def filename = directory / 'content' / 'core' / 'api'/ (module + '.md')
51+
filename.parent.mkdir parents: True exist_ok: True
52+
filename.write_text '---\ntitle: {0}\n---\n@pydoc {0}\n'.format(module)
53+
for module in databind_json_modules:
54+
def filename = directory / 'content' / 'json' / 'api'/ (module + '.md')
4355
filename.parent.mkdir parents: True exist_ok: True
4456
filename.write_text '---\ntitle: {0}\n---\n@pydoc {0}\n'.format(module)
4557
}

docs/content/core/basic-usage.md

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
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+
```
Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
---
2-
title: databind.core
3-
---
1+
# Changelog
42

53
@shell cd ../databind.core && slap changelog format --markdown --all

docs/content/core/dataclass-ext.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Dataclass extension
2+
3+
The standard library `dataclasses` module does not allow to define non-default arguments after default arguments.
4+
You can use `databind.core.dataclasses` as a drop-in replacement to get this feature. It behaves exactly like the
5+
standard library, only that non-default arguments may follow default arguments. Such arguments can be passed to
6+
the constructor as positional or keyword arguments.
7+
8+
!!! note
9+
10+
You will loose Mypy type checking support for dataclasses decorated with `databind.core.dataclasses.dataclass`.
11+
12+
```py
13+
from databind.core import dataclasses
14+
15+
@dataclasses.dataclass
16+
class A:
17+
value1: int = 42
18+
19+
@dataclasses.dataclass
20+
class B(A):
21+
value2: str
22+
23+
print(B(0, 'Hello, World!'))
24+
print(B(value2='Answer to the universe'))
25+
```

docs/content/core/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@cat ../../../databind.core/README.md

docs/content/core/settings.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Settings
2+
3+
Settings in Databind are Python objects that convey additional information to the serialization process. A setting
4+
must be expicitly supported by a {@pylink databind.core.converter.Converter} in order to take effect. As such, all
5+
settings provided by `databind.core` merely provide a standard set of settings that should but may not all be supported
6+
by serialization lirbary implementations.
7+
8+
Settings must be subclasses from {@pylink databind.core.settings.Setting}, {@pylink databind.core.settings.BooleanSetting}
9+
or {@pylink databind.core.settings.ClassDecoratedSetting}.
10+
11+
## Specifying settings
12+
13+
You can specify settings at various places in your code to make them apply at various stages during the serialization.
14+
The following list shows the order of precedence, from highest to lowest:
15+
16+
1. Type-hint local settings specified in the metadata of `typing.Annotated` hints.
17+
2. Settings that were used to annotate a type.
18+
3. Global settings that are passed to {@pylink databind.core.mapper.ObjectMapper.convert}, or the respective
19+
`serialize`/`deserialize` methods.
20+
21+
## Settings priority
22+
23+
The above precedence only takes effect within the same priority group. The priority of all setting defaults to `NORMAL`
24+
unless specified otherwise. The following priority groups exist:
25+
26+
* `LOW`: Settings with this priority group are resolved after `NORMAL` settings.
27+
* `NORMAL`: The default priority group.
28+
* `HIGH`: Settings with this priority group are resolved before `NORMAL` settings.
29+
* `ULTIMATE`: Settings with this priority group are resolved before `HIGH` settings.
30+
31+
Converters are usually only interested in the first instance of any setting type.

docs/content/databind.core.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

docs/content/databind.json.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

0 commit comments

Comments
 (0)