Skip to content

Free up #initialize for command subclasses - #167

Open
timriley wants to merge 3 commits into
mainfrom
reduce-command-boilerplate
Open

Free up #initialize for command subclasses#167
timriley wants to merge 3 commits into
mainfrom
reduce-command-boilerplate

Conversation

@timriley

@timriley timriley commented Sep 3, 2026

Copy link
Copy Markdown
Member

Free up #initialize in command subclasses so they can use it for their own purposes only.

stdin:, stdout:, and stderr: are still injectable dependencies, but these are handled by .new, which sets them as instance variables directly, before forwarding all other args onto #initialize.

Aside from reducing boilerplate for users in their command subclasses, this also decreases the risk of bugs. Without this, every command subclass that adds its own #initialize needs to remember to accept **args and then call super(**args), otherwise the streams would never be set.

I thought of this change as part of updating hanami-cli to use the new streams provided as part of dry-cli v2. Now that I've made this change here, my next step will be to show what the updated hanami-cli commands look like. Stand by for that, but in the meantime, I'm still interested in feedback on this change in isolation :)

@timriley
timriley requested a review from a team September 3, 2026 12:56
@timriley timriley changed the title Reduce boilerplate for command subclasses Free up #initialize for command subclasses Sep 3, 2026
Free up `#initialize` in command subclasses so they can use it for their own purposes only.

`stdin:`, `stdout:`, and `stderr:` are still injectable dependencies, but these are handled by `.new`, which sets them as instance variables directly, before forwarding all other args onto `#initialize`.

This not only reduces boilerplate for authors of command subclasses, but also decreases the risk of bugs. Without this, every command subclass that needs its own initialize would also need to remember to include `**args` and then call `super(**args)`, otherwise the streams would never be set.
@timriley
timriley force-pushed the reduce-command-boilerplate branch from 9b6d522 to be06df2 Compare September 3, 2026 13:05

@parndt parndt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this!

@aaronmallen aaronmallen left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is good soup

Do away with protected methods, make `#with_streams` public, and make `#set_Streams` private.
This formalises the behaviour we introduced where the `stderr:`, `stdin:`, `stdout:` args were assigned to ivars without subclass `#initialize` methods needing to worry about receiving those as `**args` and forwarding them to `super`.

Now our users can take advantage of this behavior. It’s common for a CLI app to require some “standard” dependencies be provided to every command class through their `#initialize` methods. These can now be set up like so:

```
# In a CLI app’s base command class

# `inflector:` is a standard dependency
def self.auto_assign_keywords = super + %i[inflector]

# Supply default values here
private def auto_assign(inflector: Dry::Inflector.new, **kwargs)
  super(**kwargs)
  @inflector = inflector
end
```

This will make `inflector:` work as an argument to `.new`, and assign its value to an ivar automatically (via `#auto_assign`) before `#initialize` is called.
@timriley

timriley commented Sep 6, 2026

Copy link
Copy Markdown
Member Author

Thanks for checking this out, folks! As it turns out, I discovered an additional need while continuing the hanami-cli updates: making it it possible for users of Dry CLI to specify their own "auto-assigned" keywords like we have here with stdout:, etc.

To support this, I've introduced an "auto-assigned keywords" feature. See it in the last commit above. The lowdown:

This feature formalises the behaviour we introduced where the stderr:, stdin:, stdout: args were assigned to ivars without subclass #initialize methods needing to worry about receiving those as **args and forwarding them to super.

Now our users can take advantage of this behavior. It’s common for a CLI app to require some “standard” dependencies be provided to every command class through their #initialize methods. These can now be set up like so:

# In a CLI app’s base command class

# `inflector:` is a standard dependency
def self.auto_assign_keywords = super + %i[inflector]

# Supply default values here
private def auto_assign(inflector: Dry::Inflector.new, **kwargs)
  super(**kwargs)
  @inflector = inflector
end

The above will make inflector: work as an argument to .new, and assign its value to an ivar automatically (via #auto_assign) before #initialize is called.


We're using this for the fs: dependency in hanami-cli. See lib/hanami/cli/command.rb in hanami/hanami-cli#447, where we do this:

def self.auto_assign_keywords = super + %i[fs]

# ...

private

def auto_assign(fs: nil, **kwargs)
  super(**kwargs)
  @fs = fs
end

This means we don't have to worry about adding **args and super(**args) to every single command subclass in Hanami CLI.

This is an example of Dry CLI working as "a framework for a framework", allowing CLI apps to implement their own common baseline behaviour without having to foist any boilerplate on their own internal command classes.

@alassek alassek left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have felt the awkwardness of this problem myself, and I think this is a reasonable approach.

Comment thread lib/dry/cli/command.rb
Comment on lines +481 to +490
# A subclass adding its own should add to this list and assign them in {#auto_assign}:
#
# ```
# def self.auto_assign_keywords = super + %i[inflector]
#
# private def auto_assign(inflector: Dry::Inflector.new, **kwargs)
# super(**kwargs)
# @inflector = inflector
# end
# ```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this would be trivial enough to metaprogram auto_assign, is your intention here to make it difficult on purpose to discourage overuse?

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.

5 participants