mirror of https://github.com/pulumi/pulumi.git
79e814fe0f
`Output`s are a central part of Pulumi programs. As well as tracking dependencies between resources (e.g. that `A`'s input `x` comes from `B`'s output `y`), they allow us to specify that some value will only be available at a future point (e.g. after a resource has been created or updated). This is typically done in each language by implementing `Output`s using some asynchronous or future value, such as NodeJS's `Promise`. In Python, we use `asyncio` `Task`s. In order to make `Output`s ergonomic to use, we implement "lifting" of properties in languages that support it. Suppose for instance we have an object `c` that is an instance of the following class `C`: ```python class C: x: str y: int def __init__(self, x: str, y: int) -> None: self.x = x self.y = y c = C("x", 42) ``` Because `c: C`, we have that `c.x == "x"` and `c.y == 42` as we might expect. Consider though some output property of a resource that produces a `C`. This property will be of type `Output[C]`, since the value of type `C` won't be available until the resource has been set up by Pulumi as part of program execution. If we want to pass that output's `x` value to some other resource, we might have to write: ```python r1 = ... # r1 has a property c: Output[C] r2 = R("r", RArgs(x=r1.c.apply(lambda cc: cc.x))) ``` Observe that we have to use `apply` to unwrap the output and access the property inside. This is tedious and ugly, and exactly the problem lifting solves. Lifting allows us to write `r1.c.x` and have it be implemented as `r1.c.apply(lambda cc: cc.x)` under the hood. In Python, this is achieved using Python's `__getattr__` "dunder" method ("dunder" being short for "double underscore", the convention adopted for "special" identifiers in Python). `__getattr__` allows us to perform dynamic property lookup on an object, which in the case of `Output` we use to delegate property access to the underlying value using `apply`. This works really well and contributes significantly to Pulumi programs being easy to read and write in Python. Unfortunately, it has a flaw: `__getattr__` is also used to check whether attributes exist on an object (using `hasattr`), and thus has a contract whereby it is _synchronously_ expected to raise an `AttributeError` if an attribute does not exist. In returning an _asynchronous_ task (that may _later_ resolve to an `AttributeError`), `Output` is in violation of this contract. This means that code which calls e.g. `hasattr(r1.c, "z")` will yield a future task that if resolved, will blow up with an `AttributeError`. Historically, this hasn't really been a problem. With the advent of https://github.com/pulumi/pulumi/pull/15744 and subsequent improvements/fixes, however, this is now an issue, since we explicitly await all outstanding outputs before program termination. In the example above, for instance, the orphaned task will be forced at the end of program execution. The `AttributeError` will rear its head and Pulumi will exit with an error and stack trace. This commit implements a fix proportional to the cases where this appears to be a problem. Namely, libraries such as Pydantic that use dunder attributes in their implementation and check for the presence of these attributes when executing. When `__getattr__` is called with a dunder attribute, we synchronously `raise AttributeError` rather than lifting it. There are (we believe) no cases where this would affect a Pulumi-generated SDK (since dunder names aren't generally used for public resource properties) and the dunder properties on the `Output` itself (e.g. `__dict__`, etc.) will continue to work since their resolution will succeed normally and a call to `__getattr__` will thus not be made. Fixes #16399 |
||
---|---|---|
.. | ||
automation | ||
data/lazy_import_test | ||
langhost | ||
provider | ||
runtime | ||
__init__.py | ||
conftest.py | ||
helpers.py | ||
test_broken_dynamic_provider.py | ||
test_config.py | ||
test_deprecated.py | ||
test_invoke.py | ||
test_monitor_termination.py | ||
test_next_serialize.py | ||
test_output.py | ||
test_resource.py | ||
test_runtime_to_json.py | ||
test_stack_reference.py | ||
test_stack_registers_outputs.py | ||
test_translate_output_properties.py | ||
test_types_input_type.py | ||
test_types_input_type_types.py | ||
test_types_output_type.py | ||
test_types_resource_types.py | ||
test_urn.py | ||
test_utils.py |