kr8s-org / kr8s

A batteries-included Python client library for Kubernetes that feels familiar for folks who already know how to use kubectl
https://kr8s.org
BSD 3-Clause "New" or "Revised" License
839 stars 45 forks source link

Enable custom object subclasses #435

Closed jacobtomlinson closed 4 months ago

jacobtomlinson commented 4 months ago

The goal of this PR is to make it easier for folks to create custom API object subclasses.

Currently we support using new_class to generate a new object. But now that we support this automatically in #432 the only reasons to manually create a class are to either use classmethods like .get() and .list(), or to create custom methods on the subclass.

Creating manual subclasses of APIObject in the sync API is a bit tricky as you need to jump through some hoops to get the sync wrapping working, hence why we recommend folks use kr8s.objects.new_class which does this for you. But that kind of defeats the goal of creating objects with custom methods.

If you subclass the class you generate with new_class the subclass is ignored because kr8s._objects.get_class returns the first match. This PR modifies the behaviour of get_class to return the last match, which allows you to subclass any APIObject based class to override which one should be used. This means that users can use new_class to generate a base, and then subclass that to create their custom class. You don't even need to assign the base to anything, you can just use it in the class definitiion.

from kr8s.objects import new_class

class CustomObject(new_class("CustomObject", version="example.org", namespaced=True)):

    def my_custom_method(self) -> str:
        return "foo"

This works nicely because the CustomObject class can call any of it's parent methods and they will be sync wrapped correctly and as far as the user if concerned they are just creating a new sync object.

You can also do the same with the async version of new_class to create async objects.

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"

The only complexity would be if a user wants to create both a sync and async implementation like we provide with the core objects. In that case they would need to create an async class and then use the kr8s._async_utils.sync() decorator to convert it. They would also need to ensure the async methods are using the async_ named methods on the base class so that they don't get clobbered when it is sync wrapped. In reality I don't think many folks will want to create both a sync and async class, and if they do it's probably better for them to define them both separately and suffer a small amount of duplication.

In theory with this change users could also subclass core objects. For example if they wanted to extend Pod they could do so.

import kr8s
from kr8s.objects import Pod

class MyAwesomePod(Pod):
    def some_extra_method(self):
        ...

pods = kr8s.get("pods")  # Now returns a list of MyAwesomePod objects instead of Pod objects

This gives users access to more footguns, but also might give a bit more flexibility if they find a limitation in the kr8s implementations of objects. For example they could override an existing method and change it's behaviour, but that may break other code which is expecting a particular protocol. If people start doing this we might want to think about using more flexible protocols in places.

codecov[bot] commented 4 months ago

Codecov Report

Attention: Patch coverage is 91.66667% with 1 line in your changes missing coverage. Please review.

Project coverage is 95.30%. Comparing base (87063fc) to head (717ecb0). Report is 121 commits behind head on main.

Files with missing lines Patch % Lines
kr8s/_objects.py 80.00% 1 Missing :warning:
Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #435 +/- ## ========================================== + Coverage 94.61% 95.30% +0.68% ========================================== Files 29 30 +1 Lines 3141 3748 +607 ========================================== + Hits 2972 3572 +600 - Misses 169 176 +7 ```

:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.