From 4cb91b3229a79d6299af1e8e59dc8a35299777b9 Mon Sep 17 00:00:00 2001 From: Mitch Garnaat Date: Thu, 28 Mar 2019 10:16:56 -0700 Subject: [PATCH] feat(python): Add support for synchronous callbacks (#407) --- .../jsii-python-runtime/bin/generate-calc | 11 +- .../src/jsii/_kernel/__init__.py | 102 +++++++++++++++--- .../src/jsii/_kernel/providers/base.py | 15 ++- .../src/jsii/_kernel/providers/process.py | 22 +++- .../src/jsii/_kernel/types.py | 1 + .../tests/test_compliance.py | 53 ++++----- 6 files changed, 147 insertions(+), 57 deletions(-) diff --git a/packages/jsii-python-runtime/bin/generate-calc b/packages/jsii-python-runtime/bin/generate-calc index 6f71de2519..f6da108efc 100755 --- a/packages/jsii-python-runtime/bin/generate-calc +++ b/packages/jsii-python-runtime/bin/generate-calc @@ -23,11 +23,10 @@ subprocess.run( "install", "--force-reinstall", "--upgrade", - "--find-links", - os.path.abspath("."), - "--find-links", - os.path.abspath(".env/jsii-calc/python"), - "jsii-calc", - ], + ] + + + [x for x in os.listdir(".") if x.endswith(".whl")] + + + [os.path.join('.env/jsii-calc/python', x) for x in os.listdir(".env/jsii-calc/python") if x.endswith(".whl")], check=True, ) diff --git a/packages/jsii-python-runtime/src/jsii/_kernel/__init__.py b/packages/jsii-python-runtime/src/jsii/_kernel/__init__.py index 5c2b321903..3dcd0f5dea 100644 --- a/packages/jsii-python-runtime/src/jsii/_kernel/__init__.py +++ b/packages/jsii-python-runtime/src/jsii/_kernel/__init__.py @@ -2,22 +2,25 @@ import inspect import itertools -from typing import Any, List, Optional, Type +from typing import Any, List, Optional, Type, Union import functools import attr +from jsii.errors import JSIIError from jsii import _reference_map from jsii._utils import Singleton from jsii._kernel.providers import BaseProvider, ProcessProvider from jsii._kernel.types import JSClass, Referenceable +from jsii._kernel.types import Callback from jsii._kernel.types import ( EnumRef, LoadRequest, BeginRequest, CallbacksRequest, CreateRequest, + CreateResponse, CompleteRequest, DeleteRequest, EndRequest, @@ -30,6 +33,13 @@ StatsRequest, ObjRef, Override, + CompleteRequest, + CompleteResponse, + GetResponse, + SetResponse, + InvokeResponse, + KernelResponse, + BeginResponse ) @@ -40,12 +50,6 @@ class Object: __jsii_type__ = "Object" -def _handle_callback(kernel, callback): - obj = _reference_map.resolve_id(callback.invoke.objref.ref) - method = getattr(obj, callback.cookie) - return method(*callback.invoke.args) - - def _get_overides(klass: JSClass, obj: Any) -> List[Override]: overrides = [] @@ -120,6 +124,39 @@ def _make_reference_for_native(kernel, d): return d +def _handle_callback(kernel, callback): + # need to handle get, set requests here as well as invoke requests + if callback.invoke: + obj = _reference_map.resolve_id(callback.invoke.objref.ref) + method = getattr(obj, callback.cookie) + return method(*callback.invoke.args) + elif callback.get: + obj = _reference_map.resolve_id(callback.get.objref.ref) + return getattr(obj, callback.cookie) + elif callback.set: + obj = _reference_map.resolve_id(callback.set.objref.ref) + return setattr(obj, callback.cookie, callback.set.value) + else: + raise JSIIError("Callback does not contain invoke|get|set") + + +def _callback_till_result(kernel, response: Callback, response_type: Type[KernelResponse]) -> Any: + while isinstance(response, Callback): + try: + result = _handle_callback(kernel, response) + except Exception as exc: + response = kernel.sync_complete(response.cbid, str(exc), None, response_type) + else: + response = kernel.sync_complete(response.cbid, None, result, response_type) + + if isinstance(response, InvokeResponse): + return response.result + elif isinstance(response, GetResponse): + return response.value + else: + return response + + @attr.s(auto_attribs=True, frozen=True, slots=True) class Statistics: @@ -155,14 +192,17 @@ def create( overrides = _get_overides(klass, obj) - obj.__jsii_ref__ = self.provider.create( + response = self.provider.create( CreateRequest( fqn=klass.__jsii_type__, args=_make_reference_for_native(self, args), overrides=overrides, ) ) - + if isinstance(response, Callback): + obj.__jsii_ref__ = _callback_till_result(self, response, CreateResponse) + else: + obj.__jsii_ref__ = response return obj.__jsii_ref__ def delete(self, ref: ObjRef) -> None: @@ -170,18 +210,24 @@ def delete(self, ref: ObjRef) -> None: @_dereferenced def get(self, obj: Referenceable, property: str) -> Any: - return self.provider.get( + response = self.provider.get( GetRequest(objref=obj.__jsii_ref__, property=property) - ).value + ) + if isinstance(response, Callback): + return _callback_till_result(self, response, GetResponse) + else: + return response.value def set(self, obj: Referenceable, property: str, value: Any) -> None: - self.provider.set( + response = self.provider.set( SetRequest( objref=obj.__jsii_ref__, property=property, value=_make_reference_for_native(self, value), ) ) + if isinstance(response, Callback): + _callback_till_result(self, response, SetResponse) @_dereferenced def sget(self, klass: JSClass, property: str) -> Any: @@ -205,13 +251,17 @@ def invoke( if args is None: args = [] - return self.provider.invoke( + response = self.provider.invoke( InvokeRequest( objref=obj.__jsii_ref__, method=method, args=_make_reference_for_native(self, args), ) - ).result + ) + if isinstance(response, Callback): + return _callback_till_result(self, response, InvokeResponse) + else: + return response.result @_dereferenced def sinvoke( @@ -229,6 +279,28 @@ def sinvoke( ).result @_dereferenced + def complete( + self, cbid: str, err: Optional[str], result: Any + ) -> Any: + return self.provider.complete( + CompleteRequest( + cbid=cbid, + err=err, + result=result + ) + ) + + def sync_complete( + self, cbid: str, err: Optional[str], result: Any, response_type: Type[KernelResponse] + ) -> Any: + return self.provider.sync_complete( + CompleteRequest( + cbid=cbid, + err=err, + result=result), + response_type=response_type + ) + def ainvoke( self, obj: Referenceable, method: str, args: Optional[List[Any]] = None ) -> Any: @@ -242,6 +314,8 @@ def ainvoke( args=_make_reference_for_native(self, args), ) ) + if isinstance(promise, Callback): + promise = _callback_till_result(self, promise, BeginResponse) callbacks = self.provider.callbacks(CallbacksRequest()).callbacks while callbacks: diff --git a/packages/jsii-python-runtime/src/jsii/_kernel/providers/base.py b/packages/jsii-python-runtime/src/jsii/_kernel/providers/base.py index 23266a8c9d..749492646b 100644 --- a/packages/jsii-python-runtime/src/jsii/_kernel/providers/base.py +++ b/packages/jsii-python-runtime/src/jsii/_kernel/providers/base.py @@ -1,6 +1,6 @@ import abc -from typing import Optional +from typing import Optional, Union, Type from jsii._kernel.types import ( LoadRequest, @@ -28,6 +28,9 @@ CompleteResponse, StatsRequest, StatsResponse, + Callback, + CompleteRequest, + KernelResponse ) @@ -63,13 +66,21 @@ def sset(self, request: StaticSetRequest) -> SetResponse: ... @abc.abstractmethod - def invoke(self, request: InvokeRequest) -> InvokeResponse: + def invoke(self, request: InvokeRequest) -> Union[InvokeResponse, Callback]: ... @abc.abstractmethod def sinvoke(self, request: StaticInvokeRequest) -> InvokeResponse: ... + @abc.abstractmethod + def complete(self, request: CompleteRequest) -> Union[InvokeResponse, GetResponse]: + ... + + @abc.abstractmethod + def sync_complete(self, request: CompleteRequest, response_type: Type[KernelResponse]) -> Union[InvokeResponse, GetResponse]: + ... + @abc.abstractmethod def delete(self, request: DeleteRequest) -> DeleteResponse: ... diff --git a/packages/jsii-python-runtime/src/jsii/_kernel/providers/process.py b/packages/jsii-python-runtime/src/jsii/_kernel/providers/process.py index 6e2a25fdec..875f7100f4 100644 --- a/packages/jsii-python-runtime/src/jsii/_kernel/providers/process.py +++ b/packages/jsii-python-runtime/src/jsii/_kernel/providers/process.py @@ -52,6 +52,9 @@ CompleteResponse, StatsRequest, StatsResponse, + Callback, + CompleteRequest, + CompleteResponse, ) from jsii.errors import JSIIError, JavaScriptError @@ -79,8 +82,17 @@ class _ErrorRespose: error: str stack: str +@attr.s(auto_attribs=True, frozen=True, slots=True) +class _CallbackResponse: + + callback: Callback -_ProcessResponse = Union[_OkayResponse, _ErrorRespose] +@attr.s(auto_attribs=True, frozen=True, slots=True) +class _CompleteRequest: + + complete: CompleteRequest + +_ProcessResponse = Union[_OkayResponse, _ErrorRespose, _CallbackResponse] # Workaround for mypy#5354 _ProcessResponse_R: Type[Any] if not TYPE_CHECKING: @@ -296,6 +308,8 @@ def send( if isinstance(resp, _OkayResponse): return self._serializer.structure(resp.ok, response_type) + elif isinstance(resp, _CallbackResponse): + return resp.callback else: raise JSIIError(resp.error) from JavaScriptError(resp.stack) @@ -326,7 +340,7 @@ def sget(self, request: StaticGetRequest) -> GetResponse: def sset(self, request: StaticSetRequest) -> SetResponse: return self._process.send(request, SetResponse) - def invoke(self, request: InvokeRequest) -> InvokeResponse: + def invoke(self, request: InvokeRequest) -> Union[InvokeResponse, Callback]: return self._process.send(request, InvokeResponse) def sinvoke(self, request: StaticInvokeRequest) -> InvokeResponse: @@ -347,6 +361,10 @@ def callbacks(self, request: CallbacksRequest) -> CallbacksResponse: def complete(self, request: CompleteRequest) -> CompleteResponse: return self._process.send(request, CompleteResponse) + def sync_complete(self, request: CompleteRequest, response_type: Type[KernelResponse]) -> Union[InvokeResponse, GetResponse]: + resp = self._process.send(_CompleteRequest(complete=request), response_type) + return resp + def stats(self, request: Optional[StatsRequest] = None) -> StatsResponse: if request is None: request = StatsRequest() diff --git a/packages/jsii-python-runtime/src/jsii/_kernel/types.py b/packages/jsii-python-runtime/src/jsii/_kernel/types.py index c08d31c990..bcc00bc663 100644 --- a/packages/jsii-python-runtime/src/jsii/_kernel/types.py +++ b/packages/jsii-python-runtime/src/jsii/_kernel/types.py @@ -236,6 +236,7 @@ class StatsResponse: GetResponse, InvokeResponse, StatsResponse, + Callback, ] diff --git a/packages/jsii-python-runtime/tests/test_compliance.py b/packages/jsii-python-runtime/tests/test_compliance.py index 96bd1b2d71..c55291df0f 100644 --- a/packages/jsii-python-runtime/tests/test_compliance.py +++ b/packages/jsii-python-runtime/tests/test_compliance.py @@ -10,6 +10,7 @@ AbstractClassReturner, Add, AllTypes, + AllTypesEnum, AsyncVirtualMethods, Calculator, ClassWithPrivateConstructorAndAutomaticProperties, @@ -51,9 +52,6 @@ # provided to us by pytest, we are making these tests match the Java Compliance # Tests as closely as possible to make keeping them in sync easier. -# These map distinct reasons for failures, so we an easily find them. -xfail_callbacks = pytest.mark.skip(reason="Implement callback support") - class DerivedFromAllTypes(AllTypes): pass @@ -474,12 +472,12 @@ def test_asyncOverrides_callAsyncMethod(): def test_asyncOverrides_overrideAsyncMethod(): obj = OverrideAsyncMethods() - obj.call_me() == 4452 + assert obj.call_me() == 4452 def test_asyncOverrides_overrideAsyncMethodByParentClass(): obj = OverrideAsyncMethodsByBaseClass() - obj.call_me() == 4452 + assert obj.call_me() == 4452 def test_asyncOverrides_overrideCallsSuper(): @@ -504,7 +502,6 @@ def override_me(self, mult): obj.call_me() -@xfail_callbacks def test_syncOverrides(): obj = SyncOverrides() assert obj.caller_is_method() == 10 * 5 @@ -518,18 +515,14 @@ def test_syncOverrides(): # and from an async method obj.multiplier = 3 - assert obj.caller_is_async == 10 * 5 * 3 - + assert obj.caller_is_async() == 10 * 5 * 3 -@xfail_callbacks def test_propertyOverrides_get_set(): so = SyncOverrides() - assert so.retrieve_value_of_the_property == "I am an override!" + assert so.retrieve_value_of_the_property() == "I am an override!" so.modify_value_of_the_property("New Value") assert so.another_the_property == "New Value" - -@xfail_callbacks def test_propertyOverrides_get_calls_super(): class SuperSyncVirtualMethods(SyncVirtualMethods): @property @@ -545,8 +538,6 @@ def the_property(self, value): assert so.retrieve_value_of_the_property() == "super:initial value" assert so.the_property == "super:initial value" - -@xfail_callbacks def test_propertyOverrides_set_calls_super(): class SuperSyncVirtualMethods(SyncVirtualMethods): @property @@ -555,15 +546,19 @@ def the_property(self): @the_property.setter def the_property(self, value): - super().the_property = f"{value}:by override" + # + # This is the way this was originally coded: + # super().the_property = f"{value}:by override" + # but this causes a problem because of: + # https://bugs.python.org/issue14965 + # so now we have this more convoluted form. + super(self.__class__, self.__class__).the_property.__set__(self, f"{value}:by override") so = SuperSyncVirtualMethods() so.modify_value_of_the_property("New Value") assert so.the_property == "New Value:by override" - -@xfail_callbacks def test_propertyOverrides_get_throws(): class ThrowingSyncVirtualMethods(SyncVirtualMethods): @property @@ -580,7 +575,6 @@ def the_property(self, value): so.retrieve_value_of_the_property() -@xfail_callbacks def test_propertyOverrides_set_throws(): class ThrowingSyncVirtualMethods(SyncVirtualMethods): @property @@ -597,11 +591,9 @@ def the_property(self, value): so.modify_value_of_the_property("Hii") -@pytest.mark.xfail( - reason="Overrides are still not implemented.", strict=True -) def test_propertyOverrides_interfaces(): - class TInterfaceWithProperties(IInterfaceWithProperties): + @jsii.implements(IInterfaceWithProperties) + class TInterfaceWithProperties: x = None @@ -624,11 +616,9 @@ def read_write_string(self, value): assert interact.write_and_read("Hello") == "Hello!?" -@pytest.mark.xfail( - reason="Overrides are still not implemented.", strict=True -) def test_interfaceBuilder(): - class TInterfaceWithProperties(IInterfaceWithProperties): + @jsii.implements(IInterfaceWithProperties) + class TInterfaceWithProperties: x = "READ_WRITE" @@ -649,7 +639,6 @@ def read_write_string(self, value): assert interact.just_read() == "READ_ONLY" assert interact.write_and_read("Hello") == "Hello" -@xfail_callbacks def test_syncOverrides_callsSuper(): obj = SyncOverrides() assert obj.caller_is_property == 10 * 5 @@ -657,7 +646,6 @@ def test_syncOverrides_callsSuper(): assert obj.caller_is_property == 10 * 2 -@pytest.mark.skip def test_fail_syncOverrides_callsDoubleAsync_method(): obj = SyncOverrides() obj.call_async = True @@ -667,7 +655,6 @@ def test_fail_syncOverrides_callsDoubleAsync_method(): obj.caller_is_method() -@pytest.mark.skip def test_fail_syncOverrides_callsDoubleAsync_propertyGetter(): obj = SyncOverrides() obj.call_async = True @@ -687,7 +674,6 @@ def test_fail_syncOverrides_callsDoubleAsync_propertySetter(): obj.caller_is_property = 12 -@xfail_callbacks def test_testInterfaces(): friendly: IFriendly friendlier: IFriendlier @@ -723,7 +709,6 @@ def test_testInterfaces(): assert poly.say_hello(PureNativeFriendlyRandom()) == "oh, I am a native!" -@xfail_callbacks def test_testNativeObjectsWithInterfaces(): # create a pure and native object, not part of the jsii hierarchy, only implements # a jsii interface @@ -899,10 +884,12 @@ def test_eraseUnsetDataValues(): assert not EraseUndefinedHashValues.does_key_exist(opts, "option2") -@xfail_callbacks def test_objectIdDoesNotGetReallocatedWhenTheConstructorPassesThisOut(): class PartiallyInitializedThisConsumerImpl(PartiallyInitializedThisConsumer): - def consume_partially_initialized_this(self): + def consume_partially_initialized_this(self, obj, dt, en): + assert obj is not None + assert isinstance(dt, datetime) + assert en.member == AllTypesEnum.ThisIsGreat.value return "OK" reflector = PartiallyInitializedThisConsumerImpl()