Skip to content

Commit f9cb504

Browse files
author
Callum Dickinson
committed
Replace supplementary record/manager classes with mixins
Records in Odoo can have some additional fields that are shared across many different record models using model inheritance. To support adding these kinds of shared fields (and related methods) to record/manager classes in the OpenStack Odoo Client library in a more modular way, add support for the use of **mixins** to take advantage of Python's multiple inheritance to add such fields and methods to custom record/manager classes. **Protocol** classes were created for `RecordBase` (called `RecordProtocol`) and `RecordManagerBase` (called `RecordManagerProtocol`) which contain common attribute/field type hints and method stubs. The mixin classes subclass these protocol classes to provide type hints within mixin classes as if they subclass the record/manager base classes (they can't do this directly for complicated reasons). The implementations for the record and record manager base classes have been refactored to reduce duplication as part of this work. The `NamedRecordManagerBase` and `CodedRecordManagerBase` classes have been reimplemented using mixins to not only utilise this paradigm inside the library itself, but also demonstrate its usage in a simple and practical way for anyone looking to write their own mixins. The `get_by_unique_field` method provided by `RecordManagerWithUniqueFieldBase` has been incorporated into `RecordManagerBase` to make it available for use in any custom manager class. Finally, a basic unit testing pipeline using `pytest` has been added. Initially a single test to make sure the package can be imported correctly has been added, but the plan is to expand coverage over time.
1 parent 57344be commit f9cb504

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+2171
-1026
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
- name: Setup uv
3030
uses: astral-sh/setup-uv@v7
3131
with:
32-
version: "0.9.2"
32+
version: "0.9.17"
3333
- name: Create virtual environment
3434
run: uv sync --only-dev
3535
- name: Publish the docs to GitHub Pages

.github/workflows/tag.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
- name: Setup uv
2424
uses: astral-sh/setup-uv@v7
2525
with:
26-
version: "0.9.2"
26+
version: "0.9.17"
2727
- name: Build source dist and wheels
2828
run: uv build
2929
- name: Upload source dist and wheels to artifacts
@@ -58,7 +58,7 @@ jobs:
5858
- name: Setup uv
5959
uses: astral-sh/setup-uv@v7
6060
with:
61-
version: "0.9.2"
61+
version: "0.9.17"
6262
- name: Publish source dist and wheels to PyPI
6363
run: uv publish
6464

@@ -109,7 +109,7 @@ jobs:
109109
- name: Setup uv
110110
uses: astral-sh/setup-uv@v7
111111
with:
112-
version: "0.9.2"
112+
version: "0.9.17"
113113
- name: Create virtual environment
114114
run: uv sync --only-dev
115115
- name: Publish the docs to GitHub Pages

.github/workflows/test.yml

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,34 @@ jobs:
3030
- name: Run pre-commit hooks
3131
uses: pre-commit/[email protected]
3232

33+
test:
34+
needs: pre-commit
35+
runs-on: ubuntu-24.04
36+
strategy:
37+
matrix:
38+
python_version:
39+
- "3.10"
40+
- "3.11"
41+
- "3.12"
42+
- "3.13"
43+
- "3.14"
44+
steps:
45+
- name: Clone full tree, and checkout branch
46+
uses: actions/checkout@v5
47+
with:
48+
fetch-depth: 0
49+
- name: Setup Python
50+
uses: actions/setup-python@v6
51+
with:
52+
python-version: "${{ matrix.python_version }}"
53+
cache: "pip"
54+
- name: Setup uv
55+
uses: astral-sh/setup-uv@v7
56+
with:
57+
version: "0.9.17"
58+
- name: Run tests
59+
run: uv run poe test
60+
3361
build:
3462
needs: pre-commit
3563
runs-on: ubuntu-24.04
@@ -46,7 +74,7 @@ jobs:
4674
- name: Setup uv
4775
uses: astral-sh/setup-uv@v7
4876
with:
49-
version: "0.9.2"
77+
version: "0.9.17"
5078
- name: Build source dist and wheels
5179
run: uv build
5280
- name: Upload source dist and wheels to artifacts

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ coverage.xml
5050
.hypothesis/
5151
.pytest_cache/
5252
cover/
53+
rspec.xml
5354

5455
# Translations
5556
*.mo

.pre-commit-config.yaml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,20 @@ repos:
1515
- id: check-added-large-files
1616
- id: check-merge-conflict
1717
- repo: https://github.com/crate-ci/typos
18-
rev: "v1.38.1"
18+
rev: "v1.40.0"
1919
hooks:
2020
- id: typos
2121
- repo: https://github.com/astral-sh/uv-pre-commit
22-
rev: "0.9.2"
22+
rev: "0.9.17"
2323
hooks:
2424
- id: uv-lock
2525
- repo: https://github.com/astral-sh/ruff-pre-commit
26-
rev: "v0.14.0"
26+
rev: "v0.14.9"
2727
hooks:
2828
- id: ruff-check
2929
- id: ruff-format
3030
- repo: https://github.com/pre-commit/mirrors-mypy
31-
rev: "v1.18.2"
31+
rev: "v1.19.0"
3232
hooks:
3333
- id: mypy
3434
additional_dependencies:

changelog.d/13.changed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Replace supplementary record/manager classes with mixins

docs/managers/custom.md

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,230 @@ The following internal attributes are also available for use in methods:
791791
* `_odoo` (`odoorpc.ODOO`) - The OdooRPC connection object
792792
* `_env` (`odoorpc.env.Environment`) - The OdooRPC environment object for the model
793793

794+
## Mixins
795+
796+
Python supports [multiple inheritance](https://docs.python.org/3/tutorial/classes.html#multiple-inheritance)
797+
when creating new classes. A common use case for multiple inheritance is to extend
798+
functionality of a class through the use of *mixin classes*, which are minimal
799+
classes that only consist of supplementary attributes and methods, that get added
800+
to other classes through subclassing.
801+
802+
The OpenStack Odoo Client library for Python supports the use of mixin classes
803+
to add functionality to custom record and manager classes in a modular way.
804+
Multiple mixins can be added to record and manager classes to allow mixing and
805+
matching additional functionality as required.
806+
807+
### Using Mixins
808+
809+
To extend the functionality of your custom record and manager classes,
810+
append the mixins for the record class and/or record manager class
811+
**AFTER** the inheritance for `RecordBase` and `RecordManagerBase`.
812+
You also need to specify the **same** type arguments to the mixins as
813+
is already being done for `RecordBase` and `RecordManagerBase`.
814+
815+
```python
816+
from __future__ import annotations
817+
818+
from openstack_odooclient import (
819+
NamedRecordManagerMixin,
820+
NamedRecordMixin,
821+
RecordBase,
822+
RecordManagerBase,
823+
)
824+
825+
class CustomRecord(
826+
RecordBase["CustomRecordManager"],
827+
NamedRecordMixin["CustomRecordManager"],
828+
):
829+
custom_field: str
830+
"""Description of the field."""
831+
832+
class CustomRecordManager(
833+
RecordManagerBase[CustomRecord],
834+
NamedRecordManagerMixin[CustomRecord],
835+
):
836+
env_name = "custom.record"
837+
record_class = CustomRecord
838+
```
839+
840+
That's all that needs to be done. The additional attributes and/or methods
841+
should now be available on your record and manager objects.
842+
843+
The following mixins are provided with the Odoo Client library.
844+
845+
#### Named Records
846+
847+
If your record model has a unique `name` field on it (of `str` type),
848+
you can use the `NamedRecordMixin` and `NamedRecordManagerMixin` mixins
849+
to define the `name` field on the record class, and add the
850+
`get_by_name` method to your custom record manager class.
851+
852+
```python
853+
from __future__ import annotations
854+
855+
from openstack_odooclient import (
856+
NamedRecordManagerMixin,
857+
NamedRecordMixin,
858+
RecordBase,
859+
RecordManagerBase,
860+
)
861+
862+
class CustomRecord(
863+
RecordBase["CustomRecordManager"],
864+
NamedRecordMixin["CustomRecordManager"],
865+
):
866+
custom_field: str
867+
"""Description of the field."""
868+
869+
# Added by NamedRecordMixin:
870+
#
871+
# name: str
872+
# """The unique name of the record."""
873+
874+
class CustomRecordManager(
875+
RecordManagerBase[CustomRecord],
876+
NamedRecordManagerMixin[CustomRecord],
877+
):
878+
env_name = "custom.record"
879+
record_class = CustomRecord
880+
881+
# Added by NamedRecordManagerMixin:
882+
#
883+
# def get_by_name(...):
884+
# ...
885+
```
886+
887+
For more information on using record managers with unique `name` fields,
888+
see [Named Record Managers](index.md#named-record-managers).
889+
890+
#### Coded Records
891+
892+
If your record model has a unique `code` field on it (of `str` type),
893+
you can use the `CodedRecordMixin` and `CodedRecordManagerMixin` mixins
894+
to define the `code` field on the record class, and add the
895+
`get_by_code` method to your custom record manager class.
896+
897+
```python
898+
from __future__ import annotations
899+
900+
from openstack_odooclient import (
901+
CodedRecordManagerMixin,
902+
CodedRecordMixin,
903+
RecordBase,
904+
RecordManagerBase,
905+
)
906+
907+
class CustomRecord(
908+
RecordBase["CustomRecordManager"],
909+
CodedRecordMixin["CustomRecordManager"],
910+
):
911+
custom_field: str
912+
"""Description of the field."""
913+
914+
# Added by CodedRecordMixin:
915+
#
916+
# code: str
917+
# """The unique name for this record."""
918+
919+
class CustomRecordManager(
920+
RecordManagerBase[CustomRecord],
921+
CodedRecordManagerMixin[CustomRecord],
922+
):
923+
env_name = "custom.record"
924+
record_class = CustomRecord
925+
926+
# Added by CodedRecordManagerMixin:
927+
#
928+
# def get_by_code(...):
929+
# ...
930+
```
931+
932+
For more information on using record managers with unique `code` fields,
933+
see [Coded Record Managers](index.md#coded-record-managers).
934+
935+
### Creating Mixins
936+
937+
It is possible to create your own custom mixins to incorporate into
938+
custom record and manager classes.
939+
940+
There are two mixin types: **record mixins** and **record manager mixins**.
941+
942+
#### Record Mixins
943+
944+
Record mixins are used to add custom fields and methods to record classes.
945+
946+
Here is the full implementation of `NamedRecordMixin` as an example
947+
of a mixin for a record class, that simply adds the `name` field:
948+
949+
```python
950+
from __future__ import annotations
951+
952+
from typing import Generic
953+
954+
from openstack_odooclient import RM, RecordProtocol
955+
956+
class NamedRecordMixin(RecordProtocol[RM], Generic[RM]):
957+
name: str
958+
"""The unique name of the record."""
959+
```
960+
961+
A record mixin consists of a class that subclasses `RecordProtocol[RM]`
962+
(where `RM` is the type variable for a record manager class) to get the type
963+
hints for a record class' common fields and methods. `Generic[RM]` is also
964+
subclassed to make the mixin itself a generic class, to allow `RM` to be
965+
passed when creating a record class with the mixin.
966+
967+
Once you have the class, simply define any fields and methods you'd like
968+
to add.
969+
970+
You can then use the mixin as shown in [Using Mixins](#using-mixins).
971+
972+
When defining custom methods, in addition to accessing fields/methods
973+
defined within the mixin, fields/methods from the `RecordBase` class
974+
are also available:
975+
976+
```python
977+
from __future__ import annotations
978+
979+
from typing import Generic
980+
981+
from openstack_odooclient import RM, RecordProtocol
982+
983+
class NamedRecordMixin(RecordProtocol[RM], Generic[RM]):
984+
name: str
985+
"""The unique name of the record."""
986+
987+
def custom_method(self) -> None:
988+
self.name # str
989+
self._env.custom_method(self.id)
990+
```
991+
992+
#### Record Manager Mixins
993+
994+
Record manager mixins are expected to be mainly used to add custom methods
995+
to a record manager class.
996+
997+
```python
998+
from __future__ import annotations
999+
1000+
from typing import Generic
1001+
1002+
from openstack_odooclient import R, RecordManagerProtocol
1003+
1004+
class NamedRecordManagerMixin(RecordManagerProtocol[R], Generic[R]):
1005+
def custom_method(self, record: int | R) -> None:
1006+
self._env.custom_method( # self._env available from RecordManagerBase
1007+
record if isinstance(record, int) else record.id,
1008+
)
1009+
```
1010+
1011+
A record manager mixin consists of a class that subclasses
1012+
`RecordManagerProtocol[R]` (where `R` is the type variable for a record class)
1013+
to get the type hints for a record manager class' common attributes and
1014+
methods. `Generic[R]` is also subclassed to make the mixin itself a generic
1015+
class, to allow `R` to be passed when creating a record manager class
1016+
with the mixin.
1017+
7941018
## Extending Existing Record Types
7951019

7961020
The Odoo Client library provides *limited* support for extending the built-in record types.

docs/managers/index.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -873,9 +873,7 @@ The managers for these record types have additional methods for querying records
873873
* [Currencies](currency.md)
874874
* [OpenStack Customer Groups](customer-group.md)
875875
* [OpenStack Grant Types](grant-type.md)
876-
* [Partner Categories](partner-category.md)
877876
* [Pricelists](pricelist.md)
878-
* [Product Categories](product-category.md)
879877
* [OpenStack Reseller Tiers](reseller-tier.md)
880878
* [Sale Orders](sale-order.md)
881879
* [OpenStack Support Subscription Types](support-subscription-type.md)

docs/managers/partner-category.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ name: str
9999

100100
The name of the partner category.
101101

102+
Not guaranteed to be unique, even under the same parent category.
103+
102104
### `parent_id`
103105

104106
```python

docs/managers/product-category.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ The complete product category tree.
8989
name: str
9090
```
9191

92-
Name of the product category.
92+
The name of the product category.
93+
94+
Not guaranteed to be unique, even under the same parent category.
9395

9496
### `parent_id`
9597

0 commit comments

Comments
 (0)