Skip to content

Commit

Permalink
Enable custom object subclasses (#435)
Browse files Browse the repository at this point in the history
  • Loading branch information
jacobtomlinson authored Jul 4, 2024
1 parent 992c5df commit 4e721b1
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 5 deletions.
120 changes: 117 additions & 3 deletions docs/object.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,12 +305,16 @@ Instead we have focused on making the API extensible so that if there isn't a bu

### Extending the objects API

To create your own objects which will be a subclass of [](#kr8s.objects.APIObject), however we recommend you use the {py:func}`new_class <kr8s.objects.new_class>` class factory to ensure all of the required attributes are set. These will be used when constructing API calls by the API client.
To create your own objects we recommend you use the {py:func}`new_class <kr8s.objects.new_class>` class factory to ensure all of the required attributes are set. These will be used when constructing API calls by the API client.

```{danger}
Manually subclassing `APIObject` is considered an advanced topic and requires strong understanding of `kr8s` internals and how the sync/async wrapping works. For now it is recommended that you do not do this.
While all objects generated by `new_class` are a subclass of [](#kr8s.objects.APIObject) subclassing `APIObject` manually is considered an advanced topic and requires strong understanding of `kr8s` internals and how the sync/async wrapping works. We recommend that you do not do this.
```

`````{tab-set}
````{tab-item} Sync
:sync: sync
```python
from kr8s.objects import new_class
Expand All @@ -320,11 +324,31 @@ CustomObject = new_class(
namespaced=True,
)
```
````
````{tab-item} Async
:sync: async
```python
from kr8s.asyncio.objects import new_class
CustomObject = new_class(
kind="CustomObject",
version="example.org",
namespaced=True,
)
```
````
`````

The [](#kr8s.objects.APIObject) base class contains helper methods such as `.create()`, `.delete()`, `.patch()`, `.exists()`, etc.

There are also optional helpers that can be enabled for resources that support them. For example you can enable `.scale()` for resources which support updating the number of replicas.

`````{tab-set}
````{tab-item} Sync
:sync: sync
```python
from kr8s.objects import new_class
Expand All @@ -336,19 +360,89 @@ CustomScalableObject = new_class(
scalable_spec="replicas", # The spec key to patch when scaling
)
```
````
````{tab-item} Async
:sync: async
```python
from kr8s.asyncio.objects import new_class
CustomScalableObject = new_class(
kind="CustomObject",
version="example.org",
namespaced=True,
scalable=True,
scalable_spec="replicas", # The spec key to patch when scaling
)
```
````
`````

If you wish to extend the API of your custom class you can directly subclass the type returned by `new_class`.

`````{tab-set}
````{tab-item} Sync
:sync: sync
```python
from kr8s.objects import new_class
class CustomObject(new_class("CustomObject", version="example.org", namespaced=True)):
def my_custom_method(self) -> str:
return "foo"
```
````
````{tab-item} Async
:sync: async
```python
from kr8s.asyncio.objects import new_class
class CustomObject(new_class("CustomObject", version="example.org", namespaced=True)):
async def my_custom_method(self) -> str:
return "foo"
```
````
`````



### Using custom objects with other `kr8s` functions

When using the [`kr8s` API](client) some methods such as `kr8s.get("pods")` will want to return kr8s objects, in this case a `Pod`. The API client handles this by looking up all of the subclasses of [`APIObject`](#kr8s.objects.APIObject) and matching the `kind` against the kind returned by the API. If the API returns a kind of object that there is no kr8s object to deserialize into it will create a new class for you automatically.

`````{tab-set}
````{tab-item} Sync
:sync: sync
```python
import kr8s
cos = kr8s.get("customobjects") # If a resource called `customobjects` exists on the server a class will be created dynamically for it
```
````
````{tab-item} Async
:sync: async
```python
import kr8s.asyncio
cos = await kr8s.asyncio.get("customobjects") # If a resource called `customobjects` exists on the server a class will be created dynamically for it
```
````
`````

When you create your own custom objects with `new_class` the client is then able to use those objects in its response.

`````{tab-set}
````{tab-item} Sync
:sync: sync
```python
import kr8s
from kr8s.objects import new_class
Expand All @@ -362,9 +456,29 @@ CustomObject = new_class(
cos = kr8s.get("customobjects") # Will return a list of CustomObject instances
```
````
````{tab-item} Async
:sync: async
```python
import kr8s.asyncio
from kr8s.asyncio.objects import new_class
CustomObject = new_class(
kind="CustomObject",
version="example.org",
namespaced=True,
asyncio=False,
)
cos = await kr8s.get("customobjects") # Will return a list of CustomObject instances
```
````
`````

```{note}
If multiple subclasses of [`APIObject`](#kr8s.objects.APIObject) are created with the same API version and kind the first one registered will be used.
If multiple subclasses of [`APIObject`](#kr8s.objects.APIObject) are created with the same API version and kind the last one registered will be used. This allows you to create your own subclasses of core objects or objects returned by `new_class`.
```

## Interoperability with other libraries
Expand Down
7 changes: 5 additions & 2 deletions kr8s/_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -1575,6 +1575,7 @@ def get_class(
Raises:
KeyError: If no object is registered for the given kind and version.
"""
result = None
group = None
if "/" in kind:
kind, version = kind.split("/", 1)
Expand Down Expand Up @@ -1606,16 +1607,18 @@ def _walk_subclasses(cls):
if (group is None or cls_group == group) and (
version is None or cls_version == version
):
return cls
result = cls
if (
group
and not version
and "." in group
and cls_group == group.split(".", 1)[1]
and cls_version == group.split(".", 1)[0]
):
return cls
result = cls

if result:
return result
raise KeyError(
f"No object registered for {kind}{'.' + group if group else ''}. "
"See https://docs.kr8s.org/en/stable/object.html#extending-the-objects-api "
Expand Down
11 changes: 11 additions & 0 deletions kr8s/tests/test_objects.py
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,17 @@ async def test_new_sync_class_registration_from_spec():
assert not my_sync_resource_instance._asyncio


async def test_class_registration_multiple_subclass():
class MyResource(new_class("MyResource.foo.kr8s.org/v1alpha1")):
def my_custom_method(self) -> str:
return "foo"

assert get_class("MyResource", "foo.kr8s.org/v1alpha1") is MyResource

r = MyResource({})
assert r.my_custom_method() == "foo"


async def test_deployment_scale(example_deployment_spec):
deployment = await Deployment(example_deployment_spec)
await deployment.create()
Expand Down

0 comments on commit 4e721b1

Please sign in to comment.