Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for hydrating
sources
with the Target API (#9306)
This fleshes out how we use `AsyncField`s, which are much more complex than `PrimitiveField`s. ## Result ```python sources = await Get[SourcesResult](SourcesRequest, tgt.get(Sources).request) print(sources.snapshot.files) ``` This also works: ```python if tgt.has_fields([PythonLibrarySources]): sources1 = await Get[Sources](SourcesRequest, tgt.get(PythonLibrarySources).request) sources2 = await Get[Sources](SourcesRequest, tgt.get(Sources).request) assert sources1 == sources2 ``` `PythonSources` and its subclasses will validate that all resulting files end in `*.py` (new behavior). `PythonLibrarySources` and `PythonTestsSources` will use the previous default globs. `PythonBinarySources` will enforce that `sources` is 0 or 1 files (previous behavior). ## Solution ### Ensuring support for subclassed `AsyncField`s With the Target API, we allow new targets to subclass `Field`s for custom behavior. For example, `PythonLibrarySources` might use the default globs of `*.py` whereas `PythonTestSources` might use the default globs of `test_*.py`. To allow these custom subclasses of `Field`s, we added support in #9286 for substituting in the subclass with the original parent class. For example, `my_python_library.get(Sources) == my_python_library.get(PythonSources) == my_python_library.get(PythonLibrarySources)`. This works great with `PrimitiveField` but is tricky to implement with `AsyncField` due to the engine not supporting subclasses. Originally, I tried achieving this extensibility through a union, which would allow the engine to have multiple ways to get a common result type like `SourcesResult`. But, this created a problem that there became multiple paths in the rule graph to compute the same product, e.g. `Sources->SourcesResult`, `PythonSources->SourcesResult`, etc. Instead, each `AsyncField` should define a simple `Request` dataclass that simply wraps the underlying `AsyncField`. This allows us to have only one path from `SourcesRequest -> SourcesResult`, but still give custom behavior in the underlying `SourcesRequest`. Within the hydration rule, the rule will call standardized extension points provided by the underlying field. **This means that the onus is on the `AsyncField` author to expose certain entry points for customizing the field's behavior.** For example, `Sources` defines the entry points of `default_globs` and `validate_snapshot()`. `Dependencies` might provide entry points like `inject_dependencies()` and `validate_dependencies()` (not necessarily, only possibilities). While this approach has lots of boilerplate and less extensibility than `PrimitiveField`s, it solves the graph ambiguity and still allows for subclassing an `AsyncField`. ### Fixing `__eq__` for `Field`s The previous naive implementation resulted in `Field`s only comparing their classvar `alias`, rather than their actual underlying values. This meant that the engine would cache values when it should not have. This tweaks how we use dataclasses to ensure that the engine works correctly with `AsyncField`s.
- Loading branch information