Support callable as a source for the registry key
I suggest to support a callable with arguments as a source for the registry key. Currently only properties are supported. In my use case, registry key comes from a function that takes output type as an argument. I would like not to introduce extra property just for the registry.
I suggest something like the following:
class Someclass:
def schema(self, output_type):
....
registry = ClassRegistry(attr_name="schema", callable_kwargs={"output_type": str})
For the use case without the arguments the following addition works (last 2 lines):
class BaseMutableRegistry(BaseRegistry[T], ABC):
...
def register( ...
...
# ``@register`` usage:
if is_class(key):
if typing.TYPE_CHECKING:
key = typing.cast(D, key)
if self.attr_name:
attr_key = getattr(key, self.attr_name)
if callable(attr_key):
attr_key = attr_key(key)
...
Thank you for the registry!
Kia ora @tokarenko thanks for writing this up!
Can you tell me a bit more about your use case? I think there a few different ways that we can approach this, depending on how Someclass is used.
Here are a couple of ideas; let me know if any of these are on the mark or if you had something different in mind.
You can pass arguments to the init method for the returned class instance:
from class_registry import ClassRegistry
class Pokemon:
def __init__(self, name: str):
self.name = name
pokedex = ClassRegistry[Pokemon](attr_name="element")
@pokedex.register
class ElectricPokemon(Pokemon):
element = "electric"
pika = pokedex.get("electric", name="Pikachu") # <- Passing kwarg to ``__init__()``
assert pika.name == "Pikachu"
If you want to create a class factory, try invoking ClassRegistry.register inside of the factory method, so that it can decide what key to pass to the register method:
from inspect import isclass
from typing import Type
from class_registry import ClassRegistry
class Pokemon:
element: str
class PokemonRegistrar:
def __init__(self, registry: ClassRegistry[Pokemon]):
self._registry = registry
def generate(self, element_: str) -> Type[Pokemon]:
@self._registry.register(element_) # <- Specifying registry key from arguments
class TypedPokemon(Pokemon):
element = element_
return TypedPokemon
pokedex = ClassRegistry[Pokemon]()
biolab = PokemonRegistrar(pokedex)
ElectricPokemon = biolab.generate("electricity")
assert isclass(ElectricPokemon)
assert ElectricPokemon.element == "electricity"
pika = pokedex["electricity"]
assert isinstance(pika, ElectricPokemon)
Thank you @todofixthis for your expanded reply and suggestion. I may try to wrap the decorator. Still I would be grateful if you could alleviate the need for it. I think that in general case registry key may come for a callable as well as from a property. Supporting callables without arguments is as easy as two lines of code I provided above which do not impact current API in any way. It will suffice for my use case.
Recently I realized that the registry key attribute may be of nested object, e.g. "obj1.obj2". To support nested attribute access and a callable without arguments I suggest the following modification (last 3 lines):
class BaseMutableRegistry(BaseRegistry[T], ABC):
...
def register( ...
...
# ``@register`` usage:
if is_class(key):
if typing.TYPE_CHECKING:
key = typing.cast(D, key)
if self.attr_name:
attr_key = functools.reduce(getattr, self.attr_name.split("."), key)
if callable(attr_key):
attr_key = attr_key()
...
@todofixthis , will you introduce this change? I can see no downsides. There are only changes for better ) I am packaging my code for production, and I would like to avoid another wrapper/fork...
I appreciate your enthusiasm, but you haven't actually answered my question. When I asked about your use case, I wasn't asking about the solution you were trying to build but rather the larger engineering problem you're trying to solve. My concern is that what you've described is so different from any other usage of this library that you might be trying to use this library in a way for which it wasn't designed (XY Problem).
Before I invest my free time into writing tests, implementing new functionality, handling edge cases (there are quite a few that you don't seem to have considered), updating documentation, preparing and publishing a release, and communicating with users, I need to be sure that this is in line with the overall design of the library.
@todofixthis , I am sorry for the misunderstanding. I will restate the problem:
"... in general case registry key may come from a callable as well as from a property".
Later I realized that the registry key may come from a nested object, e.g. "obj1.obj2". I am trying to register object with registry key located in a callable of a nested object (settings.schema):
class Settings:
def schema() -> Hashable:
....
class ObjectToRegister:
settings: Settings
Currently only direct properties are supported. I think it is reasonable to support nested properties "settings.schema" as many object in practice are compound. Although support of callables may seem a stretch, I think it is easy enough to support at least callables without arguments. Property does not work in my case, as I need to support many schema formats. So schema() is actually parametrized with optional format argument in my case. If called without arguments it returns hashable schema for the registry key.