Support arbitrary stack export values in python. (#3015)

This commit is contained in:
CyrusNajmabadi 2019-08-01 20:00:07 -07:00 committed by GitHub
parent aac25eabc4
commit d1376db975
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 132 additions and 13 deletions

View file

@ -9,6 +9,10 @@ CHANGELOG
- Add `requireOutput` to `StackReference` [#3007](https://github.com/pulumi/pulumi/pull/3007)
- Arbitrary values can now be exported from a Python app. This includes dictionaries, lists, class
instances, and the like. Values are treated as "plain old python data" and generally kept as
simple values (like strings, numbers, etc.) or the simple collections supported by the Pulumi data model (specifically, dictionaries and lists).
### Compatibility
- Deprecated functions in `@pulumi/pulumi` will now issue warnings if you call them. Please migrate

View file

@ -152,6 +152,7 @@ disable=print-statement,
too-many-arguments,
too-many-branches,
too-many-locals,
too-many-return-statements,
missing-docstring,
fixme,
broad-except,

View file

@ -16,19 +16,21 @@
Support for automatic stack components.
"""
import asyncio
from typing import Callable, Any, Dict
import collections
from typing import Callable, Any, Dict, List
from ..resource import ComponentResource
from ..resource import ComponentResource, Resource
from .settings import get_project, get_stack, get_root_resource, set_root_resource
from .rpc_manager import RPC_MANAGER
from .. import log
from ..output import Output
async def run_in_stack(func: Callable):
"""
Run the given function inside of a new stack resource. This ensures that any stack export calls will end
up as output properties on the resulting stack component in the checkpoint file. This is meant for internal
runtime use only and is used by the Python SDK entrypoint program.
Run the given function inside of a new stack resource. This ensures that any stack export calls
will end up as output properties on the resulting stack component in the checkpoint file. This
is meant for internal runtime use only and is used by the Python SDK entrypoint program.
"""
try:
Stack(func)
@ -49,12 +51,12 @@ async def run_in_stack(func: Callable):
# Wait for all outstanding RPCs to retire.
await RPC_MANAGER.wait_for_outstanding_rpcs()
# Asyncio event loops require that all outstanding tasks be completed by the time that the event
# loop closes. If we're at this point and there are no outstanding RPCs, we should just cancel
# all outstanding tasks.
# Asyncio event loops require that all outstanding tasks be completed by the time that the
# event loop closes. If we're at this point and there are no outstanding RPCs, we should
# just cancel all outstanding tasks.
#
# We will occasionally start tasks deliberately that we know will never complete. We must cancel
# them before shutting down the event loop.
# We will occasionally start tasks deliberately that we know will never complete. We must
# cancel them before shutting down the event loop.
log.debug("Canceling all outstanding tasks")
for task in asyncio.Task.all_tasks():
# Don't kill ourselves, that would be silly.
@ -92,7 +94,7 @@ class Stack(ComponentResource):
try:
func()
finally:
self.register_outputs(self.outputs)
self.register_outputs(massage(self.outputs, []))
# Intentionally leave this resource installed in case subsequent async work uses it.
def output(self, name: str, value: Any):
@ -100,3 +102,84 @@ class Stack(ComponentResource):
Export a stack output with a given name and value.
"""
self.outputs[name] = value
# Note: we use a List here instead of a set as many objects are unhashable. This is inefficient,
# but python seems to offer no alternative.
def massage(attr: Any, seen: List[Any]):
"""
massage takes an arbitrary python value and attempts to *deeply* convert it into
plain-old-python-value that can registered as an output. In general, this means leaving alone
things like strings, ints, bools. However, it does mean trying to make other values into either
lists or dictionaries as appropriate. In general, iterable things are turned into lists, and
dictionary-like things are turned into dictionaries.
"""
# Basic primitive types (numbers, booleans, strings, etc.) don't need any special handling.
if is_primitive(attr):
return attr
# 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
# the popo objects.
if isinstance(attr, Resource):
return attr.urn
# 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 = {}
for key, value in attr.items():
# ignore private keys
if not key.startswith("_"):
result[key] = massage(value, seen)
return result
if isinstance(attr, Output):
return attr.apply(lambda v: massage(v, seen))
if hasattr(attr, "__dict__"):
# recurse on the dictionary itself. It will be handled above.
return massage(attr.__dict__, seen)
# finally, recurse through iterables, converting into a list of massaged values.
return [massage(a, seen) for a in attr]
def reference_contains(val1: Any, seen: List[Any]) -> bool:
for val2 in seen:
if val1 is val2:
return True
return False
def is_primitive(attr: Any) -> bool:
if attr is None:
return True
if isinstance(attr, str):
return True
# dictionaries, lists and dictionary-like things are not primitive.
if isinstance(attr, dict):
return False
if hasattr(attr, "__dict__"):
return False
try:
iter(attr)
return False
except TypeError:
pass
return True

View file

@ -13,4 +13,24 @@
# limitations under the License.
import pulumi
pulumi.export("the-coolest", "pulumi")
class TestClass:
def __init__(self):
self.num = 1
self._private = 2
recursive = {"a": 1}
recursive["b"] = 2
recursive["c"] = recursive
pulumi.export("string", "pulumi")
pulumi.export("number", 1)
pulumi.export("boolean", True)
pulumi.export("list", [])
pulumi.export("list_with_none", [None])
pulumi.export("list_of_lists", [[], []])
pulumi.export("list_of_outputs", [[pulumi.Output.from_input(1)], pulumi.Output.from_input([2])])
pulumi.export("set", set(["val"]))
pulumi.export("dict", {"a": 1})
pulumi.export("output", pulumi.Output.from_input(1))
pulumi.export("class", TestClass())
pulumi.export("recursive", recursive)

View file

@ -27,5 +27,16 @@ class StackOutputTest(LanghostTest):
def register_resource_outputs(self, _ctx, _dry_run, _urn, ty, _name, _resource, outputs):
self.assertEqual(ty, "pulumi:pulumi:Stack")
self.assertDictEqual({
"the-coolest": "pulumi"
"string": "pulumi",
"number": 1.0,
"boolean": True,
"list": [],
"list_with_none": [None],
"list_of_lists": [[], []],
"list_of_outputs": [[1], [2]],
"set": ["val"],
"dict": {"a": 1.0},
"output": 1.0,
"class": {"num": 1.0},
"recursive": {"a": 1.0, "b": 2.0},
}, outputs)