-
Notifications
You must be signed in to change notification settings - Fork 84
Support class decoration for params and autoparams #130
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
|
Hi! Sorry, but I can't accept this. You see, this: @inject.params(val=int)
@inject.params("method", val=int)
class MyClass:
def __init__(self, val: int):
self.val = val
def method(self, val: int):
return valIs completely identical to: class MyClass:
@inject.params(val=int)
def __init__(self, val: int):
self.val = val
@inject.params("method", val=int)
def method(self, val: int):
return valBut in the first case you invent a new method decorating mechanism. And this method is not obvious to other developers at all. Nobody is expecting it. Please, explain to me why you need such a feature. |
|
Hello, Ivan! Hope I understood you correctly 🙂 Let me share all the things behind the scene.
Okay, now let's jump into special case I've faced before the idea came up to me. Imagine some library with (for ex.)
I'd like to have less coupled codebase and avoid repeating the code. The only thing I came up with for such complex cases is class-decorations. Class decorators don't care for direct members of the class and can dynamically access attributes from the whole MRO-tree. This problem is very relevant for my current project, so I'm really involved here. Hope I uncovered all the stuff behind the scene. |
|
Here is an example. Let's say we have this base class in library called class CrudApplicationService:
def __init__(
self,
db_service,
domain_adapter,
domain_publisher,
event_repository,
interchange_adapter,
interchange_publisher,
job_service,
job_adapter,
log_service,
scheduler_adapter,
):
# Dependencies
self.db_service = db_service
self.domain_adapter = domain_adapter
self.domain_publisher = domain_publisher
self.event_repository = event_repository
self.interchange_adapter = interchange_adapter
self.interchange_publisher = interchange_publisher
self.job_service = job_service
self.job_adapter = job_adapter
self.log_service = log_service
self.scheduler_adapter = scheduler_adapterAnd we want to have 2 our services to manage Right now we can class OrdersCrudApplicationService:
@params(db_service=OrdersDbService)
def __init__(
self,
db_service,
domain_adapter,
domain_publisher,
event_repository,
interchange_adapter,
interchange_publisher,
job_service,
job_adapter,
log_service,
scheduler_adapter,
):
super().__init__(
db_service=db_service,
domain_adapter=domain_adapter,
domain_publisher=domain_publisher,
event_repository=event_repository,
interchange_adapter=interchange_adapter,
interchange_publisher=interchange_publisher,
job_service=job_service,
job_adapter=job_adapter,
log_service=log_service,
scheduler_adapter=scheduler_adapter,
)
class ClientsCrudApplicationService:
@params(db_service=ClientsDbService)
def __init__(
self,
db_service,
domain_adapter,
domain_publisher,
event_repository,
interchange_adapter,
interchange_publisher,
job_service,
job_adapter,
log_service,
scheduler_adapter,
):
super().__init__(
db_service=db_service,
domain_adapter=domain_adapter,
domain_publisher=domain_publisher,
event_repository=event_repository,
interchange_adapter=interchange_adapter,
interchange_publisher=interchange_publisher,
job_service=job_service,
job_adapter=job_adapter,
log_service=log_service,
scheduler_adapter=scheduler_adapter,
)In this cases I'd like to reuse features from the library and just define some flavor with presets. @params(db_service=OrdersDbService)
class OrdersCrudApplicationService: ...
@params(db_service=ClientsDbService)
class ClientsCrudApplicationService: ...P.S. I used this library code and design to show this example. |
Yes. I'm talking here specifically about this. It is too much magic: @inject.params("method", val=int)
class MyClass:But class decorator are ok, of course, as in: @inject.params(val=int)
class MyClass: |
|
I'm on a vacation right now — I will reply at the end of the next week or so. |
|
Ok, have a good time. |
|
A bit later but I'm here. 🙂 Let's go back to our topic. I'm not sure about you concerns so I'll just drop down my thought on this topic and you will adjust me. Let me start with a bit of analysis on
The second part is about class decorations in general:
Then we shouldn't forget about other way of instance creation — So it all sums up into the next thought — why do we have the only But let's talk about "non class instantiation methods" — other methods may require explicit late dependency injections (it may be some sink to write the method result or anything else.) Don't forget about class inheritance we've mention in our discussion earlier — class-level decorators shine when you need to wrap parent method without boilerplating proxy-method in current class. I love python very much and I use all these techniques to make my codebase nice and good. I need all this things to be addressed — that's my reasoning and angles/horizon of this domain/question/topic. So it's required to have an option to specify method name during class-level decorations. I'd like to have consistent, corresponding and convenient API and behaviour for class-level decorations in Let me address a few points from above.
Nope, I did not — it is already here for
It's not if you take into account all points from above for class inheritance, instantiation, factories and related things — the scope to be addressed during injection is much wider than original one we have in
I hope I was able to explain this time. 🙂 This material covers the domain I'm facing here and around in my projects and codebases. Finally, let's talk about the solutions. I will describe both signatures from my proposal. Let's start with def autoparams(*selected: str, method_name: t.Union[str, _MISSING] = _MISSING):This decorator focuses on positional arguments — the list of argument names to be injected. Now def params(
method_name: t.Union[str, _MISSING] = _MISSING,
/,
**args_to_classes: Binding,
) -> t.Callable:I repeat, there is no fundamental difference between This decorator focuses on keyword-arguments — the pairs of parameter names to be provided and keys to be loaded for injections. So we have positional arguments free:
Putting altogether — to extend This way of thinking followed the existing logic working for And this is my reasoning for proposed technical solution. But I may guess I feel your inner concerns — I'll try to address them and guess for your reasoning. We have 2 basic and simple decorators — at first glance. I can understand, accept and support this. Going forward with this idea brings us to the next hypotheses:
def clsparams(method_name: t.Union[str, _MISSING] = _MISSING, /, **args_to_classes: Binding) -> t.Callable:
def clsautoparams(*selected: str, method_name: t.Union[str, _MISSING] = _MISSING):I'm okay with both ways
So here we are. I drop a ton of text but addressed my need, theory, guesses about your concerns, advocated my proposal and made a new one for different values. What do you think now? Which direction we can move (these 2 or any of your suggestions here)? |
|
Oh 🙂 I’ll take a look tomorrow. |
|
So, on all you issues.
Historically. People added what they needed.
Sure, if you need you can add to if inspect.isclass(fn):
types = t.get_type_hints(fn.__init__)
else:
types = t.get_type_hints(fn)
Nope, you added this functionality. It was never there. This is the code you added: def autoparams(*selected: str, method_name: t.Union[str, _MISSING] = _MISSING):
# ...
def autoparams_decorator(cls_or_func: t.Callable[..., T]) -> t.Callable[..., T]:
# nonlocal method_name
# is_class = inspect.isclass(cls_or_func)
# if is_class:
# if method_name is _MISSING:
# method_name = "__init__"
# fn = getattr(cls_or_func, method_name)
# elif method_name is not _MISSING:
# raise TypeError("You can't provide method name with function")
# else:
# fn = cls_or_func
fn, cls, m_name = _parse_cls_or_fn(cls_or_func, method_name)
type_hints = t.get_type_hints(fn)
No, it's already there.
Yes, please, do not add it.
You may add
You may add explicit class decorators which accept method names. Please, do not add optional @inject.method_params('my_method', ...)
class MyClass
You may add class support to
Already answered this. |
|
Oh, I see now. Got your point — it's in the very local context. And I have to manage my personal stuff next couple of weeks, so I won't be able to write new patch soon. I can do it in this PR or in separate if you don't this to stay openned for such a long time. |
|
Great. Thank you for your understanding. Sure, take your time. Both this PR or a new PR are OK. |
Please after #129