Revert "[python/sdk] - Remove python 3.6 support (#8161)" (#8332)

This reverts commit 895ae970ac.
This commit is contained in:
Emiliza Gutierrez 2021-11-02 10:19:01 -07:00 committed by GitHub
parent e629bc32d5
commit d243efae19
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 106 additions and 79 deletions

View file

@ -37,8 +37,3 @@
- [codegen/python] - Fixes issue with `$fn_output` functions failing in
preview when called with unknown arguments
[#8320](https://github.com/pulumi/pulumi/pull/8320)
### Miscellaneous
- [sdk/python] - Drop support for python 3.6
[#8161](https://github.com/pulumi/pulumi/pull/8161)

View file

@ -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://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) |
| 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) |
### Languages
@ -182,7 +182,7 @@ details of the core Pulumi CLI and [programming model concepts](https://www.pulu
| -- | -------- | ------ | ------- |
| <img src="https://www.pulumi.com/logos/tech/logo-js.png" height=38 /> | [JavaScript](./sdk/nodejs) | Stable | Node.js 12+ |
| <img src="https://www.pulumi.com/logos/tech/logo-ts.png" height=38 /> | [TypeScript](./sdk/nodejs) | Stable | Node.js 12+ |
| <img src="https://www.pulumi.com/logos/tech/logo-python.png" height=38 /> | [Python](./sdk/python) | Stable | Python 3.7+ |
| <img src="https://www.pulumi.com/logos/tech/logo-python.png" height=38 /> | [Python](./sdk/python) | Stable | Python 3.6+ |
| <img src="https://www.pulumi.com/logos/tech/logo-golang.png" height=38 /> | [Go](./sdk/go) | Stable | Go 1.14+ |
| <img src="https://www.pulumi.com/logos/tech/dotnet.png" height=38 /> | [.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.

View file

@ -69,15 +69,21 @@ if __name__ == "__main__":
successful = False
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)
# 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)
# 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.

View file

@ -276,6 +276,7 @@ from . import _utils
T = TypeVar('T')
_PULUMI_NAME = "_pulumi_name"
_PULUMI_INPUT_TYPE = "_pulumi_input_type"
_PULUMI_OUTPUT_TYPE = "_pulumi_output_type"
@ -286,27 +287,22 @@ _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")
@ -394,10 +390,8 @@ 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)
@ -405,7 +399,6 @@ 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)
@ -422,7 +415,6 @@ 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.
@ -438,7 +430,6 @@ 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
@ -549,7 +540,6 @@ 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")
@ -561,7 +551,6 @@ 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
@ -651,22 +640,41 @@ 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
else:
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__"):
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__"):
return tp.__args__
return ()
def _is_union_type(tp):
return (tp is Union or
isinstance(tp, typing._GenericAlias) and tp.__origin__ is Union) # type: ignore
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
def _is_optional_type(tp):
@ -911,29 +919,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): # Optional[InputType[T]]
if isInputType(args) and args[2] is type(None): # Optiona[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)

View file

@ -24,6 +24,8 @@ 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
@ -87,7 +89,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.all_tasks(loop)
pending = asyncio.Task.all_tasks(loop) if _py_version_less_than_3_7 else asyncio.all_tasks(loop) # pylint: disable=no-member
log.debug(f"Cancelling {len(pending)} tasks.")
for task in pending:
task.cancel()

View file

@ -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
from ..runtime.stack import Stack, run_pulumi_func, wait_for_rpcs
if TYPE_CHECKING:
from ..resource import Resource
@ -68,8 +68,7 @@ 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.
@ -94,8 +93,7 @@ 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
"""
@ -106,26 +104,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 args MockCallArgs
:param MockCallArgs args.
"""
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 args MockResourceArgs
:param MockResourceArgs args.
"""
return "", {}
@ -145,9 +143,9 @@ class MockMonitor:
def make_urn(self, parent: str, type_: str, name: str) -> str:
if parent != "":
qualified_type = parent.split("::")[2]
parent_type = qualified_type.split("$").pop()
type_ = parent_type + "$" + type_
qualifiedType = parent.split("::")[2]
parentType = qualifiedType.split("$").pop()
type_ = parentType + "$" + type_
return "urn:pulumi:" + "::".join([get_stack(), get_project(), type_, name])
@ -170,9 +168,7 @@ 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, {}))
@ -233,7 +229,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:

View file

@ -44,10 +44,7 @@ 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

View file

@ -22,6 +22,7 @@ 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:
@ -30,9 +31,9 @@ if TYPE_CHECKING:
def _get_running_tasks() -> List[asyncio.Task]:
pending = []
for task in asyncio.all_tasks():
for task in _all_tasks():
# Don't kill ourselves, that would be silly.
if not task == asyncio.current_task():
if not task == _get_current_task():
pending.append(task)
return pending

View file

@ -12,8 +12,31 @@
# 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:
"""
@ -32,9 +55,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 = asyncio.current_task(loop)
task = _get_current_task(loop)
if task is not None:
asyncio.tasks._leave_task(loop, task) # type: ignore
_leave_task(loop, task)
# 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.
@ -44,21 +67,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:
asyncio.tasks._enter_task(loop, task) # type: ignore
_enter_task(loop, task)
# Return the result of the future.
return fut.result()

View file

@ -35,7 +35,6 @@ setup(name='pulumi',
'py.typed'
]
},
python_requires='>=3.7',
# Keep this list in sync with Pipfile
install_requires=[
'protobuf>=3.6.0',