-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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
[Low-Code CDK] Handle forward references in manifest #20893
Conversation
080e60f
to
e849cef
Compare
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'm liking what you have so far and I think the code to parse the layers is more intuitive to understand so nice work! I had a few follow ups related to testing and how we invoke the factory a bit. Just some discussion points to flesh out rather than strict changes.
Also it might be worth it to run the validate Gradle command that Maxime wrote to verify. We should expect them to all pass (although one heads up about source-monday which has been acting a bit weird this week).
except (KeyError, IndexError): | ||
raise UndefinedReferenceException(path, reference) | ||
|
||
def _read_reference_value(self, ref: str, manifest_node: Mapping[str, Any]) -> Any: |
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.
since we don't reference self here, we can make this a static method
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.
👍 done.
@@ -46,7 +46,7 @@ | |||
|
|||
factory = DeclarativeComponentFactory() | |||
|
|||
resolver = ManifestReferenceResolver() | |||
resolver = ManifestReferenceResolver |
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 feel like its a little more confusing to define the class here which sort of turns resolver
into a sort of alias of ManifestReferenceResolver. For simplicity, since we're going to be instantiating a new ManifestReferenceResolver
per test, we can probably just get rid of this and in each test just do:
config = ManifestReferenceResolver(YamlDeclarativeSource._parse(content)).preprocess_manifest()
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.
👍 makes sense to me, updated.
@@ -65,7 +65,7 @@ def test_factory(): | |||
request_body_json: | |||
body_offset: "{{ next_page_token['offset'] }}" | |||
""" | |||
config = resolver.preprocess_manifest(YamlDeclarativeSource._parse(content), {}, "") | |||
config = resolver(YamlDeclarativeSource._parse(content)).preprocess_manifest() |
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 don't think any of the existing tests have an inline schema which require a forward reference. Can you add a test to verify that your changes work for the main use case we were implementing on this for? We would probably just adapt an existing test and add the inlineschema component that then references a schema object at the bottom of the text blob
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.
Good call! Added.
if not isinstance(evaluated_ref, dict): | ||
return evaluated_ref | ||
else: | ||
return evaluated_ref | evaluated_dict |
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.
If I'm understanding this right, we get the references values, and then update with the values defined on this component. So the values defined on this component take precedence. If so, can we add a small comment saying that values defined on this node take precedence over the referenced values.
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.
Yep that's right. Added a comment.
@@ -96,91 +96,102 @@ class ManifestReferenceResolver: | |||
|
|||
ref_tag = "$ref" | |||
|
|||
def preprocess_manifest(self, manifest: Mapping[str, Any], evaluated_mapping: Mapping[str, Any], path: Union[str, Tuple[str]]): | |||
def __init__(self, manifest: Mapping[str, Any]): |
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 can see why we're going with the approach to have the set and the manifest defined on the factory itself, but it steel feels a little weird for them to be inserted into the factory during instantiation. When I think of a factory I see it as generic and the preprocess method would take input and emit output.
Granted we need to reference the manifest as we traverse the manifest so its probably easier to define it on the factory class itself.
The other thing that feels weird is that when the set()
is defined at the start, we're very reliant on correctly popping off visited elements after traversal and that at the end of preprocess the visited set is cleared.
As part of the preprocess_manifest()
either when starting or finishing a run, we could clear the set so we know on each attempt we start clean. And the same could be said for assigning self.manifest =
in that method (Although we would probably still instantiate empty values during init?). I'm not married to this approach either, but I think its worth a discussion because it stuck out to me that we're coupling the manifest to the factory pretty tightly in the refactor.
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.
So if you grep for ManifestReferenceResolver
we actually only instantiate it and do the preprocessing once - when ManifestDeclarativeSource
is initialized, and before the component factory is invoked at all. I'm wondering if that changes the discomfort with initializing it with the manifest + visited
set. From there on out, the fully resolved config is accessed via ManifestDeclarativeSource
and no additional calls are made to preprocess_manifest
. That said if it feels more natural we can certainly consider clearing out the visited
set after processing, and go back to the original design where we just passed the manifest into preprocess_manifest
.
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.
Right so it's not a hard blocker by any means since like you said it's only used once. It's just a little bit of an awkward design. I don't think this part of flow would change, but one of the risks would be if we ever moved to using the factory or resolver multiple times, if we don't pop every element off, we might end up with some unexpected results accidentally using a set w/ stale elements.
How much lift is it to pass the manifest to the resolver like we used to be doing? The part that I noticed in your new implementation was that we use the self.manifest
field in _lookup_reference_value()
. If the lift isn't too big to continue the pass in the manifest and start w/ a clean execution of the function every time then I think that feels more intuitive. That being said, if trying to accommodate this makes the function too confusing, I think it is fine to leave as you already had it.
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.
It's no trouble at all - the code was in that state in one iteration, but I'd modified it thinking that it would be a little cleaner this way since I was having to pass manifest
around as an argument in so many places. But to protect future-us from issues if we use the resolver multiple times I'm happy to change it back!
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.
Pushed this change.
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.
Thanks for the review @brianjlai! I've addressed your comments and added some info around the instantiation of ManifestReferenceResolver
.
@@ -46,7 +46,7 @@ | |||
|
|||
factory = DeclarativeComponentFactory() | |||
|
|||
resolver = ManifestReferenceResolver() | |||
resolver = ManifestReferenceResolver |
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.
👍 makes sense to me, updated.
@@ -65,7 +65,7 @@ def test_factory(): | |||
request_body_json: | |||
body_offset: "{{ next_page_token['offset'] }}" | |||
""" | |||
config = resolver.preprocess_manifest(YamlDeclarativeSource._parse(content), {}, "") | |||
config = resolver(YamlDeclarativeSource._parse(content)).preprocess_manifest() |
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.
Good call! Added.
except (KeyError, IndexError): | ||
raise UndefinedReferenceException(path, reference) | ||
|
||
def _read_reference_value(self, ref: str, manifest_node: Mapping[str, Any]) -> Any: |
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.
👍 done.
if not isinstance(evaluated_ref, dict): | ||
return evaluated_ref | ||
else: | ||
return evaluated_ref | evaluated_dict |
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.
Yep that's right. Added a comment.
@@ -96,91 +96,102 @@ class ManifestReferenceResolver: | |||
|
|||
ref_tag = "$ref" | |||
|
|||
def preprocess_manifest(self, manifest: Mapping[str, Any], evaluated_mapping: Mapping[str, Any], path: Union[str, Tuple[str]]): | |||
def __init__(self, manifest: Mapping[str, Any]): |
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.
So if you grep for ManifestReferenceResolver
we actually only instantiate it and do the preprocessing once - when ManifestDeclarativeSource
is initialized, and before the component factory is invoked at all. I'm wondering if that changes the discomfort with initializing it with the manifest + visited
set. From there on out, the fully resolved config is accessed via ManifestDeclarativeSource
and no additional calls are made to preprocess_manifest
. That said if it feels more natural we can certainly consider clearing out the visited
set after processing, and go back to the original design where we just passed the manifest into preprocess_manifest
.
@brianjlai forgot to ask - what is the gradle command you're suggesting? |
The command is here: It basically just attempts to run the |
8460813
to
7853b9f
Compare
Thanks @brianjlai, the |
…ifestReferenceResolver`
9ddbb7d
to
a624c50
Compare
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.
Glad to see it running through the validation script. Just a couple of small suggestions and nits. Nice work!
@@ -96,91 +96,102 @@ class ManifestReferenceResolver: | |||
|
|||
ref_tag = "$ref" | |||
|
|||
def preprocess_manifest(self, manifest: Mapping[str, Any], evaluated_mapping: Mapping[str, Any], path: Union[str, Tuple[str]]): | |||
|
|||
def preprocess_manifest(self, manifest): |
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.
nit: can you add type hints and outputs to the method signature
elif isinstance(node, list): | ||
return [self._evaluate_node(v, manifest) for v in node] | ||
elif isinstance(node, str) and node.startswith("*ref("): | ||
if visited is None: |
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 its a bit more clear to instantiate the empty set and pass it as a parameter which when we first invoke the first _evaluate_node()
rather than rely on this conditional block to set it up. Also given how its used, in the flow it feels more like a required parameter instead of defaulting to None
:
return self._evaluate_node(manifest, manifest, set())
What do you think?
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.
@brianjlai sorry I overlooked these before merging! Good suggestions, I'll make these changes in a separate PR.
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.
(#21268)
[Low-Code CDK] Handle forward references in manifest
What
Allow users to organize yaml manifests with more flexibility, by handling forward references.
Closes #20503
How
Updates the
ManifestReferenceResolver
class to look up references by reading the value(s) at the referenced path in the manifest (rather than looking for values in previously evaluated keys).Recommended reading order
manifest_reference_resolver.py
manifest_declarative_source.py
custom_exceptions.py