dynapydantic 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/.github/workflows/ci.yml +7 -1
  2. dynapydantic-0.2.0/.github/workflows/deploy-docs.yml +42 -0
  3. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/.github/workflows/pre-commit.yml +2 -1
  4. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/.gitignore +3 -0
  5. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/.pre-commit-config.yaml +9 -0
  6. dynapydantic-0.2.0/PKG-INFO +222 -0
  7. dynapydantic-0.2.0/README.md +212 -0
  8. dynapydantic-0.2.0/docs/README.md +1 -0
  9. dynapydantic-0.2.0/docs/reference.md +3 -0
  10. dynapydantic-0.2.0/htmlcov/class_index.html +313 -0
  11. dynapydantic-0.1.0/htmlcov/coverage_html_cb_6fb7b396.js → dynapydantic-0.2.0/htmlcov/coverage_html_cb_bcae5fc4.js +12 -10
  12. dynapydantic-0.2.0/htmlcov/function_index.html +418 -0
  13. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/htmlcov/index.html +80 -27
  14. dynapydantic-0.2.0/htmlcov/status.json +1 -0
  15. dynapydantic-0.1.0/htmlcov/style_cb_81f8c14c.css → dynapydantic-0.2.0/htmlcov/style_cb_8432e98f.css +52 -4
  16. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/htmlcov/z_f3e6dac33013b94d___init___py.html +27 -25
  17. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/htmlcov/z_f3e6dac33013b94d_exceptions_py.html +9 -9
  18. dynapydantic-0.2.0/htmlcov/z_f3e6dac33013b94d_polymorphic_py.html +128 -0
  19. dynapydantic-0.2.0/htmlcov/z_f3e6dac33013b94d_subclass_tracking_model_py.html +260 -0
  20. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/htmlcov/z_f3e6dac33013b94d_tracking_group_py.html +210 -206
  21. dynapydantic-0.2.0/mkdocs.yml +53 -0
  22. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/pyproject.toml +16 -1
  23. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/src/dynapydantic/__init__.py +2 -0
  24. dynapydantic-0.2.0/src/dynapydantic/polymorphic.py +29 -0
  25. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/src/dynapydantic/subclass_tracking_model.py +55 -13
  26. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/src/dynapydantic/tracking_group.py +11 -7
  27. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/tests/example/base-package/base_package/cli.py +4 -2
  28. dynapydantic-0.2.0/tests/example/base-package/uv.lock +149 -0
  29. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/tests/test_plugins.py +4 -2
  30. dynapydantic-0.2.0/tests/test_polymorphic.py +88 -0
  31. dynapydantic-0.2.0/tests/test_recursive_models.py +64 -0
  32. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/tests/test_subclass_tracking_model.py +6 -1
  33. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/tests/test_tracking_group.py +1 -1
  34. dynapydantic-0.2.0/uv.lock +1499 -0
  35. dynapydantic-0.1.0/PKG-INFO +0 -21
  36. dynapydantic-0.1.0/README.md +0 -11
  37. dynapydantic-0.1.0/htmlcov/class_index.html +0 -213
  38. dynapydantic-0.1.0/htmlcov/function_index.html +0 -283
  39. dynapydantic-0.1.0/htmlcov/status.json +0 -1
  40. dynapydantic-0.1.0/htmlcov/z_f3e6dac33013b94d_subclass_tracking_model_py.html +0 -218
  41. dynapydantic-0.1.0/uv.lock +0 -753
  42. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/.python-version +0 -0
  43. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/LICENSE +0 -0
  44. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/htmlcov/.gitignore +0 -0
  45. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/htmlcov/favicon_32_cb_58284776.png +0 -0
  46. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/htmlcov/keybd_closed_cb_ce680311.png +0 -0
  47. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/src/dynapydantic/exceptions.py +0 -0
  48. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/src/dynapydantic/py.typed +0 -0
  49. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/tests/__init__.py +0 -0
  50. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/tests/example/animal-plugins/animal_plugins/__init__.py +0 -0
  51. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/tests/example/animal-plugins/pyproject.toml +0 -0
  52. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/tests/example/base-package/base_package/__init__.py +0 -0
  53. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/tests/example/base-package/base_package/__main__.py +0 -0
  54. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/tests/example/base-package/base_package/animal.py +0 -0
  55. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/tests/example/base-package/base_package/cat.py +0 -0
  56. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/tests/example/base-package/base_package/circle.py +0 -0
  57. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/tests/example/base-package/base_package/shape.py +0 -0
  58. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/tests/example/base-package/pyproject.toml +0 -0
  59. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/tests/example/shape-plugins/pyproject.toml +0 -0
  60. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/tests/example/shape-plugins/shape_plugins/__init__.py +0 -0
  61. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/tests/example/shape-plugins/shape_plugins/plugin_classes.py +0 -0
  62. {dynapydantic-0.1.0 → dynapydantic-0.2.0}/tests/example/shape-plugins/shape_plugins/registration.py +0 -0
@@ -23,7 +23,13 @@ jobs:
23
23
  - name: Install the project
24
24
  run: uv sync --locked --all-extras --dev
25
25
  - name: Run tests
26
- run: uv run pytest
26
+ run: uv run pytest --cov-report=xml
27
+ - name: Upload coverage to Coveralls
28
+ uses: coverallsapp/github-action@v2
29
+ if: matrix.python-version == '3.13'
30
+ with:
31
+ github-token: ${{ secrets.GITHUB_TOKEN }}
32
+ path-to-lcov: coverage.xml
27
33
 
28
34
  publish-pypi:
29
35
  name: Publish to PyPI
@@ -0,0 +1,42 @@
1
+ name: Deploy versioned docs to GitHub Pages
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*' # deploy when a version tag is pushed
7
+ branches:
8
+ - main # deploy dev version on main
9
+
10
+ permissions:
11
+ contents: write
12
+
13
+ jobs:
14
+ deploy:
15
+ runs-on: ubuntu-latest
16
+
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ with:
20
+ fetch-depth: 0 # mike needs access to full git history
21
+ - name: Install uv
22
+ uses: astral-sh/setup-uv@v6
23
+ - name: Install the project
24
+ run: uv sync --locked --all-extras --dev
25
+ - name: Get version info
26
+ id: vars
27
+ run: |
28
+ VERSION=${GITHUB_REF#refs/tags/}
29
+ echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
30
+ if: startsWith(github.ref, 'refs/tags/')
31
+ - name: Set Git identity
32
+ run: |
33
+ git config user.name "github-actions[bot]"
34
+ git config user.email "github-actions[bot]@users.noreply.github.com"
35
+ - name: Deploy tagged release
36
+ if: startsWith(github.ref, 'refs/tags/')
37
+ run: |
38
+ uv run mike deploy ${{ steps.vars.outputs.version }} --update-aliases latest --push
39
+ uv run mike set-default latest --push
40
+ - name: Deploy dev version from main
41
+ if: github.ref == 'refs/heads/main'
42
+ run: uv run mike deploy dev --push --update-aliases
@@ -10,5 +10,6 @@ jobs:
10
10
  runs-on: ubuntu-latest
11
11
  steps:
12
12
  - uses: actions/checkout@v3
13
- - uses: actions/setup-python@v3
13
+ - name: Install uv
14
+ uses: astral-sh/setup-uv@v6
14
15
  - uses: pre-commit/action@v3.0.1
@@ -15,3 +15,6 @@ wheels/
15
15
 
16
16
  # OS files
17
17
  .DS_Store
18
+
19
+ # MKDocs
20
+ site
@@ -29,3 +29,12 @@ repos:
29
29
  rev: 0.7.20
30
30
  hooks:
31
31
  - id: uv-lock
32
+
33
+ - repo: local
34
+ hooks:
35
+ - id: pyrefly
36
+ name: pyrefly type check
37
+ entry: uv run pyrefly check
38
+ language: system
39
+ types: [python]
40
+ pass_filenames: false
@@ -0,0 +1,222 @@
1
+ Metadata-Version: 2.4
2
+ Name: dynapydantic
3
+ Version: 0.2.0
4
+ Summary: Dyanmic pydantic models
5
+ Author-email: Philip Salvaggio <salvaggio.philip@gmail.com>
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.10
8
+ Requires-Dist: pydantic>=2.0
9
+ Description-Content-Type: text/markdown
10
+
11
+ # dynapydantic
12
+
13
+ [![CI](https://github.com/psalvaggio/dynapydantic/actions/workflows/ci.yml/badge.svg)](https://github.com/psalvaggio/dynapydantic/actions/workflows/ci.yml)
14
+ [![Pre-commit](https://github.com/psalvaggio/dynapydantic/actions/workflows/pre-commit.yml/badge.svg)](https://github.com/psalvaggio/dynapydantic/actions/workflows/pre-commit.yml)
15
+ [![Docs](https://img.shields.io/badge/docs-Docs-blue?style=flat-square&logo=github&logoColor=white&link=https://psalvaggio.github.io/dynapydantic/dev/)](https://psalvaggio.github.io/dynapydantic/dev/)
16
+ [![PyPI - Version](https://img.shields.io/pypi/v/dynapydantic)](https://pypi.org/project/dynapydantic/)
17
+ [![Coverage Status](https://coveralls.io/repos/github/psalvaggio/dynapydantic/badge.svg?branch=main)](https://coveralls.io/github/psalvaggio/dynapydantic?branch=main)
18
+ [![Conda Version](https://img.shields.io/conda/v/conda-forge/dynapydantic)](https://anaconda.org/conda-forge/dynapydantic)
19
+
20
+
21
+ `dynapydantic` is an extension to the [pydantic](https://pydantic.dev) Python
22
+ package that allow for dynamic tracking of `pydantic.BaseModel` subclasses.
23
+
24
+ ## Installation
25
+ This project can be installed via PyPI:
26
+ ```
27
+ pip install dynapydantic
28
+ ```
29
+ or with `conda` via the `conda-forge` channel:
30
+ ```
31
+ conda install dynapydantic
32
+ ```
33
+
34
+
35
+ ## Motiviation
36
+ Consider the following simple class setup:
37
+ ```python
38
+ import pydantic
39
+
40
+ class Base(pydantic.BaseModel):
41
+ pass
42
+
43
+ class A(Base):
44
+ field: int
45
+
46
+ class B(Base):
47
+ field: str
48
+
49
+ class Model(pydantic.BaseModel):
50
+ val: Base
51
+ ```
52
+ As expected, we can use `A`'s and `B`'s for `Model.val`:
53
+ ```python
54
+ >>> m = Model(val=A(field=1))
55
+ >>> m
56
+ Model(val=A(field=1))
57
+ ```
58
+ However, we quickly run into trouble when serializing and validating:
59
+ ```python
60
+ >>> m.model_dump()
61
+ {'base': {}}
62
+ >>> m.model_dump(serialize_as_any=True)
63
+ {'val': {'field': 1}}
64
+ >>> Model.model_validate(m.model_dump(serialize_as_any=True))
65
+ Model(val=Base())
66
+ ```
67
+
68
+ Pydantic provides a solution for serialization via `serialize_as_any` (and
69
+ its corresponding field annotation `SerializeAsAny`), but offers no native
70
+ solution for the validation half. Currently, the canonical way of doing this
71
+ is to annotate the field as a discriminated union of all subclasses. Often, a
72
+ single field in the model is chosen as the "discriminator". This library,
73
+ `dynapydantic`, automates this process.
74
+
75
+ Let's reframe the above problem with `dynapydantic`:
76
+ ```python
77
+ import dynapydantic
78
+ import pydantic
79
+
80
+ class Base(
81
+ dynapydantic.SubclassTrackingModel,
82
+ discriminator_field="name",
83
+ discriminator_value_generator=lambda t: t.__name__,
84
+ ):
85
+ pass
86
+
87
+ class A(Base):
88
+ field: int
89
+
90
+ class B(Base):
91
+ field: str
92
+
93
+ class Model(pydantic.BaseModel):
94
+ val: dynapydantic.Polymorphic[Base]
95
+ ```
96
+ Now, the same set of operations works as intended:
97
+ ```python
98
+ >>> m = Model(val=A(field=1))
99
+ >>> m
100
+ Model(val=A(field=1, name='A'))
101
+ >>> m.model_dump()
102
+ {'val': {'field': 1, 'name': 'A'}}
103
+ >>> Model.model_validate(m.model_dump())
104
+ Model(val=A(field=1, name='A')
105
+ ```
106
+
107
+
108
+ ## How it works
109
+
110
+ ### `TrackingGroup`
111
+ The core entity in this library is the `dynapydantic.TrackingGroup`:
112
+ ```python
113
+ import typing as ty
114
+
115
+ import dynapydantic
116
+ import pydantic
117
+
118
+ mygroup = dynapydantic.TrackingGroup(
119
+ name="mygroup",
120
+ discriminator_field="name"
121
+ )
122
+
123
+ @mygroup.register("A")
124
+ class A(pydantic.BaseModel):
125
+ """A class to be tracked, will be tracked as "A"."""
126
+ a: int
127
+
128
+ @mygroup.register()
129
+ class B(pydantic.BaseModel):
130
+ """Another class, will be tracked as "B"."""
131
+ name: ty.Literal["B"] = "B"
132
+ a: int
133
+
134
+ class Model(pydantic.BaseModel):
135
+ """A model that can have A or B"""
136
+ field: mygroup.union() # call after all subclasses have been registered
137
+
138
+ print(Model(field={"name": "A", "a": 4})) # field=A(a=4, name='A')
139
+ print(Model(field={"name": "B", "a": 5})) # field=B(name='B', a=5)
140
+ ```
141
+
142
+ The `union()` method produces a [discriminated union](https://docs.pydantic.dev/latest/concepts/unions/#discriminated-unions)
143
+ of all registered `pydantic.BaseModel` subclasses. It also accepts an
144
+ `annotated=False` keyword argument to produce a plain `typing.Union` for use
145
+ in type annotations, but since this is a runtime-computed union, this will not
146
+ work with static type checkers. This union is based on a discriminator field,
147
+ which was configured by the `discriminator_field` argument to `TrackingGroup`.
148
+ The field can be created by hand, as was shown with `B`, or `dynapydantic`
149
+ will inject it for you, as was shown with `A`.
150
+
151
+ `TrackingGroup` has a few opt-in features to make it more powerful and easier to use:
152
+ 1. `discriminator_value_generator`: This parameter is a optional callback
153
+ function that is called with each class that gets registered and produces a
154
+ default value for the discriminator field. This allows the user to call
155
+ `register()` without a value for the discriminator. For example, passing:
156
+ `lambda cls: cls.__name__` would use the name of the class as the
157
+ discriminator value.
158
+ 2. `plugin_entry_point`: This parameter indicates to `dynapydantic` that there
159
+ might be models to be discovered in other packages. Packages are discovered
160
+ by the Python entrypoint mechanism. See the `tests/example` directory for an
161
+ example of how this works.
162
+
163
+ ### `SubclassTrackingModel`
164
+ The most common use case of this pattern is to automatically register subclasses
165
+ of a given `pydantic.BaseModel`. This is supported via the use of
166
+ `dynapydantic.SubclassTrackingModel`. For example:
167
+ ```python
168
+ import typing as ty
169
+
170
+ import dynapydantic
171
+ import pydantic
172
+
173
+ class Base(
174
+ dynapydantic.SubclassTrackingModel,
175
+ discriminator_field="name",
176
+ discriminator_value_generator=lambda cls: cls.__name__,
177
+ ):
178
+ """Base model, will track its subclasses"""
179
+
180
+ # The TrackingGroup can be specified here like model_config, or passed in
181
+ # kwargs of the class declaration, just like how model_config works with
182
+ # pydantic.BaseModel. If you do it like this, you have to give the tracking
183
+ # group a name, whereas using kwargs will generate the name for you.
184
+ # tracking_config: ty.ClassVar[dynapydantic.TrackingGroup] = dynapydantic.TrackingGroup(
185
+ # name="BaseSubclasses",
186
+ # discriminator_field="name",
187
+ # discriminator_value_generator=lambda cls: cls.__name__,
188
+ # )
189
+
190
+
191
+ class Intermediate(Base, exclude_from_union=True):
192
+ """Subclasses can opt out of being tracked"""
193
+
194
+ class Derived1(Intermediate):
195
+ """Non-direct descendants are registered"""
196
+ a: int
197
+
198
+ class Derived2(Intermediate):
199
+ """You can override the value generator if desired"""
200
+ name: ty.Literal["Custom"] = "Custom"
201
+ a: int
202
+
203
+ print(Base.registered_subclasses())
204
+ # {'Derived1': <class '__main__.Derived1'>, 'Custom': <class '__main__.Derived2'>}
205
+
206
+ # if plugin_entry_point was specificed, load plugin packages
207
+ # Base.load_plugins()
208
+
209
+ class Model(pydantic.BaseModel):
210
+ """A model that can have any registered Base subclass"""
211
+ field: dynapydantic.Polymorphic[Base]
212
+
213
+ print(Model(field={"name": "Derived1", "a": 4}))
214
+ # field=Derived1(a=4, name='Derived1')
215
+ print(Model(field={"name": "Custom", "a": 5}))
216
+ # field=Derived2(name='Custom', a=5)
217
+ ```
218
+ It is important to note that the subclasses that are supported are those that
219
+ were defined *prior* to defining the model that uses `dynapydantic.Polymorphic`
220
+ (`Model` in the above example). If you declare additional subclasses afterwards,
221
+ you must call `.model_rebuild(force=True)` on the model that uses the subclass
222
+ union.
@@ -0,0 +1,212 @@
1
+ # dynapydantic
2
+
3
+ [![CI](https://github.com/psalvaggio/dynapydantic/actions/workflows/ci.yml/badge.svg)](https://github.com/psalvaggio/dynapydantic/actions/workflows/ci.yml)
4
+ [![Pre-commit](https://github.com/psalvaggio/dynapydantic/actions/workflows/pre-commit.yml/badge.svg)](https://github.com/psalvaggio/dynapydantic/actions/workflows/pre-commit.yml)
5
+ [![Docs](https://img.shields.io/badge/docs-Docs-blue?style=flat-square&logo=github&logoColor=white&link=https://psalvaggio.github.io/dynapydantic/dev/)](https://psalvaggio.github.io/dynapydantic/dev/)
6
+ [![PyPI - Version](https://img.shields.io/pypi/v/dynapydantic)](https://pypi.org/project/dynapydantic/)
7
+ [![Coverage Status](https://coveralls.io/repos/github/psalvaggio/dynapydantic/badge.svg?branch=main)](https://coveralls.io/github/psalvaggio/dynapydantic?branch=main)
8
+ [![Conda Version](https://img.shields.io/conda/v/conda-forge/dynapydantic)](https://anaconda.org/conda-forge/dynapydantic)
9
+
10
+
11
+ `dynapydantic` is an extension to the [pydantic](https://pydantic.dev) Python
12
+ package that allow for dynamic tracking of `pydantic.BaseModel` subclasses.
13
+
14
+ ## Installation
15
+ This project can be installed via PyPI:
16
+ ```
17
+ pip install dynapydantic
18
+ ```
19
+ or with `conda` via the `conda-forge` channel:
20
+ ```
21
+ conda install dynapydantic
22
+ ```
23
+
24
+
25
+ ## Motiviation
26
+ Consider the following simple class setup:
27
+ ```python
28
+ import pydantic
29
+
30
+ class Base(pydantic.BaseModel):
31
+ pass
32
+
33
+ class A(Base):
34
+ field: int
35
+
36
+ class B(Base):
37
+ field: str
38
+
39
+ class Model(pydantic.BaseModel):
40
+ val: Base
41
+ ```
42
+ As expected, we can use `A`'s and `B`'s for `Model.val`:
43
+ ```python
44
+ >>> m = Model(val=A(field=1))
45
+ >>> m
46
+ Model(val=A(field=1))
47
+ ```
48
+ However, we quickly run into trouble when serializing and validating:
49
+ ```python
50
+ >>> m.model_dump()
51
+ {'base': {}}
52
+ >>> m.model_dump(serialize_as_any=True)
53
+ {'val': {'field': 1}}
54
+ >>> Model.model_validate(m.model_dump(serialize_as_any=True))
55
+ Model(val=Base())
56
+ ```
57
+
58
+ Pydantic provides a solution for serialization via `serialize_as_any` (and
59
+ its corresponding field annotation `SerializeAsAny`), but offers no native
60
+ solution for the validation half. Currently, the canonical way of doing this
61
+ is to annotate the field as a discriminated union of all subclasses. Often, a
62
+ single field in the model is chosen as the "discriminator". This library,
63
+ `dynapydantic`, automates this process.
64
+
65
+ Let's reframe the above problem with `dynapydantic`:
66
+ ```python
67
+ import dynapydantic
68
+ import pydantic
69
+
70
+ class Base(
71
+ dynapydantic.SubclassTrackingModel,
72
+ discriminator_field="name",
73
+ discriminator_value_generator=lambda t: t.__name__,
74
+ ):
75
+ pass
76
+
77
+ class A(Base):
78
+ field: int
79
+
80
+ class B(Base):
81
+ field: str
82
+
83
+ class Model(pydantic.BaseModel):
84
+ val: dynapydantic.Polymorphic[Base]
85
+ ```
86
+ Now, the same set of operations works as intended:
87
+ ```python
88
+ >>> m = Model(val=A(field=1))
89
+ >>> m
90
+ Model(val=A(field=1, name='A'))
91
+ >>> m.model_dump()
92
+ {'val': {'field': 1, 'name': 'A'}}
93
+ >>> Model.model_validate(m.model_dump())
94
+ Model(val=A(field=1, name='A')
95
+ ```
96
+
97
+
98
+ ## How it works
99
+
100
+ ### `TrackingGroup`
101
+ The core entity in this library is the `dynapydantic.TrackingGroup`:
102
+ ```python
103
+ import typing as ty
104
+
105
+ import dynapydantic
106
+ import pydantic
107
+
108
+ mygroup = dynapydantic.TrackingGroup(
109
+ name="mygroup",
110
+ discriminator_field="name"
111
+ )
112
+
113
+ @mygroup.register("A")
114
+ class A(pydantic.BaseModel):
115
+ """A class to be tracked, will be tracked as "A"."""
116
+ a: int
117
+
118
+ @mygroup.register()
119
+ class B(pydantic.BaseModel):
120
+ """Another class, will be tracked as "B"."""
121
+ name: ty.Literal["B"] = "B"
122
+ a: int
123
+
124
+ class Model(pydantic.BaseModel):
125
+ """A model that can have A or B"""
126
+ field: mygroup.union() # call after all subclasses have been registered
127
+
128
+ print(Model(field={"name": "A", "a": 4})) # field=A(a=4, name='A')
129
+ print(Model(field={"name": "B", "a": 5})) # field=B(name='B', a=5)
130
+ ```
131
+
132
+ The `union()` method produces a [discriminated union](https://docs.pydantic.dev/latest/concepts/unions/#discriminated-unions)
133
+ of all registered `pydantic.BaseModel` subclasses. It also accepts an
134
+ `annotated=False` keyword argument to produce a plain `typing.Union` for use
135
+ in type annotations, but since this is a runtime-computed union, this will not
136
+ work with static type checkers. This union is based on a discriminator field,
137
+ which was configured by the `discriminator_field` argument to `TrackingGroup`.
138
+ The field can be created by hand, as was shown with `B`, or `dynapydantic`
139
+ will inject it for you, as was shown with `A`.
140
+
141
+ `TrackingGroup` has a few opt-in features to make it more powerful and easier to use:
142
+ 1. `discriminator_value_generator`: This parameter is a optional callback
143
+ function that is called with each class that gets registered and produces a
144
+ default value for the discriminator field. This allows the user to call
145
+ `register()` without a value for the discriminator. For example, passing:
146
+ `lambda cls: cls.__name__` would use the name of the class as the
147
+ discriminator value.
148
+ 2. `plugin_entry_point`: This parameter indicates to `dynapydantic` that there
149
+ might be models to be discovered in other packages. Packages are discovered
150
+ by the Python entrypoint mechanism. See the `tests/example` directory for an
151
+ example of how this works.
152
+
153
+ ### `SubclassTrackingModel`
154
+ The most common use case of this pattern is to automatically register subclasses
155
+ of a given `pydantic.BaseModel`. This is supported via the use of
156
+ `dynapydantic.SubclassTrackingModel`. For example:
157
+ ```python
158
+ import typing as ty
159
+
160
+ import dynapydantic
161
+ import pydantic
162
+
163
+ class Base(
164
+ dynapydantic.SubclassTrackingModel,
165
+ discriminator_field="name",
166
+ discriminator_value_generator=lambda cls: cls.__name__,
167
+ ):
168
+ """Base model, will track its subclasses"""
169
+
170
+ # The TrackingGroup can be specified here like model_config, or passed in
171
+ # kwargs of the class declaration, just like how model_config works with
172
+ # pydantic.BaseModel. If you do it like this, you have to give the tracking
173
+ # group a name, whereas using kwargs will generate the name for you.
174
+ # tracking_config: ty.ClassVar[dynapydantic.TrackingGroup] = dynapydantic.TrackingGroup(
175
+ # name="BaseSubclasses",
176
+ # discriminator_field="name",
177
+ # discriminator_value_generator=lambda cls: cls.__name__,
178
+ # )
179
+
180
+
181
+ class Intermediate(Base, exclude_from_union=True):
182
+ """Subclasses can opt out of being tracked"""
183
+
184
+ class Derived1(Intermediate):
185
+ """Non-direct descendants are registered"""
186
+ a: int
187
+
188
+ class Derived2(Intermediate):
189
+ """You can override the value generator if desired"""
190
+ name: ty.Literal["Custom"] = "Custom"
191
+ a: int
192
+
193
+ print(Base.registered_subclasses())
194
+ # {'Derived1': <class '__main__.Derived1'>, 'Custom': <class '__main__.Derived2'>}
195
+
196
+ # if plugin_entry_point was specificed, load plugin packages
197
+ # Base.load_plugins()
198
+
199
+ class Model(pydantic.BaseModel):
200
+ """A model that can have any registered Base subclass"""
201
+ field: dynapydantic.Polymorphic[Base]
202
+
203
+ print(Model(field={"name": "Derived1", "a": 4}))
204
+ # field=Derived1(a=4, name='Derived1')
205
+ print(Model(field={"name": "Custom", "a": 5}))
206
+ # field=Derived2(name='Custom', a=5)
207
+ ```
208
+ It is important to note that the subclasses that are supported are those that
209
+ were defined *prior* to defining the model that uses `dynapydantic.Polymorphic`
210
+ (`Model` in the above example). If you declare additional subclasses afterwards,
211
+ you must call `.model_rebuild(force=True)` on the model that uses the subclass
212
+ union.
@@ -0,0 +1 @@
1
+ ../README.md
@@ -0,0 +1,3 @@
1
+ # API Reference
2
+
3
+ ::: dynapydantic