Skip to content
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

Eliminate most rule registration boilerplate. #10477

Merged
merged 5 commits into from Jul 28, 2020

Conversation

jsirois
Copy link
Member

@jsirois jsirois commented Jul 27, 2020

Introduce a register_rules function that can find and register all
@rules and the SubsystemRules they depend on. This can be used to
both reduce boilerplate and also make rule development less surprising.
Using register_rules it now becomes rarer to edit rules and encounter
errors due to forgetting to register rules for the various introduced
subsystems and @rule functions.

Also streamline the Rule interface and the def rules() semantics;
now all rules are Rules save for UnionRule which can be dealt away
with later to leave users only exposed to registering Rules and allow
def rules() -> Iterable[Rule]:.

[ci skip-rust-tests]

@jsirois
Copy link
Member Author

jsirois commented Jul 27, 2020

1st commit is the technology, 2nd commit uses it.

@jsirois
Copy link
Member Author

jsirois commented Jul 28, 2020

OK - Rebased and de-conflicted. PTAL.

Copy link
Contributor

@Eric-Arellano Eric-Arellano left a comment

Choose a reason for hiding this comment

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

Wonderful! Thanks, John!

src/python/pants/engine/internals/graph.py Outdated Show resolved Hide resolved
Copy link
Sponsor Member

@stuhood stuhood left a comment

Choose a reason for hiding this comment

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

This seems to strike a good balance of adding magic to remove boilerplate. I think it would be good to trend toward explicit calls to the function with module arguments though.

Thanks!

@@ -79,4 +79,4 @@ async def create_awslambda(


def rules():
return [create_awslambda]
return register_rules()
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

Thoughts on inverting this, and calling it from outside the module? Ie, having an importer call with the module name:

register_rules(pants.backend.awslambda.common.awslambda_common_rules)

Would have two advantages I think:

  1. no uncertainty about what happens when called with no args (although that's not too magical)
  2. no need for a convention inside of each module to export rules: things are mostly consistent, but avoiding the need for the convention entirely would be great.

It looks like to make it work though, UnionRule could maybe move to being a class decorator... ie:

@union
class MyUnion: pass

@union_member(MyUnion)
class MyUnionMember: pass

...so maybe better as a followup step.

Copy link
Member Author

Choose a reason for hiding this comment

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

Thoughts on inverting this, and calling it from outside the module?

The API supports it and that was my intention, but its not easy right now so took this smaller step.
Since we hack RootRules in to support tests in our production rules() lists (not cool), using this function from outside the module does not catch those and foces you to register those.

It looks like to make it work though, UnionRule could maybe move to being a class decorator... ie:

I have an old branch that instead just forces union members to be subclasses of the @union decorated (potentially just marker) type. That will kill all member boilerplate - you just subclass which should feel like it makes sense to rule authors opting into the test goal say from a pure Python perspective, Pants engine aside.

Copy link
Contributor

Choose a reason for hiding this comment

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

I like the idea of @union_member, although I don't think that would work with plugin fields: #10469

I also don't think that we want plugin fields to have to subclass the target they're being used for. A key part of the Target API is that a single field definition may be used across multiple targets. Also, fields already subclass a field template. So, we would end up with:

class Example(IntField, PythonLibrary.PluginField, PythonTests.PluginField):
   alias = "example"

Copy link
Member Author

Choose a reason for hiding this comment

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

PluginField should not be using Union IMO. If we instead Target.register_plugin_field(Field) -> Rule then Rule can be whatever type we want and e don't need to bastardize UnionRule. Hopefully everyone realizes that by bastardizing Unions in this way, if you look at the action graph you get spammed with a bunch of fake entries for non-rule edges.

Copy link
Member Author

Choose a reason for hiding this comment

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

Union has a very specific purpose: allow a Get to retrieve a given product type from an unknown provider - ie a rule set forthcoming that can provide that product type given an unknown input type at the time of writing the top-level goal. Thats pretty darn specific. Not everything is a union, just that.

implicitly form a loosely coupled RuleGraph: this facility exists only to assist with
boilerplate removal.
"""
def register_rules(*namespaces: Union[ModuleType, Mapping[str, Any]]) -> Iterable[Rule]:
Copy link
Sponsor Member

Choose a reason for hiding this comment

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

It's unlikely to be the slowest part of startup, but none of our calls to def rules are memoized. Perhaps the inner portion of this method (post module identification) could be.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah. The fact that we re-export rules at all / need to seems problematic. I have not thought through why we need to do that which is what leads to the need for this optimization now IIUC. It would be great to kill that need instead / in addition.

Copy link
Sponsor Member

Choose a reason for hiding this comment

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

Mm. There was some discussion of this the other day: https://pantsbuild.slack.com/archives/C0D7TNJHL/p1595565944292800 ... but yea, unclear what to do, as it does seem to make things easier in some cases.

@stuhood
Copy link
Sponsor Member

stuhood commented Jul 28, 2020

One other bit: this is more like collect_rules then register_rules, as it doesn't have side effects. "collect", "gather", "select", etc...?

@Eric-Arellano
Copy link
Contributor

this is more like collect_rules then register_rules

+1

@jsirois
Copy link
Member Author

jsirois commented Jul 28, 2020

Agreed - went with collect_rules.

Introduce a register_rules function that can find and register all
@rules and the SubsystemRules they depend on. This can be used to
both reduce boilerplate and also make rule development less surprising.
Using register_rules it now becomes rarer to edit rules and encounter
errors due to forgetting to register rules for the various introduced
subsystems and @rule functions.

Also streamline the Rule interface and the `def rules()` semantics;
now all rules are Rules save for UnionRule which can be dealt away
with later to leave users only exposed to registering Rules.

[ci skip-rust-tests]
# Rust tests will be skipped. Delete if not intended.
[ci skip-rust-tests]
# Rust tests will be skipped. Delete if not intended.
[ci skip-rust-tests]
# Rust tests and lints will be skipped. Delete if not intended.
[ci skip-rust]

# Building wheels and fs_util will be skipped. Delete if not intended.
[ci skip-build-wheels]
@coveralls
Copy link

Coverage Status

Coverage remained the same at 0.0% when pulling bd20fcb on jsirois:rule/register_ergonomics into 2773086 on pantsbuild:master.

@jsirois jsirois merged commit 8c2bb30 into pantsbuild:master Jul 28, 2020
@jsirois jsirois deleted the rule/register_ergonomics branch July 28, 2020 03:40
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.

None yet

4 participants