pulumi/sdk/python/lib/test
Will Jones 79e814fe0f
Don't lift dunder attributes on Python `Output`s (#16439)
`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
2024-06-22 00:04:34 +00:00
..
automation Add removeStack options to NodeJS Auto API SDK (#16333) 2024-06-14 08:35:06 +00:00
data/lazy_import_test [sdk/python] Workaround lazy module regression (#16038) 2024-04-24 06:53:59 +00:00
langhost Handful of mypy fixes in the python test code (#16091) 2024-05-02 17:46:52 +00:00
provider Use black to format lib/test (#16028) 2024-04-23 08:29:58 +00:00
runtime Use black to format lib/test (#16028) 2024-04-23 08:29:58 +00:00
__init__.py Fix a couple of issues when projecting Protobuf and UNKNOWN in Python (#1468) 2018-06-06 16:09:07 -07:00
conftest.py Use black to format lib/test (#16028) 2024-04-23 08:29:58 +00:00
helpers.py Use black to format lib/test (#16028) 2024-04-23 08:29:58 +00:00
test_broken_dynamic_provider.py Use black to format lib/test (#16028) 2024-04-23 08:29:58 +00:00
test_config.py Use black to format lib/test (#16028) 2024-04-23 08:29:58 +00:00
test_deprecated.py Don't incorrectly emit deprecated warnings (#16400) 2024-06-17 16:04:55 +00:00
test_invoke.py Use black to format lib/test (#16028) 2024-04-23 08:29:58 +00:00
test_monitor_termination.py Use black to format lib/test (#16028) 2024-04-23 08:29:58 +00:00
test_next_serialize.py More descriptive exception in serialize_property (#16098) 2024-05-10 11:34:18 +00:00
test_output.py Don't lift dunder attributes on Python `Output`s (#16439) 2024-06-22 00:04:34 +00:00
test_resource.py Start mypy linting lib/test (#16040) 2024-04-30 12:54:13 +00:00
test_runtime_to_json.py Use black to format lib/test (#16028) 2024-04-23 08:29:58 +00:00
test_stack_reference.py Use black to format lib/test (#16028) 2024-04-23 08:29:58 +00:00
test_stack_registers_outputs.py Use black to format lib/test (#16028) 2024-04-23 08:29:58 +00:00
test_translate_output_properties.py Start mypy linting lib/test (#16040) 2024-04-30 12:54:13 +00:00
test_types_input_type.py Start mypy linting lib/test (#16040) 2024-04-30 12:54:13 +00:00
test_types_input_type_types.py Start mypy linting lib/test (#16040) 2024-04-30 12:54:13 +00:00
test_types_output_type.py Start mypy linting lib/test (#16040) 2024-04-30 12:54:13 +00:00
test_types_resource_types.py Start mypy linting lib/test (#16040) 2024-04-30 12:54:13 +00:00
test_urn.py Use black to format lib/test (#16028) 2024-04-23 08:29:58 +00:00
test_utils.py Start mypy linting lib/test (#16040) 2024-04-30 12:54:13 +00:00