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

Add pants-plugins/stevedore_extensions to add teach pants' dependency inference about our runtime-loaded plugins #5869

Closed
wants to merge 45 commits into from

Conversation

cognifloyd
Copy link
Member

@cognifloyd cognifloyd commented Jan 18, 2023

Background

This is another part of introducing pants, as discussed in various TSC meetings.

Related PRs can be found in:

Overview of this PR

We use openstack/stevedore for several dynamic (discovered at runtime) StackStorm plugins. We use this to load:

  • runners,
  • orquesta functions,
  • sso backends,
  • the metrics driver, and
  • the rbac backend.

Since a stevedore extension does not use the python import system, pants does not know how to infer dependencies on any of this code. We need to extend the dependency inference system to teach it how to handle stevedore extensions. This PR adds a pants plugin that strives to accomplish that by allowing us to:

  • use stevedore_extension targets to inform pants about what code is required for a given extension (where each extension is identified by a stevedore namespace and a plugin name), and
  • use stevedore_namespace fields to record dependencies on extensions in the given namespace.

When a target has the stevedore_namespace field, this plugin will add dependencies on all of the stevedore_extension targets that define plugins in that namespace. For example, that means that pants will include all of our runners whenever code depends on the st2common.runners.runner namespace.

Scope of this PR

This PR only adds the stevedore_extension target and adds the stevedore_namespace field to the python_tests and python_test targets.

After adding the dependency inference (in this PR) there is one more thing required for python_test targets to successfully get access, via stevedore, to the relevant python code. We need to generate an entry_points.txt file. We will include that in the next PR, which you can preview at: pants-plugins-stevedore_extensions...pants-plugins-stevedore_extensions-entry_points_txt

Another follow-up PR will also add the stevedore_namespaces field to python_distribution targets (a wheel is a distribution). If you want a preview of that PR, you can look at: pants-plugins-stevedore_extensions-entry_points_txt...pants-plugins-stevedore_extensions-setup_py

Note, also, that I have not enabled the plugin yet:

st2/pants.toml

Line 30 in 7148058

#"stevedore_extensions",

That keeps this PR focused on the plugin itself including adding tests for it. A third follow-up PR will add the stevedore_extension target and stevedore_namespaces field metadata to our BUILD files. If you want to explore some of that metadata, you can look at: pants-plugins-stevedore_extensions-setup_py...pants-stevedore_extensions

stevedore_extension target

In the plugin, the stevedore_extension target is defined here:

class StevedoreExtension(Target):
alias = "stevedore_extension"
core_fields = (
*COMMON_TARGET_FIELDS,
StevedoreNamespaceField,
StevedoreEntryPointsField,
PythonResolveField,
)
help = "Entry points used to generate setuptools metadata for stevedore."

There are several fields on this target. In this example BUILD file, we see three fields: name, namespace, and entry_points. (Yes, there are other fields available on the target. But, those are only needed for other pants features; we won't use them in our BUILD files.)

stevedore_extension(
name="runner",
namespace="st2common.runners.runner",
entry_points={
"noop": "noop_runner.noop_runner",
},
)

Look in target_types.py to see the definition of the namespace (StevedoreNamespaceField) and entry_points (StevedoreEntryPointsField) fields. In particular, please review the help text for each of these fields.

stevedore_namespaces field (on python_tests/python_test targets)

In the plugin, the stevedore_namespaces field is defined here:

class StevedoreNamespacesField(StringSequenceField):
alias = "stevedore_namespaces"
help = softwrap(
"""
List the stevedore namespaces required by this target.
All stevedore_extension targets with these namespaces will be added as
dependencies so that they are available on PYTHONPATH during tests.
The stevedore namespace format (my.stevedore.extension) is similar
to a python namespace.
"""
)

When we identify tests that need particular stevedore extensions, we list the required namespaces in BUILD files like this:

python_tests(
name="tests",
dependencies=[
# several files import tests.unit.base which is ambiguous. Tell pants which one to use.
"st2common/tests/unit/base.py",
],
stevedore_namespaces=["st2common.runners.runner"],
)

In this example, the unit tests get a dependency on all of the runners.

Dependency inference rules

To make dependency inference add these dependencies, the pants plugin adds several rules.

Dependencies on stevedore_extension targets

Starting with the inferred dependencies, and going backwards in the rule graph, here are the rules to infer dependencies on stevedore_extension targets.

@rule(
desc="Infer stevedore_extension target dependencies for python_tests based on namespace list.",
level=LogLevel.DEBUG,
)
async def infer_stevedore_namespace_dependencies(
request: InferStevedoreNamespaceDependencies,
stevedore_extensions: StevedoreExtensions,
) -> InferredDependencies:

This rule needs StevedoreExtensions which comes from this rule:

@rule(
desc="Creating map of stevedore_extension namespaces to StevedoreExtension targets",
level=LogLevel.DEBUG,
)
async def map_stevedore_extensions(
stevedore_extensions: AllStevedoreExtensionTargets,
) -> StevedoreExtensions:

Which needs AllStevedoreExtensions from this rule (which gets AllTargets from pants provided rules):

@rule(desc="Find all StevedoreExtension targets in project", level=LogLevel.DEBUG)
def find_all_stevedore_extension_targets(
targets: AllTargets,
) -> AllStevedoreExtensionTargets:

stevedore_extension targets depend on python code

The stevedore_extension targets would not be very helpful if they didn't pull in dependencies on relevant python code. So, we parse the entry_points field and infer dependencies on all of the python code that provides those entry points. Starting from

@rule(
desc="Inferring dependency from the stevedore_extension `entry_points` field",
level=LogLevel.DEBUG,
)
async def infer_stevedore_entry_points_dependencies(
request: InferStevedoreExtensionDependencies,
python_setup: PythonSetup,
) -> InferredDependencies:

In that rule, it requests ResolvedStevedoreEntryPoints from the pants engine, which pulls in this rule:

@rule(
desc="Determining the entry points for a `stevedore_extension` target",
level=LogLevel.DEBUG,
)
async def resolve_stevedore_entry_points(
request: ResolveStevedoreEntryPointsRequest,
) -> ResolvedStevedoreEntryPoints:

Thus we have an inferred set of dependencies from python_test through stevedore_extension to python_source.

Developing pants plugins

Pants has extensive documentation on the plugin API, including how to create targets, how to write rules, and there's even a mini tutorial about how to add a formatter (adding formatters is a very common thing for plugins to do).

@cognifloyd cognifloyd added this to the pants milestone Jan 18, 2023
@cognifloyd cognifloyd self-assigned this Jan 18, 2023
@pull-request-size pull-request-size bot added the size/XL PR that changes 500-999 lines. Consider splitting work into several ones that easier to review. label Jan 18, 2023
@cognifloyd cognifloyd changed the title Pants plugins stevedore extensions Add pants-plugins/stevedore_extensions to add teach pants' dependency inference about our runtime-loaded plugins Jan 18, 2023
@pull-request-size pull-request-size bot added size/XXL PR that changes 1000+ lines. You should absolutely split your PR into several. and removed size/XL PR that changes 500-999 lines. Consider splitting work into several ones that easier to review. labels Jan 19, 2023
@cognifloyd cognifloyd force-pushed the pants-plugins-stevedore_extensions branch from 138135c to f06f8c1 Compare January 19, 2023 03:15
@pull-request-size pull-request-size bot added size/XL PR that changes 500-999 lines. Consider splitting work into several ones that easier to review. and removed size/XXL PR that changes 1000+ lines. You should absolutely split your PR into several. labels Jan 19, 2023
@cognifloyd cognifloyd requested review from arm4b, rush-skills, amanda11, nzlosh, winem and a team January 19, 2023 04:14
@cognifloyd cognifloyd marked this pull request as ready for review January 19, 2023 04:14
Copy link
Contributor

@amanda11 amanda11 left a comment

Choose a reason for hiding this comment

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

A few queries.

async def resolve_stevedore_entry_points(
request: ResolveStevedoreEntryPointsRequest,
) -> ResolvedStevedoreEntryPoints:
# based on: pants.backend.python.target_types_rules.resolve_pex_entry_point
Copy link
Member Author

Choose a reason for hiding this comment

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

@cognifloyd cognifloyd requested review from amanda11 and a team January 21, 2023 05:02
@cognifloyd cognifloyd force-pushed the pants-plugins-stevedore_extensions branch from b4515dd to 0eb1354 Compare January 23, 2023 15:58
@cognifloyd
Copy link
Member Author

Rebased on master

Copy link
Contributor

@amanda11 amanda11 left a comment

Choose a reason for hiding this comment

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

Happy with the changes, just the change on the comment as per suggestion to add.

@cognifloyd cognifloyd requested a review from a team January 23, 2023 18:42
@cognifloyd cognifloyd enabled auto-merge January 23, 2023 20:34
@cognifloyd cognifloyd force-pushed the pants-plugins-stevedore_extensions branch from 3731eec to 45cb023 Compare January 27, 2023 21:37
@cognifloyd
Copy link
Member Author

rebased

cognifloyd added a commit to cognifloyd/pants that referenced this pull request Jan 31, 2023
This backend provides dependency inference for apps that use
openstack/stevedore to load plugins at runtime.

This was originally developed for the StackStorm project:
StackStorm/st2#5869
@cognifloyd cognifloyd disabled auto-merge February 1, 2023 04:51
@cognifloyd cognifloyd marked this pull request as draft February 1, 2023 04:51
@cognifloyd
Copy link
Member Author

I submitted pantsbuild/pants#18132 to try and get this plugin into upstream pants instead of in our repo. In doing that I realized I could vastly simplify it, so I did a major refactor. Even if it's not accepted upstream, the new version of the plugin will be easier to review with fewer and smaller PRs to get it all in.

I'm marking this as draft until I see where the upstreaming effort lands.

stuhood pushed a commit to pantsbuild/pants that referenced this pull request Feb 3, 2023
This adds `pants.backend.python.framework.stevedore`.

This was originally developed for the StackStorm project in StackStorm/st2#5869 but it seems general enough to include in pants itself. Other people on [slack](https://pantsbuild.slack.com/archives/C01CQHVDMMW/p1674865736900199) agree, so here's the PR.

## What is openstack/stevedore?

Python projects can use [openstack/stevedore](https://github.com/openstack/stevedore) for dynamic (discovered at runtime) extensions/plugins. I find this [page in the stevedore docs](https://docs.openstack.org/stevedore/latest/user/tutorial/naming.html) to be most helpful in understanding what stevedore does. Here are some of the key points:

> Stevedore uses setuptools entry points to define and load plugins. An entry point is standard way to refer to a named object defined inside a Python module or package. The name can be a reference to any class, function, or instance, as long as it is created when the containing module is imported (i.e., it needs to be a module-level global).
>
> Entry points are registered using a _name_ in a _namespace_.
>
> Entry point names are usually considered user-visible. ... Because they are public, names are typically as short as possible while remaining descriptive. ...
>
> Namespaces, on the other hand, are an implementation detail, and while they are known to developers they are not usually exposed to users. The namespace naming syntax looks a lot like Python’s package syntax (*a.b.c*) but _namespaces do not correspond to Python packages._ ...
>
> Each namespace is owned by the code that consumes the plugins and is used to search for entry points. ...

## About the stevedore pants plugin

The primary focus of this plugin is to facilitate testing projects that use `openstack/stevedore` by adding the extensions/plugins to the pytest sandbox and ensuring they are "discoverable" by stevedore.

### How does this plugin facilitate testing code that uses stevedore?

Since a stevedore extension does not use the python import system, pants does not know how to infer dependencies on any of this code. We need to extend the dependency inference system to teach it how to handle stevedore extensions. This PR adds a pants plugin that strives to accomplish that by allowing us to:

- use `python_distribution(entry_points={...}, ...)` to define the stevedore namespaces and plugins,
    - differentiate namespaces in the `entry_points` field with a special `stevedore_namespace` object. 
- use `stevedore_namespaces` fields to record dependencies on extensions in the given namespaces.

So far, I've only added the `stevedore_namespaces` field to the `python_test` and `python_tests` targets as testing has been my primary focus. We could add it to other targets later if anyone finds that helpful.
When a target has the `stevedore_namespaces` field, this plugin will:

1. look up all of the `python_distribution` targets with an `entry_points` field that have `stevedore_namespace` tagged keys.
    - [`@rule python_target_dependencies.find_all_python_distributions_with_any_stevedore_entry_points`](https://github.com/pantsbuild/pants/pull/18132/files#diff-f50253674b1c85da837a8a04c18c23cad6dec5220b6b8a1ac62838e40346e01dR56)
    - [`@rule python_target_dependencies.map_stevedore_extensions`](https://github.com/pantsbuild/pants/pull/18132/files#diff-f50253674b1c85da837a8a04c18c23cad6dec5220b6b8a1ac62838e40346e01dR85)
    - [`@rule python_target_dependencies.find_python_distributions_with_entry_points_in_stevedore_namespaces`](https://github.com/pantsbuild/pants/pull/18132/files#diff-f50253674b1c85da837a8a04c18c23cad6dec5220b6b8a1ac62838e40346e01dR103)
2. add/infer dependencies from the test targets to all of the python code that provides the entry points in the required namespaces; in other words, we infer dependencies on a subset of the `python_distribution` target, not on the `python_distribution` target itself.
    - [`@rule python_target_dependencies.infer_stevedore_namespace_dependencies`](https://github.com/pantsbuild/pants/pull/18132/files#diff-f50253674b1c85da837a8a04c18c23cad6dec5220b6b8a1ac62838e40346e01dR142)
3. generate a `{module_path}.egg-info/entry_points.txt` file in the pytest sandbox for each relevant `python_distribution` target (the `entry_points.txt` file will only contain entry_points for the required namespaces).
    - [`@rule rules.generate_entry_points_txt_from_stevedore_extension`](https://github.com/pantsbuild/pants/pull/18132/files#diff-1d487c2b704cac1a36e9398cc29dd75e5601535e53224f67923f641c8c320a4eR42)

For example, if an project used the namespace `st2common.runners.runner`, then we could set `stevedore_namespaces=["st2common.runners.runner"]` on a `python_tests()` target. Then pants will include all of he python code that provides the named entry points in those namespaces. And then the generated `entry_points.txt` files make that python code appear to be "installed" (not just added to PYTHONPATH), thus allowing stevedore to discover the entry points and load the plugins during the tests.
@cognifloyd
Copy link
Member Author

Woohoo! This was merged upstream: pantsbuild/pants#18132

I will revisit this once there is a pants release we can use (prob dev or rc) that includes this plugin. Then we'll be able to add all the stevedore entry points metadata without adding the plugin. In the meantime, I plan to add the python_distribution metadata for building wheels.

@cognifloyd cognifloyd closed this Feb 3, 2023
@cognifloyd cognifloyd deleted the pants-plugins-stevedore_extensions branch February 12, 2024 16:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement infrastructure: ci/cd pantsbuild size/XL PR that changes 500-999 lines. Consider splitting work into several ones that easier to review. tests
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants