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
Provide interface and individual namespaces for brew CLI commands #16815
Conversation
@dduugg I like the approach, nice work! |
I'm not sure this is a great approach. It seems like we have a base class with a callback to register all classes that inherit from it. Then, we use that registry to find and call the command class that matches the command the user entered. All of that makes sense. The problem is that we currently don't load most of the commands. In fact, we only load one command per run 99% of the time. That allows us to lazy load a bunch of files based on the command that is getting run. If we have to load all commands into memory before running one of them, we'll be including most of the codebase by default which will likely negatively affect start up time. I think we should preserve the current lazy loading behavior if possible. |
There are no changes to the loading behavior, which is still a path search based on the command name. The registry is a convenience for mapping the command name to the class name, once the file has been loaded. I’m open to the critique that it’s over-designed for this use case. (I can take a look at returning the command class more directly as a result of the path resolution search.) |
Ah, okay. My bad. I completely misinterpreted what was being proposed here. I'm fine with this approach. It is unfortunate that we'll be up to three different command types but I guess that's the price we pay for backwards compatibility. |
@apainintheneck check out the latest commit. I've done away with the registry in favor of a simple search of |
This O(n) where n is most likely just 1, right? Or does n refer to all objects. I remember the Rails #descendants method being pretty inefficient because it had to walk the entire I prefer this approach since we don't plan on referring to the registry more than once so we might as well just not build it in the first place. I'm fine with whichever you prefer though. |
Sorry for the lack of specificity, I was referring to the |
I believe initial feedback has been addressed, opening this for general review. (More than happy to port over more commands to demonstrate viability.) |
IMO this is probably enough unless there is a specific command that you think will be especially tricky to port over to the new interface. I think that shouldn't be the case though, right? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me except for one missing piece with it_behaves_like "parseable arguments"
.
|
||
RSpec.describe "brew list" do | ||
RSpec.describe Homebrew::Cmd::List do |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Note that once we've ported all the commands, we can/should enable RSpec/DescribeClass
(the commands are all currently in the Homebrew
namespace, so the current approach makes sense).
|
||
def self.filtered_list(args:) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just realized that we no longer need to thread args
through other methods (it's now an ivar, since each command is instance), so I've simplified some method signatures accordingly.
Library/Homebrew/cmd/list.rb
Outdated
ls_args = [] | ||
ls_args << "-1" if args[:"1?"] | ||
ls_args << "-l" if args[:l?] | ||
ls_args << "-r" if args[:r?] | ||
ls_args << "-t" if args[:t?] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feels weaker from a typechecking point of view and feels weird seeing this but still seeing args.formula?
and args.verbose?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd overlooked this change. Is this needed to make Sorbet happy? If not, the old syntax is preferable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct. Because CLI::Args
subclasses OpenStruct
, many of the commands are just hash accessors, and not supported by Sorbet. Thus args
, as the result of Parser#parse
, has been treated as untyped:
# @return [Args] The actual return type is `Args`, but since `Args` uses `method_missing` to handle options, the
# `sig` annotates this as returning `T.untyped` to avoid spurious type errors.
sig { params(argv: T::Array[String], ignore_invalid_options: T::Boolean).returns(T.untyped) }
def parse(argv = ARGV.freeze, ignore_invalid_options: false)
As part of this new implementation, args is correctly typed, but may require changes:
# @note because `Args` makes use `OpenStruct`, subclasses may need to use a tapioca compiler,
# hash accessors, args.rbi, or other means to make this work with legacy commands:
sig { returns(CLI::Args) }
attr_reader :args
(Reviewers may have noticed that AbstractCommand
has the rare # typed: strong
, which is often overkill, but helpful for ancestors or other extensible tooling.)
Some args
commands still work, for various reasons. As noted above, args.rbi
is one option, but i don't especially love it (and I don't really have the bandwidth to write type signatures as part of the first pass of converting commands). It makes the methods available to all commands, rather than for just the command(s) that define the flag. I'm actively thinking about how to namespace the args as well, as a sort of v2 of what I'm introducing here, but I don't have anything concrete to propose at this time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem here is that while args.pinned?
and args[:pinned?]
are equivalent the new approach doesn't handle typos well. args.pinnedd?
raises an error Error: CLI arg for
pinnedd? is not declared for this command
while args[:pinnedd?]
doesn't fail and just returns nil.
Could we just keep cli args untyped and avoid this change? What additional type safety do we get by typing this attribute?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, this is just another example of why a refactor of CLI::Args
might be in the cards at some point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds like we've moving from something that checked for errors at runtime so something that never checks for errors? I'm not entirely sure what the marginal type improvement here is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds like we've moving from something that checked for errors at runtime so something that never checks for errors?
Could you elaborate, possibly with examples? I'm not sure that I understand this claim.
I'm not entirely sure what the marginal type improvement here is.
args
was previously untyped by default, but now is typed. This is an issue accessing when OpenStruct hash values via methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
diff --git a/Library/Homebrew/cmd/list.rb b/Library/Homebrew/cmd/list.rb
index 238f63f53a..d3f7953824 100644
--- a/Library/Homebrew/cmd/list.rb
+++ b/Library/Homebrew/cmd/list.rb
@@ -96,7 +96,7 @@ module Homebrew
ls_args = []
ls_args << "-1" if args[:"1?"]
- ls_args << "-l" if args[:l?]
+ ls_args << "-l" if args.somethinginvalid?
ls_args << "-r" if args[:r?]
ls_args << "-t" if args[:t?]
results in:
$ brew list
Error: CLI arg for `somethinginvalid?` is not declared for this command
However this:
diff --git a/Library/Homebrew/cmd/list.rb b/Library/Homebrew/cmd/list.rb
index 238f63f53a..f1fee19c14 100644
--- a/Library/Homebrew/cmd/list.rb
+++ b/Library/Homebrew/cmd/list.rb
@@ -96,7 +96,7 @@ module Homebrew
ls_args = []
ls_args << "-1" if args[:"1?"]
- ls_args << "-l" if args[:l?]
+ ls_args << "-l" if args[:somethinginvalid?]
ls_args << "-r" if args[:r?]
ls_args << "-t" if args[:t?]
will result in no error. So we're losing some type checking here (albeit not via Sorbet and more from our special OpenStruct subclass).
To be clear: I do agree with the idea that we should do better namespacing so that we can extend this to work via Sorbet too if possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perfect answer, thank you for the clear example. I believe the latest commit copies the behavior of method access to hash access, PTAL.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that's a fair compromise for now. It'd be nice to make the syntax more consistent at some point but that will likely require a more involved refactor.
I've got a follow-up PR ready with ten commands ported and the only issue has been adding support for CamelCase to kebab-case mapping for multi-word commands. |
ab77af1
to
d3c5375
Compare
d1a17d3
to
7c7444c
Compare
97ecb6b
to
e237237
Compare
Opening this up for re-review, after merging #16880 I've reverted the changes that used hash accessors for accessing args, because a) that style was considered less ergonomic and b) it has a different behavior than method access due to overrides in I've also reverted most of the changes to |
1f1c245
to
4b358fc
Compare
|
||
class Homebrew::CLI::Args | ||
sig { returns(T::Boolean) } | ||
def stackprof?; end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cheating a bit here – command arg methods will be isolated to individual files per command, though (for now) arg methods all live in the same namespace (Homebrew::CLI::Args
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@dduugg Potentially outrageous suggestion that can wait to another PR (including discussion can wait) to not block this one:
What if all the per-command arguments were subclasses of Homebrew::CLI::Args
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's the eventual idea, hence the "for now" above. 😉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! Happy to have you self-merge when you're happy/ready @dduugg!
brew style
with your changes locally?brew typecheck
with your changes locally?brew tests
with your changes locally?This is a strawman implementation of #16814. It adds an
AbstractCommand
, which requires implementingraw_args
(usually implemented as {$name}_args,unparsed_args
could be a better name) andrun
(implemented as${name}
). There's also aCommandRegistry
to allow lookup by command name (used in brew.rb).Two commands are ported as examples (one in
cmd
, one indev-cmd
). They are currently namespaced according to their current paths. (Best viewed with whitespace diffing disabled, due to indentation changes.)(Tests forthcoming, pending consensus on approach/implementation.)