[sdk/python] Unmarshal output values in component providers (#8212)

This commit is contained in:
Ian Wahbe 2021-11-15 10:12:12 -08:00 committed by GitHub
parent a5f72ddbeb
commit d3b2dedd1d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 552 additions and 23 deletions

View file

@ -11,6 +11,9 @@
- [sdk/dotnet] - Marshal output values.
[#8316](https://github.com/pulumi/pulumi/pull/8316)
- [sdk/python] - Unmarshal output values in component provider.
[#8212](https://github.com/pulumi/pulumi/pull/8212)
### Bug Fixes
- [engine] - Compute dependents correctly during targeted deletes.

View file

@ -25,6 +25,7 @@ import sys
import grpc
import grpc.aio
from google.protobuf import struct_pb2
from pulumi.provider.provider import Provider, CallResult, ConstructResult
from pulumi.resource import ProviderResource, Resource, DependencyResource, DependencyProviderResource, \
_parse_resource_reference
@ -81,8 +82,7 @@ class ProviderServicer(ResourceProviderServicer):
preview=request.dryRun)
pulumi.runtime.config.set_all_config(dict(request.config), request.configSecretKeys)
inputs = await self._construct_inputs(request)
inputs = await self._construct_inputs(request.inputs, request.inputDependencies)
result = self.provider.construct(name=request.name,
resource_type=request.type,
@ -102,28 +102,32 @@ class ProviderServicer(ResourceProviderServicer):
return response
@staticmethod
async def _construct_inputs(request: proto.ConstructRequest) -> Dict[str, pulumi.Input[Any]]:
async def _construct_inputs(inputs: struct_pb2.Struct, input_dependencies: Any) -> Dict[str, pulumi.Input[Any]]:
def deps(key: str) -> Set[str]:
return set(urn for urn in
request.inputDependencies.get(
input_dependencies.get(
key,
proto.ConstructRequest.PropertyDependencies()
).urns)
return {
k: await ProviderServicer._create_output(the_input, deps=deps(k))
k: await ProviderServicer._select_value(the_input, deps=deps(k))
for k, the_input in
rpc.deserialize_properties(request.inputs, keep_unknowns=True).items()
rpc.deserialize_properties(inputs, keep_unknowns=True).items()
}
@staticmethod
async def _create_output(the_input: Any, deps: Set[str]) -> Any:
async def _select_value(the_input: Any, deps: Set[str]) -> Any:
is_secret = rpc.is_rpc_secret(the_input)
# If it's a resource reference or a prompt value, return it directly without wrapping
# it as an output.
if await _is_resource_reference(the_input, deps) or (not is_secret and len(deps) == 0):
# If the input isn't a secret and either doesn't have any dependencies, already contains Outputs (from
# deserialized output values), or is a resource reference, then return it directly without wrapping it
# as an output.
if not is_secret and (
len(deps) == 0 or
_contains_outputs(the_input) or
await _is_resource_reference(the_input, deps)):
return the_input
# Otherwise, wrap it as an output so we can handle secrets
@ -220,7 +224,7 @@ class ProviderServicer(ResourceProviderServicer):
).urns)
return {
k: await ProviderServicer._create_output(the_input, deps=deps(k))
k: await ProviderServicer._select_value(the_input, deps=deps(k))
for k, the_input in
# We need to keep_internal, to keep the `__self__` that would normally be filtered because
# it starts with "__".
@ -249,7 +253,7 @@ class ProviderServicer(ResourceProviderServicer):
return proto.CallResponse(**resp)
async def Configure(self, request, context) -> proto.ConfigureResponse: # pylint: disable=invalid-overridden-method
return proto.ConfigureResponse(acceptSecrets=True, acceptResources=True)
return proto.ConfigureResponse(acceptSecrets=True, acceptResources=True, acceptOutputs=True)
async def GetPluginInfo(self, request, context) -> proto.PluginInfo: # pylint: disable=invalid-overridden-method
return proto.PluginInfo(version=self.provider.version)
@ -331,6 +335,25 @@ async def _is_resource_reference(the_input: Any, deps: Set[str]) -> bool:
and next(iter(deps)) == await cast(Resource, the_input).urn.future())
def _contains_outputs(the_input: Any) -> bool:
"""
Returns true if the input contains Outputs (deeply).
"""
if known_types.is_output(the_input):
return True
if isinstance(the_input, list):
for e in the_input:
if _contains_outputs(e):
return True
elif isinstance(the_input, dict):
for k in the_input:
if _contains_outputs(the_input[k]):
return True
return False
def _create_provider_resource(ref: str) -> ProviderResource:
"""
Rehydrate the provider reference into a registered ProviderResource,

View file

@ -1,4 +1,4 @@
# Copyright 2016-2021, Pulumi Corporation.
# Copyright 2016-2018, Pulumi Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.

View file

@ -114,12 +114,13 @@ class ConfigureResponse:
def __init__(self,
acceptSecrets: bool=False,
supportsPreview: bool=False,
acceptResources: bool=False) -> None: ...
acceptResources: bool=False,
acceptOutputs: bool=False) -> None: ...
acceptSecrets: bool
supportsPreview: bool
acceptResources: bool
acceptOutputs: bool
class GetSchemaRequest:
version: int

View file

@ -12,24 +12,42 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Dict, Any
import functools
from typing import Dict, Any, Optional, Tuple, List, Set, Callable, Awaitable
from semver import VersionInfo as Version
import os
import pytest
from pulumi.runtime.settings import Settings, configure
from pulumi.provider.server import ProviderServicer
from pulumi.runtime import proto, rpc
from pulumi.runtime import proto, rpc, rpc_manager, ResourceModule, Mocks
from pulumi.resource import CustomResource, ResourceOptions
from pulumi.runtime.proto.provider_pb2 import ConstructRequest
import google.protobuf.struct_pb2 as struct_pb2
from google.protobuf import struct_pb2
import pulumi.output
def pulumi_test(coro):
wrapped = pulumi.runtime.test(coro)
@functools.wraps(wrapped)
def wrapper(*args, **kwargs):
configure(Settings())
rpc._RESOURCE_PACKAGES.clear()
rpc._RESOURCE_MODULES.clear()
rpc_manager.RPC_MANAGER = rpc_manager.RPCManager()
wrapped(*args, **kwargs)
return wrapper
@pytest.mark.asyncio
async def test_construct_inputs_parses_request():
value = 'foobar'
inputs = _as_struct({'echo': value})
req = ConstructRequest(inputs=inputs)
inputs = await ProviderServicer._construct_inputs(req)
inputs = await ProviderServicer._construct_inputs(req.inputs, req.inputDependencies) # pylint: disable=no-member
assert len(inputs) == 1
assert inputs['echo'] == value
@ -39,7 +57,7 @@ async def test_construct_inputs_preserves_unknowns():
unknown = '04da6b54-80e4-46f7-96ec-b56ff0331ba9'
inputs = _as_struct({'echo': unknown})
req = ConstructRequest(inputs=inputs)
inputs = await ProviderServicer._construct_inputs(req)
inputs = await ProviderServicer._construct_inputs(req.inputs, req.inputDependencies) # pylint: disable=no-member
assert len(inputs) == 1
assert isinstance(inputs['echo'], pulumi.output.Unknown)
@ -48,3 +66,487 @@ def _as_struct(key_values: Dict[str, Any]) -> struct_pb2.Struct:
the_struct = struct_pb2.Struct()
the_struct.update(key_values) # pylint: disable=no-member
return the_struct
class MockResource(CustomResource):
def __init__(self, name: str, opts: Optional[ResourceOptions] = None):
CustomResource.__init__(self, "test:index:MockResource", name, opts=opts)
class MockInputDependencies:
""" A mock for ConstructRequest.inputDependencies
We need only support a `get() -> T where T.urns: List[str]` operation.
"""
def __init__(self, urns: Optional[List[str]]):
self.urns = urns if urns else []
def get(self, *args):
#pylint: disable=unused-argument
return self
class TestModule(ResourceModule):
def construct(self, name: str, typ: str, urn: str):
if typ == "test:index:MockResource":
return MockResource(name, opts=ResourceOptions(urn=urn))
raise Exception(f"unknown resource type {typ}")
def version(self) -> Optional[Version]:
return None
class TestMocks(Mocks):
def call(self, args: pulumi.runtime.MockCallArgs) -> Any:
raise Exception(f"unknown function {args.token}")
def new_resource(self, args: pulumi.runtime.MockResourceArgs) -> Tuple[Optional[str], dict]:
return args.name+"_id", args.inputs
def assert_output_equal(value: Any,
known: bool, secret: bool,
deps: Optional[List[str]] = None):
async def check(actual: Any):
assert isinstance(actual, pulumi.Output)
if callable(value):
res = value(await actual.future())
if isinstance(res, Awaitable):
await res
else:
assert (await actual.future()) == value
assert known == await actual.is_known()
assert secret == await actual.is_secret()
actual_deps: Set[Optional[str]] = set()
resources = await actual.resources()
for r in resources:
urn = await r.urn.future()
actual_deps.add(urn)
assert actual_deps == set(deps if deps else [])
return True
return check
def create_secret(value: Any):
return {rpc._special_sig_key: rpc._special_secret_sig, "value": value}
def create_resource_ref(urn: str, id_: Optional[str]):
ref = {rpc._special_sig_key: rpc._special_resource_sig, "urn": urn}
if id_ is not None:
ref["id"] = id_
return ref
def create_output_value(value: Optional[Any] = None,
secret: Optional[bool] = None,
dependencies: Optional[List[str]] = None):
val: Dict[str, Any] = {rpc._special_sig_key: rpc._special_output_value_sig}
if value is not None:
val["value"] = value
if secret is not None:
val["secret"] = secret
if dependencies is not None:
val["dependencies"] = dependencies
return val
test_urn = "urn:pulumi:stack::project::test:index:MockResource::name"
test_id = "name_id"
class UnmarshalOutputTestCase:
def __init__(self,
name: str,
input_: Any,
deps: Optional[List[str]] = None,
expected: Optional[Any] = None,
assert_: Optional[Callable[[Any], Awaitable]] = None):
self.name = name
self.input_ = input_
self.deps = deps
self.expected = expected
self.assert_ = assert_
async def run(self):
pulumi.runtime.set_mocks(TestMocks(), "project", "stack", True)
pulumi.runtime.register_resource_module("test", "index", TestModule())
# This registers the resource purely for the purpose of the test.
pulumi.runtime.settings.get_monitor().resources[test_urn] = \
pulumi.runtime.mocks.MockMonitor.ResourceRegistration(test_urn, test_id, dict())
inputs = { "value": self.input_ }
input_struct = _as_struct(inputs)
req = ConstructRequest(inputs=input_struct)
result = await ProviderServicer._construct_inputs(
req.inputs, MockInputDependencies(self.deps)) # pylint: disable=no-member
actual = result["value"]
if self.assert_:
await self.assert_(actual)
else:
assert actual == self.expected
class Assert:
"""Describes a series of asserts to be performed.
Each assert can be:
- An async value to be awaited and asserted.
assert await val
- A sync function to be called and asserted on. This will be called on the
same set of arguments that the class was called on.
assert fn(actual)
- A plain value to be asserted on.
assert val
"""
def __init__(self, *asserts):
self.asserts = asserts
async def __call__(self, *args, **kargs):
for assert_ in self.asserts:
assert await Assert.__eval(assert_, *args, **kargs)
@staticmethod
async def __eval(a, *args, **kargs) -> Any:
if isinstance(a, Awaitable):
return await a
elif isinstance(a, Callable):
a_res = a(*args, **kargs)
return await Assert.__eval(a_res, *args, **kargs)
return a
@staticmethod
def async_equal(a, b):
"""Asserts that two values are equal when evaluated with async and
given the args that `Asserts` were called on.
"""
async def check(*args, **kargs):
a_res = await Assert.__eval(a, *args, **kargs)
b_res = await Assert.__eval(b, *args, **kargs)
assert a_res == b_res
return True
return check
async def array_nested_resource_ref(actual):
async def helper(v: Any):
assert isinstance(v, list)
assert isinstance(v[0], MockResource)
assert await v[0].urn.future() == test_urn
assert await v[0].id.future() == test_id
await assert_output_equal(helper, True, False, [test_urn])(actual)
async def object_nested_resource_ref(actual):
async def helper(v: Any):
assert isinstance(v["foo"], MockResource)
assert await v["foo"].urn.future() == test_urn
assert await v["foo"].id.future() == test_id
await assert_output_equal(helper, True, False, [test_urn])(actual)
async def object_nested_resource_ref_and_secret(actual):
async def helper(v: Any):
assert isinstance(v["foo"], MockResource)
assert await v["foo"].urn.future() == test_urn
assert await v["foo"].id.future() == test_id
assert v["bar"] == "ssh"
await assert_output_equal(helper, True, True, [test_urn])(actual)
deserialization_tests = [
UnmarshalOutputTestCase(
name="unknown",
input_=rpc.UNKNOWN,
deps=["fakeURN"],
assert_=assert_output_equal(None, False, False, ["fakeURN"]),
),
UnmarshalOutputTestCase(
name="array nested unknown",
input_=[rpc.UNKNOWN],
deps=["fakeURN"],
assert_=assert_output_equal(None, False, False, ["fakeURN"]),
),
UnmarshalOutputTestCase(
name="object nested unknown",
input_={"foo": rpc.UNKNOWN},
deps=["fakeURN"],
assert_=assert_output_equal(None, False, False, ["fakeURN"]),
),
UnmarshalOutputTestCase(
name="unknown output value",
input_=create_output_value(None, False, ["fakeURN"]),
deps=["fakeURN"],
assert_=assert_output_equal(None, False, False, ["fakeURN"]),
),
UnmarshalOutputTestCase(
name="unknown output value (no deps)",
input_=create_output_value(),
assert_=assert_output_equal(None, False, False),
),
UnmarshalOutputTestCase(
name="array nested unknown output value",
input_=[create_output_value(None, False, ["fakeURN"])],
deps=["fakeURN"],
assert_=Assert(
lambda actual: isinstance(actual, list),
lambda actual: assert_output_equal(None, False, False, ["fakeURN"])(actual[0])
),
),
UnmarshalOutputTestCase(
name="array nested unknown output value (no deps)",
input_=[create_output_value(None, False, ["fakeURN"])],
assert_=Assert(
lambda actual: isinstance(actual, list),
lambda actual: assert_output_equal(None, False, False, ["fakeURN"])(actual[0])
),
),
UnmarshalOutputTestCase(
name="object nested unknown output value",
input_= { "foo": create_output_value(None, False, ["fakeURN"]) },
deps=["fakeURN"],
assert_=Assert(
lambda actual: not isinstance(actual, pulumi.Output),
lambda actual: assert_output_equal(None, False, False, ["fakeURN"])(actual["foo"]),
),
),
UnmarshalOutputTestCase(
name="object nested unknown output value (no deps)",
input_= { "foo": create_output_value(None, False, ["fakeURN"]) },
assert_=Assert(
lambda actual: not isinstance(actual, pulumi.Output),
lambda actual: assert_output_equal(None, False, False, ["fakeURN"])(actual["foo"]),
),
),
UnmarshalOutputTestCase(
name="string value (no deps)",
input_="hi",
expected="hi",
),
UnmarshalOutputTestCase(
name="array nested string value (no deps)",
input_=["hi"],
expected=["hi"],
),
UnmarshalOutputTestCase(
name="object nested string value (no deps)",
input_= { "foo": "hi" },
expected= { "foo": "hi" },
),
UnmarshalOutputTestCase(
name="string output value",
input_=create_output_value("hi", False, ["fakeURN"]),
deps=["fakeURN"],
assert_=assert_output_equal("hi", True, False, ["fakeURN"]),
),
UnmarshalOutputTestCase(
name="string output value (no deps)",
input_=create_output_value("hi"),
assert_=assert_output_equal("hi", True, False),
),
UnmarshalOutputTestCase(
name="array nested string output value",
input_=[create_output_value("hi", False, ["fakeURN"])],
deps=["fakeURN"],
assert_=Assert(
lambda actual: isinstance(actual, list),
lambda actual: assert_output_equal("hi", True, False, ["fakeURN"])(actual[0]),
),
),
UnmarshalOutputTestCase(
name="array nested string output value (no deps)",
input_=[create_output_value("hi", False, ["fakeURN"])],
assert_=Assert(
lambda actual: isinstance(actual, list),
lambda actual: assert_output_equal("hi", True, False, ["fakeURN"])(actual[0]),
),
),
UnmarshalOutputTestCase(
name="object nested string output value",
input_={ "foo": create_output_value("hi", False, ["fakeURN"])},
deps=["fakeURN"],
assert_=Assert(
lambda actual: not isinstance(actual, pulumi.Output),
lambda actual: assert_output_equal("hi", True, False, ["fakeURN"])(actual["foo"])
),
),
UnmarshalOutputTestCase(
name="object nested string output value (no deps)",
input_={ "foo": create_output_value("hi", False, ["fakeURN"])},
assert_=Assert(
lambda actual: not isinstance(actual, pulumi.Output),
lambda actual: assert_output_equal("hi", True, False, ["fakeURN"])(actual["foo"])
),
),
UnmarshalOutputTestCase(
name="string secrets (no deps)",
input_=create_secret("shh"),
assert_=assert_output_equal("shh", True, True),
),
UnmarshalOutputTestCase(
name="array nested string secrets (no deps)",
input_=[create_secret("shh")],
assert_=assert_output_equal(["shh"], True, True),
),
UnmarshalOutputTestCase(
name="object nested string secrets (no deps)",
input_={ "foo": create_secret("shh")},
assert_=assert_output_equal({"foo": "shh"}, True, True),
),
UnmarshalOutputTestCase(
name="string secret output value (no deps)",
input_=create_output_value("shh", True),
assert_=assert_output_equal("shh", True, True),
),
UnmarshalOutputTestCase(
name="array nested string secret output value (no deps)",
input_=[create_output_value("shh", True)],
assert_=Assert(
lambda actual: isinstance(actual, list),
lambda actual: assert_output_equal("shh", True, True)(actual[0]),
),
),
UnmarshalOutputTestCase(
name="object nested string secret output value (no deps)",
input_={"foo": create_output_value("shh", True)},
assert_=Assert(
lambda actual: not isinstance(actual, pulumi.Output),
lambda actual: assert_output_equal("shh", True, True)(actual["foo"]),
),
),
UnmarshalOutputTestCase(
name="string secret output value",
input_=create_output_value("shh", True, ["fakeURN1", "fakeURN2"]),
deps=["fakeURN1", "fakeURN2"],
assert_=assert_output_equal("shh", True, True, ["fakeURN1", "fakeURN2"]),
),
UnmarshalOutputTestCase(
name="string secret output value (no deps)",
input_=create_output_value("shh", True, ["fakeURN1", "fakeURN2"]),
assert_=assert_output_equal("shh", True, True, ["fakeURN1", "fakeURN2"]),
),
UnmarshalOutputTestCase(
name="array nested string secret output value",
input_=[create_output_value("shh", True, ["fakeURN1", "fakeURN2"])],
deps=["fakeURN1", "fakeURN2"],
assert_=Assert(
lambda actual: isinstance(actual, list),
lambda actual: assert_output_equal("shh", True, True, ["fakeURN1", "fakeURN2"])(actual[0]),
),
),
UnmarshalOutputTestCase(
name="array nested string secret output value (no deps)",
input_=[create_output_value("shh", True, ["fakeURN1", "fakeURN2"])],
assert_=Assert(
lambda actual: isinstance(actual, list),
lambda actual: assert_output_equal("shh", True, True, ["fakeURN1", "fakeURN2"])(actual[0]),
),
),
UnmarshalOutputTestCase(
name="object nested string secret output value",
input_={ "foo": create_output_value("shh", True, ["fakeURN1", "fakeURN2"])},
deps=["fakeURN1", "fakeURN2"],
assert_=Assert(
lambda actual: not isinstance(actual, pulumi.Output),
lambda actual: assert_output_equal("shh", True, True, ["fakeURN1", "fakeURN2"])(actual["foo"]),
),
),
UnmarshalOutputTestCase(
name="object nested string secret output value (no deps)",
input_={ "foo": create_output_value("shh", True, ["fakeURN1", "fakeURN2"])},
assert_=Assert(
lambda actual: not isinstance(actual, pulumi.Output),
lambda actual: assert_output_equal("shh", True, True, ["fakeURN1", "fakeURN2"])(actual["foo"]),
),
),
UnmarshalOutputTestCase(
name="resource ref",
input_=create_resource_ref(test_urn, test_id),
deps=[test_urn],
assert_=Assert(
lambda actual: isinstance(actual, MockResource),
Assert.async_equal(lambda actual: actual.urn.future(), test_urn),
Assert.async_equal(lambda actual: actual.id.future(), test_id),
),
),
UnmarshalOutputTestCase(
name="resource ref (no deps)",
input_=create_resource_ref(test_urn, test_id),
assert_=Assert(
lambda actual: isinstance(actual, MockResource),
Assert.async_equal(lambda actual: actual.urn.future(), test_urn),
Assert.async_equal(lambda actual: actual.id.future(), test_id),
),
),
UnmarshalOutputTestCase(
name="array nested resource ref",
input_=[create_resource_ref(test_urn, test_id)],
deps=[test_urn],
assert_=array_nested_resource_ref
),
UnmarshalOutputTestCase(
name="array nested resource ref (no deps)",
input_=[create_resource_ref(test_urn, test_id)],
assert_=Assert(
lambda actual: isinstance(actual, list),
lambda actual: isinstance(actual[0], MockResource),
Assert.async_equal(lambda actual: actual[0].urn.future(), test_urn),
Assert.async_equal(lambda actual: actual[0].id.future(), test_id),
),
),
UnmarshalOutputTestCase(
name="object nested resource ref",
input_={ "foo": create_resource_ref(test_urn, test_id) },
deps=[test_urn],
assert_=object_nested_resource_ref
),
UnmarshalOutputTestCase(
name="object nested resource ref (no deps)",
input_={ "foo": create_resource_ref(test_urn, test_id) },
assert_=Assert(
lambda actual: isinstance(actual["foo"], MockResource),
Assert.async_equal(lambda actual: actual["foo"].urn.future(), test_urn),
Assert.async_equal(lambda actual: actual["foo"].id.future(), test_id),
),
),
UnmarshalOutputTestCase(
name="object nested resource ref and secret",
input_={
"foo": create_resource_ref(test_urn, test_id),
"bar": create_secret("ssh"),
},
deps=[test_urn],
assert_=object_nested_resource_ref_and_secret
),
UnmarshalOutputTestCase(
name="object nested resource ref and secret output value",
input_={
"foo": create_resource_ref(test_urn, test_id),
"bar": create_output_value("shh", True),
},
deps=[test_urn],
assert_=Assert(
lambda actual: not isinstance(actual, pulumi.Output),
lambda actual: isinstance(actual["foo"], MockResource),
Assert.async_equal(lambda actual: actual["foo"].urn.future(), test_urn),
Assert.async_equal(lambda actual: actual["foo"].id.future(), test_id),
lambda actual: assert_output_equal("shh", True, True)(actual["bar"]),
),
),
UnmarshalOutputTestCase(
name="object nested resource ref and secret output value (no deps)",
input_={
"foo": create_resource_ref(test_urn, test_id),
"bar": create_output_value("shh", True),
},
assert_=Assert(
lambda actual: not isinstance(actual, pulumi.Output),
lambda actual: isinstance(actual["foo"], MockResource),
Assert.async_equal(lambda actual: actual["foo"].urn.future(), test_urn),
Assert.async_equal(lambda actual: actual["foo"].id.future(), test_id),
lambda actual: assert_output_equal("shh", True, True)(actual["bar"]),
),
),
]
@pytest.mark.parametrize(
"testcase", deserialization_tests, ids=list(map(lambda x: x.name, deserialization_tests)))
@pulumi_test
async def test_deserialize_correctly(testcase):
await testcase.run()

View file

@ -179,7 +179,7 @@ class NextSerializationTests(unittest.TestCase):
self.assertEqual(id, prop["id"])
res = rpc.deserialize_properties(prop)
self.assertTrue(isinstance(res, MyCustomResource))
self.assertIsInstance(res, MyCustomResource)
rpc._RESOURCE_MODULES.clear()
res = rpc.deserialize_properties(prop)
@ -209,7 +209,7 @@ class NextSerializationTests(unittest.TestCase):
self.assertEqual(id, prop["id"])
res = rpc.deserialize_properties(prop)
self.assertTrue(isinstance(res, MyCustomResource))
self.assertIsInstance(res, MyCustomResource)
rpc._RESOURCE_MODULES.clear()
res = rpc.deserialize_properties(prop)