Skip to content

Conversation

@pyctrl
Copy link
Collaborator

@pyctrl pyctrl commented Aug 21, 2025

Please after #129

@pyctrl pyctrl changed the title Class decorators Support class decoration for params and autoparams Aug 21, 2025
@ivankorobkov
Copy link
Owner

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 val

Is 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 val

But 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.

@pyctrl
Copy link
Collaborator Author

pyctrl commented Aug 25, 2025

Hello, Ivan!

Hope I understood you correctly 🙂

Let me share all the things behind the scene.

  1. I've found class-decoration support for autoparams — so for me class-decorations meant to be valid, intended and already present in project
  2. (btw it returns function instead of class which is a really bad move, I think)
  3. Another things to consider is the fact that autoparams and params can be used in same context for similar cases — they are kind of relatives and should share some commons in behavior (at least I interpreted them this may, am I right?)
  4. So I treated your examples for both cases — for autoparams and for params
  5. I also agree with your examples and their sameness — and decorating methods instead of a class is the right way for this cases

Okay, now let's jump into special case I've faced before the idea came up to me.

Imagine some library with (for ex.) Client having very massive __init__ with tons of args and kwargs`. Most of the have default values.
Now in my application I'd like to provide some injection for just one param. What can I do?

  • I can copy the whole signature of the original Client and decorate __init__ to meet my needs and just call super inside
  • same as previous one, but with only my argument defined explicitly and *args, **kwargs for others

I'd like to have less coupled codebase and avoid repeating the code.
But in this or other cases parent might be dynamic (it's python, it happens). Manual catching here them is not the way I'd like to handle injections.

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.
I'm open to discuss and find the solution you would like.

Hope I uncovered all the stuff behind the scene.

@pyctrl
Copy link
Collaborator Author

pyctrl commented Aug 25, 2025

Here is an example.

Let's say we have this base class in library called ddd.

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_adapter

And we want to have 2 our services to manage Orders and Customers.

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.
This is how it can be defined with feature from this PR:

@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.

@ivankorobkov
Copy link
Owner

So I treated your examples for both cases — for autoparams and for params

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:

@pyctrl
Copy link
Collaborator Author

pyctrl commented Sep 2, 2025

I'm on a vacation right now — I will reply at the end of the next week or so.

@ivankorobkov
Copy link
Owner

Ok, have a good time.

@pyctrl
Copy link
Collaborator Author

pyctrl commented Sep 20, 2025

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 autoparams:

  • autoparams primary way of usage is "method decorator"
  • only this way is documented
  • it has much more robust implementation
  • but autoparams can be used as "class decorator"
  • so we have 2 "method decorating mechanisms" (even before my patch — it is already present)

The second part is about class decorations in general:

  • autoparams and params decorators target callables (not data structures)
  • and these callables are methods and functions
  • if we do class-level decoration it can target
    1. attributes management (data structure)
    2. methods manipulations (behaviour)
  • first case is obviously not ours — so let's focus on the second one
  • any class instantiation under the hood will trigger methods — class itself is not a function/method
  • the chain of calls will be these (cpython source code)
    1. type.__init__ — may be overridden by metaclass
    2. __new__
    3. __init__
    4. (all of them accapt args and kwargs)
  • so we have at least 2 methods (type or metaclass __call__ could be skipped) to be injection targets

Then we shouldn't forget about other way of instance creation — classmethod factories are widely used to provide alternate class entrypoint.
It's another injection target too.

So it all sums up into the next thought — why do we have the only autoparams class decoration behaviour to manage the one and only __init__?
For me it's really contraversing. Actually I don't really agree with it. It seems wrong.

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.)
So class injection targeting is much wider than we already have in inject.

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.
And it is completely sane and valid to assign inject decorators params/autoparams to class multiple times simultaneously. I have such cases in my practice.

I'd like to have consistent, corresponding and convenient API and behaviour for class-level decorations in inject.
I want it for both autoparams and params — I don't see any difference between them in current context.

Let me address a few points from above.

But in the first case you invent a new method decorating mechanism.

Nope, I did not — it is already here for autoparams

And this method is not obvious to other developers at all. Nobody is expecting it.

  • I will skip addressing the same question to the current autoparams class-level decorating behaviour and go right to the "arguments for" instead of "against"
  • this functionality take place in the field of Python development
  • there should be a specific option for it
  • the documentation (docstrings and project docs) will address every feature, behaviour and detailed needed by user
  • this feature may be not required for each user — that's why the default behaviour won't require any from them
  • that's why this patch stays backward compatible and keeps default user flow

@inject.params("method", val=int)
It is too much magic

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 inject now.

Please, explain to me why you need such a feature.

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 autoparams.

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.
It's logical to use keyword-argument to extend functionality this way. Current behaviour unchanged.

Now params

def params(
    method_name: t.Union[str, _MISSING] = _MISSING,
    /,
    **args_to_classes: Binding,
) -> t.Callable:

I repeat, there is no fundamental difference between params and autoparams decorators.

This decorator focuses on keyword-arguments — the pairs of parameter names to be provided and keys to be loaded for injections.
params function is limited to keyword-arguments and nothing else should invade here.

So we have positional arguments free:

  • I decided to use them in this signature.
  • I treat positional arguments as valid for usage during calls in code (along with keyword-argument).
  • I also think optional positional arguments are fine and sane — it's valid pattern.

Putting altogether — to extend params functionality and keep backward compatibility we may use "optional positional argument for method name".

This way of thinking followed the existing logic working for autoparams — I followed the idea existing in current codebase.

And this is my reasoning for proposed technical solution.
I hope I was able to explain here too.


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.
And maybe we'd like to keep them "simple, straightforward and plain".

I can understand, accept and support this.

Going forward with this idea brings us to the next hypotheses:

  1. drop class-level decoration option for autoparams
  2. don't add method_name decorator parameter
  3. keep params and autoparams only for functions and methods — not classes/types
  4. make a new advanced option for class-level decorations
  5. create clsparams (or something like that) decorator for classes/types — I don't see any other signature option than my proposal above,
def clsparams(method_name: t.Union[str, _MISSING] = _MISSING, /, **args_to_classes: Binding) -> t.Callable:
  1. create clsautoparams (or something like that) decorator for classes/types —
def clsautoparams(*selected: str, method_name: t.Union[str, _MISSING] = _MISSING):

I'm okay with both ways

  1. make universal decorator (params and autoparams) which able to decorate classes/types and functions/methods
  2. split into "simple decorators" and "advanced — class-decorators"

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)?

@ivankorobkov
Copy link
Owner

Oh 🙂 I’ll take a look tomorrow.

@ivankorobkov
Copy link
Owner

So, on all you issues.

So it all sums up into the next thought — why do we have the only autoparams class decoration behaviour to manage the one and only init?
For me it's really contraversing. Actually I don't really agree with it. It seems wrong.

Historically. People added what they needed.

I'd like to have consistent, corresponding and convenient API and behaviour for class-level decorations in inject.
I want it for both autoparams and params — I don't see any difference between them in current context.

Sure, if you need you can add to @params:

if inspect.isclass(fn):
  types = t.get_type_hints(fn.__init__)
else:
  types = t.get_type_hints(fn)

But in the first case you invent a new method decorating mechanism.
Nope, I did not — it is already here for autoparams

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)

Going forward with this idea brings us to the next hypotheses:

  1. drop class-level decoration option for autoparams

No, it's already there. python-inject has been always backwards compatible.

  1. don't add method_name decorator parameter

Yes, please, do not add it.

  1. keep params and autoparams only for functions and methods — not classes/types

You may add inspect.isclass(fn) to params if you need.

  1. make a new advanced option for class-level decorations
  2. create clsparams (or something like that) decorator for classes/types — I don't see any other signature option than my proposal above,
    def clsparams(method_name: t.Union[str, _MISSING] = _MISSING, /, **args_to_classes: Binding) -> t.Callable:
  3. create clsautoparams (or something like that) decorator for classes/types —
    def clsautoparams(*selected: str, method_name: t.Union[str, _MISSING] = _MISSING):

You may add explicit class decorators which accept method names. Please, do not add optional missing method names to existing @params or @autoparams. Something like these or whatever you want:

@inject.method_params('my_method', ...)
class MyClass

I'm okay with both ways

  1. make universal decorator (params and autoparams) which able to decorate classes/types and functions/methods

You may add class support to @params if you need.

  1. split into "simple decorators" and "advanced — class-decorators"

Already answered this.

@pyctrl
Copy link
Collaborator Author

pyctrl commented Oct 1, 2025

Oh, I see now. Got your point — it's in the very local context.
Yeah, okay — I will do separate functions for this extra features.

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.

@ivankorobkov
Copy link
Owner

Great. Thank you for your understanding. Sure, take your time. Both this PR or a new PR are OK.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants