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

fix(sdk/python): Allow for duplicate output values in python programs #11559

Merged
merged 1 commit into from Dec 8, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -0,0 +1,4 @@
changes:
- type: fix
scope: sdk/python
description: Allows for duplicate output values in python
1 change: 1 addition & 0 deletions sdk/python/.gitignore
Expand Up @@ -6,3 +6,4 @@
.venv/
venv/
.coverage
build/
kpitzen marked this conversation as resolved.
Show resolved Hide resolved
67 changes: 40 additions & 27 deletions sdk/python/lib/pulumi/runtime/stack.py
Expand Up @@ -174,54 +174,67 @@ def massage(attr: Any, seen: List[Any]):
if is_primitive(attr):
return attr

if isinstance(attr, Output):
return attr.apply(lambda v: massage(v, seen))

if isawaitable(attr):
return Output.from_input(attr).apply(lambda v: massage(v, seen))

# from this point on, we have complex objects. If we see them again, we don't want to emit them
# again fully or else we'd loop infinitely.
if reference_contains(attr, seen):
# Note: for Resources we hit again, emit their urn so cycles can be easily understood in
kpitzen marked this conversation as resolved.
Show resolved Hide resolved
# the popo objects.
if isinstance(attr, Resource):
return attr.urn

return massage(attr.urn, seen)
# otherwise just emit as nothing to stop the looping.
return None

seen.append(attr)

# first check if the value is an actual dictionary. If so, massage the values of it to deeply
# make sure this is a popo.
if isinstance(attr, dict):
result = {}
# Don't use attr.items() here, as it will error in the case of outputs with an `items` property.
for key in attr:
# ignore private keys
if not key.startswith("_"):
result[key] = massage(attr[key], seen)
try:
seen.append(attr)
return massage_complex(attr, seen)
finally:
popped = seen.pop()
if popped is not attr:
raise Exception("Invariant broken when processing stack outputs")

return result

if isinstance(attr, Output):
return attr.apply(lambda v: massage(v, seen))
def massage_complex(attr: Any, seen: List[Any]) -> Any:
def is_public_key(key: str) -> bool:
return not key.startswith("_")

if isawaitable(attr):
return Output.from_input(attr).apply(lambda v: massage(v, seen))
def serialize_all_keys(include: Callable[[str], bool]):
plain_object: Dict[str, Any] = {}
for key in attr.__dict__.keys():
if include(key):
plain_object[key] = massage(attr.__dict__[key], seen)
return plain_object

if isinstance(attr, Resource):
result = massage(attr.__dict__, seen)
serialized_attr = serialize_all_keys(is_public_key)

# In preview only, we mark the result with "@isPulumiResource" to indicate that it is derived
# from a resource. This allows the engine to perform resource-specific filtering of unknowns
# from output diffs during a preview. This filtering is not necessary during an update because
# all property values are known.
if is_dry_run():
result["@isPulumiResource"] = True
return result
return (
serialized_attr
if not is_dry_run()
else {**serialized_attr, "@isPulumiResource": True}
)

if hasattr(attr, "__dict__"):
# recurse on the dictionary itself. It will be handled above.
return massage(attr.__dict__, seen)
# first check if the value is an actual dictionary. If so, massage the values of it to deeply
# make sure this is a popo.
if isinstance(attr, dict):
# Don't use attr.items() here, as it will error in the case of outputs with an `items` property.
return {
key: massage(attr[key], seen) for key in attr if not key.startswith("_")
}

if hasattr(attr, "__iter__"):
return [massage(item, seen) for item in attr]

# finally, recurse through iterables, converting into a list of massaged values.
return [massage(a, seen) for a in attr]
return serialize_all_keys(is_public_key)


def reference_contains(val1: Any, seen: List[Any]) -> bool:
Expand Down
5 changes: 5 additions & 0 deletions sdk/python/lib/test/langhost/stack_output/__main__.py
Expand Up @@ -18,6 +18,9 @@ def __init__(self):
self.num = 1
self._private = 2


my_test_class_instance = TestClass()

recursive = {"a": 1}
recursive["b"] = 2
recursive["c"] = recursive
Expand All @@ -34,3 +37,5 @@ def __init__(self):
pulumi.export("output", pulumi.Output.from_input(1))
pulumi.export("class", TestClass())
pulumi.export("recursive", recursive)
pulumi.export("duplicate_output_0", my_test_class_instance)
pulumi.export("duplicate_output_1", my_test_class_instance)
Expand Up @@ -39,4 +39,6 @@ def register_resource_outputs(self, _ctx, _dry_run, _urn, ty, _name, _resource,
"output": 1.0,
"class": {"num": 1.0},
"recursive": {"a": 1.0, "b": 2.0},
"duplicate_output_0": {'num': 1.0},
"duplicate_output_1": {'num': 1.0},
}, outputs)