Skip to content

Commit b544728

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. To make type-hinted record manager mixins possible, a **protocol** class called `RecordManagerProtocol` had to be created documenting the attributes and methods on the `RecordManagerBase` class intended for use by implementing classes. This protocol is used to set the type of the `self` parameter on mixin methods. `RecordManagerBase` now subclasses this protocol. 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 moved into `RecordManagerBase` to make it available for use in any custom manager class.
1 parent 33e5d51 commit b544728

33 files changed

+1549
-802
lines changed

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: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -791,6 +791,257 @@ 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+
813+
```python
814+
from __future__ import annotations
815+
816+
from openstack_odooclient import (
817+
NamedRecordManagerMixin,
818+
NamedRecordMixin,
819+
RecordBase,
820+
RecordManagerBase,
821+
)
822+
823+
class CustomRecord(RecordBase["CustomRecordManager"], NamedRecordMixin):
824+
custom_field: str
825+
"""Description of the field."""
826+
827+
class CustomRecordManager(
828+
RecordManagerBase[CustomRecord],
829+
NamedRecordManagerMixin[CustomRecord],
830+
):
831+
env_name = "custom.record"
832+
record_class = CustomRecord
833+
```
834+
835+
That's all that needs to be done. The additional attributes and/or methods
836+
should now be available on your record and manager objects.
837+
838+
!!! note
839+
840+
Depending on how the mixin you're trying to use is implemented
841+
(for instance, if it provides custom methods), you may need to
842+
specify your record or manager class as a generic type argument
843+
when adding the mixin to your class.
844+
845+
The following mixins are provided with the Odoo Client library.
846+
847+
#### Named Records
848+
849+
If your record model has a unique `name` field on it (of `str` type),
850+
you can use the `NamedRecordMixin` and `NamedRecordManagerMixin` mixins
851+
to define the `name` field on the record class, and add the
852+
`get_by_name` method to your custom record manager class.
853+
854+
!!! note
855+
856+
`NamedRecordManagerMixin` requires that the record class be passed
857+
as a generic type argument to the mixin class when adding it to
858+
the manager class.
859+
860+
```python
861+
from __future__ import annotations
862+
863+
from openstack_odooclient import (
864+
NamedRecordManagerMixin,
865+
NamedRecordMixin,
866+
RecordBase,
867+
RecordManagerBase,
868+
)
869+
870+
class CustomRecord(RecordBase["CustomRecordManager"], NamedRecordMixin):
871+
custom_field: str
872+
"""Description of the field."""
873+
874+
# Added by NamedRecordMixin:
875+
#
876+
# name: str
877+
# """The unique name of the record."""
878+
879+
class CustomRecordManager(
880+
RecordManagerBase[CustomRecord],
881+
NamedRecordManagerMixin[CustomRecord],
882+
):
883+
env_name = "custom.record"
884+
record_class = CustomRecord
885+
886+
# Added by NamedRecordManagerMixin:
887+
#
888+
# def get_by_name(...):
889+
# ...
890+
```
891+
892+
For more information on using record managers with unique `name` fields,
893+
see [Named Record Managers](index.md#named-record-managers).
894+
895+
#### Coded Records
896+
897+
If your record model has a unique `code` field on it (of `str` type),
898+
you can use the `CodedRecordMixin` and `CodedRecordManagerMixin` mixins
899+
to define the `code` field on the record class, and add the
900+
`get_by_code` method to your custom record manager class.
901+
902+
!!! note
903+
904+
`CodedRecordManagerMixin` requires that the record class be passed
905+
as a generic type argument to the mixin class when adding it to
906+
the manager class.
907+
908+
```python
909+
from __future__ import annotations
910+
911+
from openstack_odooclient import (
912+
CodedRecordManagerMixin,
913+
CodedRecordMixin,
914+
RecordBase,
915+
RecordManagerBase,
916+
)
917+
918+
class CustomRecord(RecordBase["CustomRecordManager"], CodedRecordMixin):
919+
custom_field: str
920+
"""Description of the field."""
921+
922+
# Added by CodedRecordMixin:
923+
#
924+
# code: str
925+
# """The unique name for this record."""
926+
927+
class CustomRecordManager(
928+
RecordManagerBase[CustomRecord],
929+
CodedRecordManagerMixin[CustomRecord],
930+
):
931+
env_name = "custom.record"
932+
record_class = CustomRecord
933+
934+
# Added by CodedRecordManagerMixin:
935+
#
936+
# def get_by_code(...):
937+
# ...
938+
```
939+
940+
For more information on using record managers with unique `code` fields,
941+
see [Coded Record Managers](index.md#coded-record-managers).
942+
943+
### Creating Mixins
944+
945+
It is possible to create your own custom mixins to incorporate into
946+
custom record and manager classes.
947+
948+
There are two mixin types: **record mixins** and **record manager mixins**.
949+
950+
#### Record Mixins
951+
952+
For simple cases creating a record mixin is easy; all you need are classes
953+
with the additional attributes or methods you want defined on them.
954+
955+
Here is the full implementation of `NamedRecordMixin` as an example
956+
of a mixin for a record class, that simply adds the `name` field:
957+
958+
```python
959+
class NamedRecordMixin:
960+
name: str
961+
"""The unique name of the record."""
962+
```
963+
964+
You can then use the mixin as shown in [Using Mixins](#using-mixins).
965+
966+
For methods that only use other attributes/methods defined within the mixin,
967+
you can define them as normal:
968+
969+
```python
970+
class NamedRecordMixin:
971+
name: str
972+
"""The unique name of the record."""
973+
974+
def custom_method(self) -> None:
975+
self.name # type => str
976+
```
977+
978+
Where it becomes more complex is when you need to access attributes/methods
979+
from the `RecordBase` class, and you want type hinting.
980+
981+
Because mixins are independent classes and not subclasses of `RecordBase`,
982+
they are not annotated with the attributes/methods from that class, even
983+
though they actually would be usable when you add the mixin to your record
984+
class.
985+
986+
You can make these annotations available on a per-method basis by overriding
987+
the type of `self` to `RecordProtocol`, as shown below.
988+
989+
```python
990+
from __future__ import annotations
991+
992+
from openstack_odooclient import RecordProtocol
993+
994+
class NamedRecordMixin:
995+
name: str
996+
"""The unique name of the record."""
997+
998+
def custom_method(self: RecordProtocol) -> None:
999+
self._env.custom_method(self.id)
1000+
```
1001+
1002+
This makes the annotations `self._env` and all the other attributes and methods
1003+
available from the `RecordBase` class evaluate correctly, but with the disadvantage
1004+
of making annotations from within the mixin itself unavailable. `self.name` in the
1005+
above example **is** actually available for use, but using it in `custom_method`
1006+
would result in an error in type checkers that would need to be silenced.
1007+
1008+
This is a limitation of Python's typing system with regard to mixins, and does not
1009+
appear to have any other workaround available at this time.
1010+
1011+
#### Record Manager Mixins
1012+
1013+
For the most part, record manager mixins are expected to be used to add custom
1014+
methods to a record manager class, and use other methods on the base record
1015+
manager class.
1016+
1017+
```python
1018+
from __future__ import annotations
1019+
1020+
from typing import Generic
1021+
1022+
from openstack_odooclient import Record, RecordManagerProtocol
1023+
1024+
class NamedRecordManagerMixin(Generic[Record]):
1025+
def custom_method(
1026+
self: RecordManagerProtocol[Record],
1027+
record: int | Record,
1028+
) -> None:
1029+
...
1030+
```
1031+
1032+
As with record mixins, because mixins are independent classes and not
1033+
subclasses of `RecordManagerBase`, they are not annotated with the
1034+
attributes/methods from that class, even though they actually would be usable
1035+
when you add the mixin to your record class. `self` is being overridden to
1036+
`RecordManagerProtocol` in the example above to allow type checking for record
1037+
manager base class attributes to work.
1038+
1039+
When `RecordManagerProtocol` is being used, any other attributes or methods
1040+
defined on the mixin class itself are still available from other methods,
1041+
but type checking will fail due to `self` being overridden. This is a
1042+
limitation of Python's typing system with regard to mixins, and does not
1043+
appear to have any other workaround available at this time.
1044+
7941045
## Extending Existing Record Types
7951046

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

docs/managers/voucher-code.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ until it expires.
212212
name: str
213213
```
214214

215-
The unique name of this voucher code.
215+
The automatically generated name of this voucher code.
216216

217217
This uses the code specified in the record as-is.
218218

openstack_odooclient/__init__.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,8 @@
1616
from __future__ import annotations
1717

1818
from .base.client import ClientBase
19-
from .base.record import FieldAlias, ModelRef, RecordBase
20-
from .base.record_manager import RecordManagerBase
21-
from .base.record_manager_coded import CodedRecordManagerBase
22-
from .base.record_manager_named import NamedRecordManagerBase
23-
from .base.record_manager_with_unique_field import (
24-
RecordManagerWithUniqueFieldBase,
25-
)
19+
from .base.record import FieldAlias, ModelRef, RecordBase, RecordProtocol
20+
from .base.record_manager import RecordManagerBase, RecordManagerProtocol
2621
from .client import Client
2722
from .exceptions import (
2823
ClientError,
@@ -77,6 +72,7 @@
7772
VolumeDiscountRangeManager,
7873
)
7974
from .managers.voucher_code import VoucherCode, VoucherCodeManager
75+
from .mixins.named_record import NamedRecordManagerMixin, NamedRecordMixin
8076

8177
__all__ = [
8278
"AccountMove",
@@ -86,7 +82,6 @@
8682
"Client",
8783
"ClientBase",
8884
"ClientError",
89-
"CodedRecordManagerBase",
9085
"Company",
9186
"CompanyManager",
9287
"Credit",
@@ -106,7 +101,8 @@
106101
"GrantTypeManager",
107102
"ModelRef",
108103
"MultipleRecordsFoundError",
109-
"NamedRecordManagerBase",
104+
"NamedRecordManagerMixin",
105+
"NamedRecordMixin",
110106
"Partner",
111107
"PartnerCategory",
112108
"PartnerCategoryManager",
@@ -123,8 +119,9 @@
123119
"ProjectManager",
124120
"RecordBase",
125121
"RecordManagerBase",
126-
"RecordManagerWithUniqueFieldBase",
122+
"RecordManagerProtocol",
127123
"RecordNotFoundError",
124+
"RecordProtocol",
128125
"ReferralCode",
129126
"ReferralCodeManager",
130127
"Reseller",
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Copyright (C) 2025 Catalyst Cloud Limited
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12+
# implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
from __future__ import annotations
17+
18+
from .base import RecordBase, RecordProtocol
19+
from .types import FieldAlias, ModelRef
20+
21+
__all__ = ["FieldAlias", "ModelRef", "RecordBase", "RecordProtocol"]

0 commit comments

Comments
 (0)