cloudtools / troposphere

troposphere - Python library to create AWS CloudFormation descriptions
BSD 2-Clause "Simplified" License
4.93k stars 1.45k forks source link

Accept any sequence of resources, parameters, and outputs #2033

Closed michael-k closed 1 year ago

michael-k commented 2 years ago

When type checking the code below against troposphere 4.0.1, mypy complains about:

example.py:28: error: Value of type variable "__ResourceTypeVar" of "broken_bound" cannot be "List[AWSObject]"  [type-var]
example.py:29: error: Value of type variable "__ResourceTypeVarNotBound" of "broken_not_bound" cannot be "List[AWSObject]"  [type-var]

For works(resource: List[BaseAWSObject]), mypy is able to infer the correct type, but it fails when a TypeVar is used. I'm not using pyright and therefore cannot say if it has the same problem.

We could use @overload instead of TypeVar to help mypy infer the correct type.

Using neither a TypeVar nor @overload isn't an option, because it would not annotate that add_X returns the same type it received.

Note that with this change any Sequence[X] is accepted and processed and not just List[X].

from typing import List, Sequence, TypeVar, Union

from troposphere import BaseAWSObject
from troposphere.sns import Topic
from troposphere.s3 import Bucket

__ResourceTypeVar = TypeVar(
    "__ResourceTypeVar", bound=Union[BaseAWSObject, List[BaseAWSObject]]
)

__ResourceTypeVarNotBound = TypeVar(
    "__ResourceTypeVarNotBound", BaseAWSObject, List[BaseAWSObject]
)

__ResourceTypeVarWorks = TypeVar(
    "__ResourceTypeVarWorks", bound=Union[BaseAWSObject, Sequence[BaseAWSObject]]
)

__ResourceTypeVarNotBoundWorks = TypeVar(
    "__ResourceTypeVarNotBoundWorks", BaseAWSObject, Sequence[BaseAWSObject]
)

def main() -> None:
    topic = Topic("Topic")
    bucket = Bucket("Bucket")
    broken_bound([topic, bucket])  # line 28
    broken_not_bound([topic, bucket])  # line 29

    works([topic, bucket])
    works_bound([topic, bucket])
    works_not_bound([topic, bucket])

def broken_bound(resources: __ResourceTypeVar) -> None:
    pass

def broken_not_bound(resources: __ResourceTypeVarNotBound) -> None:
    pass

def works(resource: List[BaseAWSObject]) -> None:
    pass

def works_bound(resources: __ResourceTypeVarWorks) -> None:
    pass

def works_not_bound(resources: __ResourceTypeVarNotBoundWorks) -> None:
    pass