diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index bb6ee6d00..8778ff898 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -21,3 +21,8 @@ - [sdk/go] - Fix regression marshaling assets/archives. [#8290](https://github.com/pulumi/pulumi/pull/8290) + +### Miscellaneous + +- [sdk/python] - Drop support for python 3.6 + [#8161](https://github.com/pulumi/pulumi/pull/8161) diff --git a/README.md b/README.md index 8fcca411d..7833bbe47 100644 --- a/README.md +++ b/README.md @@ -173,8 +173,8 @@ details of the core Pulumi CLI and [programming model concepts](https://www.pulu | Architecture | Build Status | | ------------ | ------------ | -| Linux/macOS x64 | [![Linux x64 Build Status](https://travis-ci.com/pulumi/pulumi.svg?token=cTUUEgrxaTEGyecqJpDn&branch=master)](https://travis-ci.com/pulumi/pulumi) | -| Windows x64 | [![Windows x64 Build Status](https://ci.appveyor.com/api/projects/status/uqrduw6qnoss7g4i?svg=true&branch=master)](https://ci.appveyor.com/project/pulumi/pulumi) | +| Linux/macOS x64 | ![Linux x64 Build Status](https://github.com/pulumi/pulumi/actions/workflows/master.yml/badge.svg) | +| Windows x64 | ![Windows x64 Build Status](https://github.com/pulumi/pulumi/actions/workflows/master.yml/badge.svg) | ### Languages @@ -182,7 +182,7 @@ details of the core Pulumi CLI and [programming model concepts](https://www.pulu | -- | -------- | ------ | ------- | | | [JavaScript](./sdk/nodejs) | Stable | Node.js 12+ | | | [TypeScript](./sdk/nodejs) | Stable | Node.js 12+ | -| | [Python](./sdk/python) | Stable | Python 3.6+ | +| | [Python](./sdk/python) | Stable | Python 3.7+ | | | [Go](./sdk/go) | Stable | Go 1.14+ | | | [.NET (C#/F#/VB.NET)](./sdk/dotnet) | Stable | .NET Core 3.1+ | @@ -193,5 +193,5 @@ full list of supported cloud and infrastructure providers. ## Contributing -Please See [CONTRIBUTING.md](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md) +Please see [CONTRIBUTING.md](https://github.com/pulumi/pulumi/blob/master/CONTRIBUTING.md) for information on building Pulumi from source or contributing improvements. diff --git a/sdk/python/cmd/pulumi-language-python-exec b/sdk/python/cmd/pulumi-language-python-exec index 72a0f0662..85f303fbd 100755 --- a/sdk/python/cmd/pulumi-language-python-exec +++ b/sdk/python/cmd/pulumi-language-python-exec @@ -69,21 +69,15 @@ if __name__ == "__main__": successful = False - # asyncio.get_running_loop was only added in python 3.7 but we still support python 3.6 - # In python 3.10, asyncio.get_event_loop prints a deprecation warning if no loop is present - # This code will be cleaned up as part of https://github.com/pulumi/pulumi/issues/8131 - if sys.version_info[0] == 3 and sys.version_info[1] < 7: - loop = asyncio.get_event_loop() - else: - try: - # The docs for get_running_loop are somewhat misleading because they state: - # This function can only be called from a coroutine or a callback. However, if the function is - # called from outside a coroutine or callback (the standard case when running `pulumi up`), the function - # raises a RuntimeError as expected and falls through to the exception clause below. - loop = asyncio.get_running_loop() - except RuntimeError: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) + try: + # The docs for get_running_loop are somewhat misleading because they state: + # This function can only be called from a coroutine or a callback. However, if the function is + # called from outside a coroutine or callback (the standard case when running `pulumi up`), the function + # raises a RuntimeError as expected and falls through to the exception clause below. + loop = asyncio.get_running_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) # We are (unfortunately) suppressing the log output of asyncio to avoid showing to users some of the bad things we # do in our programming model. diff --git a/sdk/python/lib/pulumi/_types.py b/sdk/python/lib/pulumi/_types.py index d757f779e..e29ec989d 100644 --- a/sdk/python/lib/pulumi/_types.py +++ b/sdk/python/lib/pulumi/_types.py @@ -276,7 +276,6 @@ from . import _utils T = TypeVar('T') - _PULUMI_NAME = "_pulumi_name" _PULUMI_INPUT_TYPE = "_pulumi_input_type" _PULUMI_OUTPUT_TYPE = "_pulumi_output_type" @@ -287,22 +286,27 @@ _TRANSLATE_PROPERTY = "_translate_property" def is_input_type(cls: type) -> bool: return hasattr(cls, _PULUMI_INPUT_TYPE) + def is_output_type(cls: type) -> bool: return hasattr(cls, _PULUMI_OUTPUT_TYPE) class _MISSING_TYPE: pass + + MISSING = _MISSING_TYPE() """ MISSING is a singleton sentinel object to detect if a parameter is supplied or not. """ + class _Property: """ Represents a Pulumi property. It is not meant to be created outside this module, rather, the property() function should be used. """ + def __init__(self, name: str, default: Any = MISSING) -> None: if not name: raise TypeError("Missing name argument") @@ -390,8 +394,10 @@ def _create_py_property(a_name: str, pulumi_name: str, typ: Any, setter: bool = """ Returns a Python property getter that looks up the value using get. """ + def getter_fn(self): return get(self, a_name) + getter_fn.__name__ = a_name getter_fn.__annotations__ = {"return": typ} setattr(getter_fn, _PULUMI_NAME, pulumi_name) @@ -399,6 +405,7 @@ def _create_py_property(a_name: str, pulumi_name: str, typ: Any, setter: bool = if setter: def setter_fn(self, value): return set(self, a_name, value) + setter_fn.__name__ = a_name setter_fn.__annotations__ = {"value": typ} return builtins.property(fget=getter_fn, fset=setter_fn) @@ -415,6 +422,7 @@ def _py_properties(cls: type) -> Iterator[Tuple[str, str, builtins.property]]: if pulumi_name is not MISSING: yield (python_name, pulumi_name, prop) + def input_type(cls: Type[T]) -> Type[T]: """ Returns the same class as was passed in, but marked as an input type. @@ -430,6 +438,7 @@ def input_type(cls: Type[T]) -> Type[T]: def create_setter(name: str) -> Callable: def setter_fn(self, value): set(self, name, value) + return setter_fn # Now, process the class's properties, replacing properties with empty setters with @@ -540,6 +549,7 @@ def getter(_fn=None, *, name: Optional[str] = None): name is the Pulumi property name. If not set, the name of the function is used. """ + def decorator(fn: Callable) -> Callable: if not callable(fn): raise TypeError("Expected fn to be callable") @@ -551,6 +561,7 @@ def getter(_fn=None, *, name: Optional[str] = None): def get_fn(self): # Get the value using the Python name, which is the name of the function. return get(self, fn.__name__) + fn = get_fn setattr(fn, _PULUMI_NAME, pulumi_name) return fn @@ -640,41 +651,22 @@ if sys.version_info[:2] >= (3, 8): get_origin = typing.get_origin # type: ignore # pylint: disable=no-member get_args = typing.get_args # type: ignore -elif sys.version_info[:2] >= (3, 7): - def get_origin(tp): - if isinstance(tp, typing._GenericAlias): # type: ignore - return tp.__origin__ - return None - - def get_args(tp): - if isinstance(tp, typing._GenericAlias): # type: ignore - return tp.__args__ - return () else: def get_origin(tp): - if hasattr(tp, "__origin__"): + if isinstance(tp, typing._GenericAlias): # type: ignore return tp.__origin__ return None + def get_args(tp): - # Emulate the behavior of get_args for Union on Python 3.6. - if _is_union_type(tp) and hasattr(tp, "_subs_tree"): - tree = tp._subs_tree() - if isinstance(tree, tuple) and len(tree) > 1: - def _eval(args): - return tuple(arg if not isinstance(arg, tuple) else arg[0][_eval(arg[1:])] for arg in args) - return _eval(tree[1:]) - if hasattr(tp, "__args__"): + if isinstance(tp, typing._GenericAlias): # type: ignore return tp.__args__ return () def _is_union_type(tp): - if sys.version_info[:2] >= (3, 7): - return (tp is Union or - isinstance(tp, typing._GenericAlias) and tp.__origin__ is Union) # type: ignore - # pylint: disable=unidiomatic-typecheck, no-member - return type(tp) is typing._Union # type: ignore + return (tp is Union or + isinstance(tp, typing._GenericAlias) and tp.__origin__ is Union) # type: ignore def _is_optional_type(tp): @@ -919,29 +911,29 @@ def unwrap_type(val: type) -> type: def isInputType(args): assert len(args) > 1 return (is_input_type(args[0]) and - args[1] is dict or get_origin(args[1]) in {dict, Dict, Mapping, collections.abc.Mapping}) + args[1] is dict or get_origin(args[1]) in {dict, Dict, Mapping, collections.abc.Mapping}) - def isInput(args, i = 1): + def isInput(args, i=1): assert len(args) > i + 1 return (get_origin(args[i]) in {typing.Awaitable, collections.abc.Awaitable} and - get_origin(args[i + 1]) is Output) + get_origin(args[i + 1]) is Output) args = get_args(val) if len(args) == 2: - if isInputType(args): # InputType[T] + if isInputType(args): # InputType[T] return args[0] elif len(args) == 3: - if isInput(args): # Input[T] + if isInput(args): # Input[T] return args[0] - if isInputType(args) and args[2] is type(None): # Optiona[InputType[T]] + if isInputType(args) and args[2] is type(None): # Optional[InputType[T]] return args[0] elif len(args) == 4: - if isInput(args) and args[3] is type(None): # Optional[Input[T]] + if isInput(args) and args[3] is type(None): # Optional[Input[T]] return args[0] - if isInputType(args) and isInput(args, 2): # Input[InputType[T]] + if isInputType(args) and isInput(args, 2): # Input[InputType[T]] return args[0] elif len(args) == 5: - if isInputType(args) and isInput(args, 2) and args[4] is type(None): # Optional[Input[InputType[T]]] + if isInputType(args) and isInput(args, 2) and args[4] is type(None): # Optional[Input[InputType[T]]] return args[0] return unwrap_optional_type(val) diff --git a/sdk/python/lib/pulumi/automation/_server.py b/sdk/python/lib/pulumi/automation/_server.py index 6a61b4e29..16f87eae4 100644 --- a/sdk/python/lib/pulumi/automation/_server.py +++ b/sdk/python/lib/pulumi/automation/_server.py @@ -24,8 +24,6 @@ from ..runtime.proto import language_pb2, plugin_pb2, LanguageRuntimeServicer from ..runtime import run_in_stack, reset_options, set_all_config from ..errors import RunError -_py_version_less_than_3_7 = sys.version_info[0] == 3 and sys.version_info[1] < 7 - class LanguageServer(LanguageRuntimeServicer): program: PulumiFn @@ -89,7 +87,7 @@ class LanguageServer(LanguageRuntimeServicer): # at the time the loop is closed, which results in a `Task was destroyed but it is pending!` error being # logged to stdout. To avoid this, we collect all the unresolved tasks in the loop and cancel them before # closing the loop. - pending = asyncio.Task.all_tasks(loop) if _py_version_less_than_3_7 else asyncio.all_tasks(loop) # pylint: disable=no-member + pending = asyncio.all_tasks(loop) log.debug(f"Cancelling {len(pending)} tasks.") for task in pending: task.cancel() diff --git a/sdk/python/lib/pulumi/runtime/mocks.py b/sdk/python/lib/pulumi/runtime/mocks.py index 6befe6ce0..629337a3a 100644 --- a/sdk/python/lib/pulumi/runtime/mocks.py +++ b/sdk/python/lib/pulumi/runtime/mocks.py @@ -25,7 +25,7 @@ from . import rpc, rpc_manager from .settings import Settings, configure, get_stack, get_project, get_root_resource from .sync_await import _ensure_event_loop, _sync_await from ..runtime.proto import engine_pb2, provider_pb2, resource_pb2 -from ..runtime.stack import Stack, run_pulumi_func, wait_for_rpcs +from ..runtime.stack import Stack, run_pulumi_func if TYPE_CHECKING: from ..resource import Resource @@ -68,7 +68,8 @@ class MockResourceArgs: resource_id: Optional[str] = None, custom: Optional[bool] = None) -> None: """ - :param str typ: The token that indicates which resource type is being constructed. This token is of the form "package:module:type". + :param str typ: The token that indicates which resource type is being constructed. + This token is of the form "package:module:type". :param str name: The logical name of the resource instance. :param dict inputs: The inputs for the resource. :param str provider: The identifier of the provider instance being used to manage this resource. @@ -93,7 +94,8 @@ class MockCallArgs: def __init__(self, token: str, args: dict, provider: str) -> None: """ - :param str token: The token that indicates which function is being called. This token is of the form "package:module:function". + :param str token: The token that indicates which function is being called. + This token is of the form "package:module:function". :param dict args: The arguments provided to the function call. :param str provider: The identifier of the provider instance being used to make the call """ @@ -104,26 +106,26 @@ class MockCallArgs: class Mocks(ABC): """ - Mocks is an abstract class that allows subclasses to replace operations normally implemented by the Pulumi engine with - their own implementations. This can be used during testing to ensure that calls to provider functions and resource constructors - return predictable values. + Mocks is an abstract class that allows subclasses to replace operations normally implemented by the Pulumi + engine with their own implementations. This can be used during testing to ensure that calls to provider + functions and resource constructors return predictable values. """ @abstractmethod - def call(self, args: MockCallArgs) -> Tuple[dict, Optional[List[Tuple[str,str]]]]: + def call(self, args: MockCallArgs) -> Tuple[dict, Optional[List[Tuple[str, str]]]]: """ call mocks provider-implemented function calls (e.g. aws.get_availability_zones). - :param MockCallArgs args. + :param args MockCallArgs """ return {}, None @abstractmethod def new_resource(self, args: MockResourceArgs) -> Tuple[Optional[str], dict]: """ - new_resource mocks resource construction calls. This function should return the physical identifier and the output properties - for the resource being constructed. + new_resource mocks resource construction calls. This function should return the physical identifier and + the output properties for the resource being constructed. - :param MockResourceArgs args. + :param args MockResourceArgs """ return "", {} @@ -143,9 +145,9 @@ class MockMonitor: def make_urn(self, parent: str, type_: str, name: str) -> str: if parent != "": - qualifiedType = parent.split("::")[2] - parentType = qualifiedType.split("$").pop() - type_ = parentType + "$" + type_ + qualified_type = parent.split("::")[2] + parent_type = qualified_type.split("$").pop() + type_ = parent_type + "$" + type_ return "urn:pulumi:" + "::".join([get_stack(), get_project(), type_, name]) @@ -168,7 +170,9 @@ class MockMonitor: if isinstance(tup, dict): (ret, failures) = (tup, None) else: - (ret, failures) = tup[0], [provider_pb2.CheckFailure(property=failure[0], reason=failure[1]) for failure in tup[1]] + (ret, failures) = tup[0], [ + provider_pb2.CheckFailure(property=failure[0], reason=failure[1]) for failure in tup[1] + ] ret_proto = _sync_await(rpc.serialize_properties(ret, {})) @@ -229,7 +233,7 @@ class MockMonitor: # Support for "outputValues" is deliberately disabled for the mock monitor so # instances of `Output` don't show up in `MockResourceArgs` inputs. has_support = request.id in {"secrets", "resourceReferences"} - return type('SupportsFeatureResponse', (object,), {'hasSupport' : has_support}) + return type('SupportsFeatureResponse', (object,), {'hasSupport': has_support}) class MockEngine: diff --git a/sdk/python/lib/pulumi/runtime/rpc_manager.py b/sdk/python/lib/pulumi/runtime/rpc_manager.py index 11dc565cd..a346e8c2f 100644 --- a/sdk/python/lib/pulumi/runtime/rpc_manager.py +++ b/sdk/python/lib/pulumi/runtime/rpc_manager.py @@ -44,7 +44,10 @@ class RPCManager: def __init__(self): self.clear() - def do_rpc(self, name: str, rpc_function: Callable[..., Awaitable[Tuple[Any, Exception]]]) -> Callable[..., Awaitable[Tuple[Any, Exception]]]: + def do_rpc(self, + name: str, + rpc_function: Callable[..., Awaitable[Tuple[Any, Exception]]] + ) -> Callable[..., Awaitable[Tuple[Any, Exception]]]: """ Wraps a given RPC function by producing an awaitable function suitable to be run in the asyncio event loop. The wrapped function catches all unhandled exceptions and reports them to the exception diff --git a/sdk/python/lib/pulumi/runtime/stack.py b/sdk/python/lib/pulumi/runtime/stack.py index b991c5fac..1eb0478b4 100644 --- a/sdk/python/lib/pulumi/runtime/stack.py +++ b/sdk/python/lib/pulumi/runtime/stack.py @@ -22,7 +22,6 @@ from typing import Callable, Any, Dict, List, TYPE_CHECKING from ..resource import ComponentResource, Resource, ResourceTransformation from .settings import get_project, get_stack, get_root_resource, is_dry_run, set_root_resource from .rpc_manager import RPC_MANAGER -from .sync_await import _all_tasks, _get_current_task from .. import log if TYPE_CHECKING: @@ -31,9 +30,9 @@ if TYPE_CHECKING: def _get_running_tasks() -> List[asyncio.Task]: pending = [] - for task in _all_tasks(): + for task in asyncio.all_tasks(): # Don't kill ourselves, that would be silly. - if not task == _get_current_task(): + if not task == asyncio.current_task(): pending.append(task) return pending diff --git a/sdk/python/lib/pulumi/runtime/sync_await.py b/sdk/python/lib/pulumi/runtime/sync_await.py index fd5ca34e9..355cae062 100644 --- a/sdk/python/lib/pulumi/runtime/sync_await.py +++ b/sdk/python/lib/pulumi/runtime/sync_await.py @@ -12,31 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. import asyncio -import sys from typing import Any, Awaitable -# If we are not running on Python 3.7 or later, we need to swap the Python implementation of Task in for the C -# implementation in order to support synchronous invokes. -if sys.version_info[0] == 3 and sys.version_info[1] < 7: - asyncio.Task = asyncio.tasks._PyTask - asyncio.tasks.Task = asyncio.tasks._PyTask - - def enter_task(loop, task): - task.__class__._current_tasks[loop] = task - - def leave_task(loop, task): - task.__class__._current_tasks.pop(loop) - - _enter_task = enter_task - _leave_task = leave_task - _all_tasks = asyncio.Task.all_tasks - _get_current_task = asyncio.Task.current_task -else: - _enter_task = asyncio.tasks._enter_task # type: ignore - _leave_task = asyncio.tasks._leave_task # type: ignore - _all_tasks = asyncio.all_tasks # type: ignore - _get_current_task = asyncio.current_task # type: ignore - def _sync_await(awaitable: Awaitable[Any]) -> Any: """ @@ -55,9 +32,9 @@ def _sync_await(awaitable: Awaitable[Any]) -> Any: # If we are executing inside a task, pretend we've returned from its current callback--effectively yielding to # the event loop--by calling _leave_task. - task = _get_current_task(loop) + task = asyncio.current_task(loop) if task is not None: - _leave_task(loop, task) + asyncio.tasks._leave_task(loop, task) # type: ignore # Pump the event loop until the future is complete. This is the kernel of BaseEventLoop.run_forever, and may not # work with alternative event loop implementations. @@ -67,21 +44,21 @@ def _sync_await(awaitable: Awaitable[Any]) -> Any: # # See https://github.com/python/cpython/blob/3.6/Lib/asyncio/base_events.py#L1428-L1452 for the details of the # _run_once kernel with which we need to cooperate. - ntodo = len(loop._ready) # type: ignore + ntodo = len(loop._ready) # type: ignore while not fut.done() and not fut.cancelled(): - loop._run_once() # type: ignore - if loop._stopping: # type: ignore + loop._run_once() # type: ignore + if loop._stopping: # type: ignore break # If we drained the ready list past what a calling _run_once would have expected, fix things up by pushing # cancelled handles onto the list. - while len(loop._ready) < ntodo: # type: ignore + while len(loop._ready) < ntodo: # type: ignore handle = asyncio.Handle(lambda: None, [], loop) handle._cancelled = True - loop._ready.append(handle) # type: ignore + loop._ready.append(handle) # type: ignore # If we were executing inside a task, restore its context and continue on. if task is not None: - _enter_task(loop, task) + asyncio.tasks._enter_task(loop, task) # type: ignore # Return the result of the future. return fut.result() diff --git a/sdk/python/lib/setup.py b/sdk/python/lib/setup.py index 0f67cbcb7..1f272b225 100644 --- a/sdk/python/lib/setup.py +++ b/sdk/python/lib/setup.py @@ -35,6 +35,7 @@ setup(name='pulumi', 'py.typed' ] }, + python_requires='>=3.7', # Keep this list in sync with Pipfile install_requires=[ 'protobuf>=3.6.0',