Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Annotated types in plugins #8665

Open
wants to merge 38 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
869603d
Add support for native union types in plugins
arnaudsjs Jan 17, 2025
15129ab
Add changelog entry
arnaudsjs Jan 17, 2025
e2c4c32
Fix typing
arnaudsjs Jan 17, 2025
719fbb3
Fix typing
arnaudsjs Jan 17, 2025
001f50c
Small improvements
arnaudsjs Jan 17, 2025
c1c7df8
Use callable from abc
arnaudsjs Jan 17, 2025
53a0a5c
Improve error message
arnaudsjs Jan 17, 2025
505c41e
Fix formatting
arnaudsjs Jan 17, 2025
fdeea80
Initial commit
jptrindade Jan 20, 2025
23cd1e7
small adjustments
jptrindade Jan 21, 2025
2bc3eaa
fixed mypy
jptrindade Jan 21, 2025
8c30ca3
test with type alias
jptrindade Jan 21, 2025
bdf64cb
Update mypy-baseline.txt
jptrindade Jan 21, 2025
d1f7288
fix mypy
jptrindade Jan 21, 2025
5ce1296
revert mypy
jptrindade Jan 21, 2025
3bbe231
renamed exception
sanderr Jan 22, 2025
ec784bf
dedicated plugin type exception
sanderr Jan 22, 2025
c16d8b4
added test scenario
sanderr Jan 22, 2025
dac146d
import fix
sanderr Jan 22, 2025
f64c3a9
addressed comments
jptrindade Jan 22, 2025
db3c27e
added to_dsl_type_simple
jptrindade Jan 22, 2025
378ca58
fix mypy
jptrindade Jan 22, 2025
0127969
some test fixes
sanderr Jan 22, 2025
092261b
test fixes
sanderr Jan 22, 2025
6a6d6ec
pep8
sanderr Jan 22, 2025
28e62aa
rejection test first try
jptrindade Jan 22, 2025
69f3697
renamed change entry
sanderr Jan 22, 2025
1fdd946
include iso7
sanderr Jan 22, 2025
e57db25
Merge branch 'refs/heads/issue/8574-add-support-union-types' into iss…
jptrindade Jan 23, 2025
b07e091
merge and refactor
jptrindade Jan 23, 2025
2a1a57d
mypy
jptrindade Jan 23, 2025
3484f4a
Merge remote-tracking branch 'refs/remotes/origin/master' into issue/…
jptrindade Jan 23, 2025
0b21043
Revert "merge and refactor"
jptrindade Jan 23, 2025
001456a
Merge branch 'refs/heads/master' into issue/8573-add-support-for-anno…
jptrindade Jan 27, 2025
9e5679e
attempted merge
jptrindade Jan 27, 2025
2fc91a9
fixed mypy
jptrindade Jan 27, 2025
48a1a97
fixed test_plugin_types.py
jptrindade Jan 27, 2025
a5135da
WIP, still problems with pylance
jptrindade Jan 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
description: Add support for annotated types to plugins.
issue-nr: 8573
change-type: minor
destination-branches: [master, iso8]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably want to include this in iso7 as well. Discussion still pending, see Slack.

42 changes: 42 additions & 0 deletions src/inmanta/plugin_typing.py
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copied this file from your PR @sanderr . I'm inclined to leave it here. Regarding the comment on how to handle std::Entity I also think object might be our best option

Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""
Copyright 2025 Inmanta

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Contact: [email protected]
"""

import typing
from dataclasses import dataclass

# TODO: move this module to inmanta.plugins.typing? Probably not because that would import the whole `plugins` namespace?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd put it in plugins or plugins.typing. I don't recall why I was hesitant to put it there tbh. It's for annotating plugins, users would need to import it either way.

I think I'd prefer plugins.typing. @wouterdb may have an opinion.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

plugins.typing would have the added difficulty that it's less trivial to import partially qualified.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only thing awkward I see is the import confusion between typing and inmanta.plugins.typing in plugins.py.
I fixed this by importing everything we use instead of just doing import typing

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that is a proper solution. qualified / unqualified should be driven by context to make sure that everything is unambiguous. Sure, things like Any and Sequence and such (though the latter should really come from collections.abc) are sufficiently wide-spread that they are unmabiguous in their own right. But get_args etc, those really require context. Imo we must use them qualified (qualifiedly?).

As to the awkwardness, I agree on that front. What I meant to say is that from inmanta.plugins import typing will never be a good approach. That leaves import inmanta.plugins.typing, which is very verbose, and import inmanta.plugins.typing; from inmanta import plugins (allowing partially qualified plugins.typing.InmantaType), which is a bit exotic, but we do it in a few places, and I don't think there's anything wrong with it myself.

So the question becomes, do we consider this import awkwardness sufficient reason to just add it to the heap of stuff in inmanta.plugins? I honestly don't know.



@dataclass(frozen=True)
class InmantaType:
"""
Declaration of an inmanta type for use with typing.Annotated.
When a plugin type is declared as typing.Annotated with an `InmantaType` as annotation, the Python type is completely
ignored for type validation and conversion to and from the DSL. Instead the string provided to the `InmantaType` is
evaluated as a DSL type, extended with "any".
For maximum static type coverage, it is recommended to use these only when absolutely necessary, and to use them as deeply
in the type as possible, e.g. prefer `Sequence[Annotated[MyEntity, InmantaType("std::Entity")]]` over
`Annotated[Sequence[Entity], InmantaType("std::Entity[]")]`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
`Annotated[Sequence[Entity], InmantaType("std::Entity[]")]`.
`Annotated[Sequence[MyEntity], InmantaType("std::Entity[]")]`.

"""

dsl_type: str


# TODO: how to do Entity? "object" is appropriate but raises too many errors for practical use. Any is Any
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll have to run this by Bart. I prefer object, as you propose, but I tend to be more strict than others.

Entity: typing.TypeAlias = typing.Annotated[object, InmantaType("std::Entity")]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a docstring

string: typing.TypeAlias = typing.Annotated[str, InmantaType("string")]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is useful anymore, considering that we support the native type now.

52 changes: 29 additions & 23 deletions src/inmanta/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
import typing_inspect

import inmanta.ast.type as inmanta_type
from inmanta import const, protocol, util
from inmanta.ast import ( # noqa: F401 Plugin exception is part of the stable api
from inmanta import const, plugin_typing, protocol, util
from inmanta.ast import ( # noqa: F401 Plugin e
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like an accidental change to me.

LocatableString,
Location,
Namespace,
Expand Down Expand Up @@ -245,11 +245,18 @@ def __eq__(self, other: object) -> bool:
}


def to_dsl_type(python_type: type[object]) -> inmanta_type.Type:
def parse_dsl_type(dsl_type: str, location: Range, resolver: Namespace) -> inmanta_type.Type:
locatable_type: LocatableString = LocatableString(dsl_type, location, 0, resolver)
return inmanta_type.resolve_type(locatable_type, resolver)


def to_dsl_type(python_type: type[object], location: Range, resolver: Namespace) -> inmanta_type.Type:
"""
Convert a python type annotation to an Inmanta DSL type annotation.

:param python_type: The evaluated python type as provided in the Python type annotation.
:param location: The location of this evaluation on the model
:param resolver: The namespace that can be used to resolve the type annotation of this argument.
"""
# Any to any
if python_type is typing.Any:
Expand All @@ -268,7 +275,7 @@ def to_dsl_type(python_type: type[object]) -> inmanta_type.Type:
# Probably not possible
return Null()
if len(other_types) == 1:
return inmanta_type.NullableType(to_dsl_type(other_types[0]))
return inmanta_type.NullableType(to_dsl_type(other_types[0], location, resolver))
# TODO: optional unions
return inmanta_type.Type()
else:
Expand Down Expand Up @@ -296,7 +303,7 @@ def to_dsl_type(python_type: type[object]) -> inmanta_type.Type:
if len(args) == 1:
return inmanta_type.TypedDict(inmanta_type.Type())

return inmanta_type.TypedDict(to_dsl_type(args[1]))
return inmanta_type.TypedDict(to_dsl_type(args[1], location, resolver))
else:
raise TypingException(None, f"invalid type {python_type}, dictionary types should be Mapping or dict")

Expand All @@ -306,27 +313,26 @@ def to_dsl_type(python_type: type[object]) -> inmanta_type.Type:
args = typing.get_args(python_type)
if not args:
return inmanta_type.List()
return inmanta_type.TypedList(to_dsl_type(args[0]))
return inmanta_type.TypedList(to_dsl_type(args[0], location, resolver))
else:
raise TypingException(None, f"invalid type {python_type}, list types should be Sequence or list")

# Set
if issubclass(origin, collections.abc.Set):
raise TypingException(None, f"invalid type {python_type}, set is not supported on the plugin boundary")

# TODO annotated types
# if typing.get_origin(t) is typing.Annotated:
# args: Sequence[object] = typing.get_args(python_type)
# inmanta_types: Sequence[plugin_typing.InmantaType] =
# [arg if isinstance(arg, plugin_typing.InmantaType) for arg in args]
# if inmanta_types:
# if len(inmanta_types) > 1:
# # TODO
# raise Exception()
# # TODO
# return parse_dsl_type(inmanta_types[0].dsl_type)
# # the annotation doesn't concern us => use base type
# return to_dsl_type(args[0])
if typing.get_origin(python_type) is typing.Annotated:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd indent this one level under the is_generic body. I don't think it's strictly necessary, and there's something to say for not nesting deeper than required, but I think that in this case consistency is more important.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And then you can use origin instead of get_origin

annotated_args = typing.get_args(python_type)
inmanta_types: Sequence[plugin_typing.InmantaType] = [
arg for arg in annotated_args if isinstance(arg, plugin_typing.InmantaType)
]
if inmanta_types:
if len(inmanta_types) > 1:
raise TypingException(None, f"invalid type {python_type}, only one InmantaType annotation is supported")
return parse_dsl_type(inmanta_types[0].dsl_type, location, resolver)
# the annotation doesn't concern us => use base type
return to_dsl_type(annotated_args[0], location, resolver)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sanderr What happens if we can't find the InmantaType? Do we default to Any?

If it's not there, it simply means that the Annotated has nothing to do with us. e.g. typing.Annotated[str, ContextForSomeMypyPlugin()]. So as far as our type-checking is concerned, it's just str. That's why we fall back to the base type here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that is the case.
If I have Annotated[str, "something"] it will default to string.
But if I have Annotated[str, plugin_typing.InmantaType("unexistent-entity")] It should fail on parse_dsl_type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could either ignore the annotation and just treat it as str (even though it was clearly tagged as something that concerns us) or return Any (I think I prefer the other option).

But either way, if this happens we should log a warning

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I misunderstood the question. I'd make sure to treat it the same as any other DSL-type plugin annotation. I would expect that means to raise an exception. i.e. what if you do def my_plugin(arg: "not-a-valid-type") -> None: ...?


if python_type in python_to_model:
return python_to_model[python_type]

Expand Down Expand Up @@ -386,19 +392,19 @@ def resolve_type(self, plugin: "Plugin", resolver: Namespace) -> inmanta_type.Ty
self._resolved_type = PLUGIN_TYPES[self.type_expression]
return self._resolved_type

plugin_line: Range = Range(plugin.location.file, plugin.location.lnr, 1, plugin.location.lnr + 1, 1)
if not isinstance(self.type_expression, str):
if isinstance(self.type_expression, type) or typing.get_origin(self.type_expression) is not None:
self._resolved_type = to_dsl_type(self.type_expression)
self._resolved_type = to_dsl_type(self.type_expression, plugin_line, resolver)
else:
raise RuntimeException(
stmt=None,
msg="Bad annotation in plugin %s for %s, expected str or python type but got %s (%s)"
% (plugin.get_full_name(), self.VALUE_NAME, type(self.type_expression).__name__, self.type_expression),
)
else:
plugin_line: Range = Range(plugin.location.file, plugin.location.lnr, 1, plugin.location.lnr + 1, 1)
locatable_type: LocatableString = LocatableString(self.type_expression, plugin_line, 0, resolver)
self._resolved_type = inmanta_type.resolve_type(locatable_type, resolver)
self._resolved_type = parse_dsl_type(self.type_expression, plugin_line, resolver)

return self._resolved_type

def validate(self, value: object) -> bool:
Expand Down
79 changes: 53 additions & 26 deletions tests/compiler/test_plugin_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,44 +17,71 @@
"""

import collections.abc
from typing import Any, Mapping, Sequence, Union
from typing import Annotated, Any, Mapping, Sequence, Union

import pytest

import inmanta.ast.type as inmanta_type
from inmanta.ast import RuntimeException
from inmanta import plugin_typing
from inmanta.ast import Namespace, Range, RuntimeException
from inmanta.plugins import Null, to_dsl_type

# from inmanta.ast.entity import Entity
# from inmanta.ast.type import TYPES

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Testing artifact?


def test_conversion(caplog):
"""
Test behaviour of to_dsl_type function.
"""
assert inmanta_type.Integer() == to_dsl_type(int)
assert inmanta_type.Float() == to_dsl_type(float)
assert inmanta_type.NullableType(inmanta_type.Float()) == to_dsl_type(float | None)
assert inmanta_type.List() == to_dsl_type(list)
assert inmanta_type.TypedList(inmanta_type.String()) == to_dsl_type(list[str])
assert inmanta_type.TypedList(inmanta_type.String()) == to_dsl_type(Sequence[str])
assert inmanta_type.List() == to_dsl_type(Sequence)
assert inmanta_type.List() == to_dsl_type(collections.abc.Sequence)
assert inmanta_type.TypedList(inmanta_type.String()) == to_dsl_type(collections.abc.Sequence[str])
assert inmanta_type.TypedDict(inmanta_type.Type()) == to_dsl_type(dict)
assert inmanta_type.TypedDict(inmanta_type.Type()) == to_dsl_type(Mapping)
assert inmanta_type.TypedDict(inmanta_type.String()) == to_dsl_type(dict[str, str])
assert inmanta_type.TypedDict(inmanta_type.String()) == to_dsl_type(Mapping[str, str])

assert inmanta_type.TypedDict(inmanta_type.String()) == to_dsl_type(collections.abc.Mapping[str, str])

assert Null() == to_dsl_type(Union[None])

assert isinstance(to_dsl_type(Any), inmanta_type.Type)
namespace = Namespace("dummy-namespace")
# namespace.set_primitives(inmanta_type.TYPES)
# FIXME: Not working because of std = self.get_ns_from_string("std") std is None
namespace.primitives = inmanta_type.TYPES

location: Range = Range("test", 1, 1, 2, 1)

assert inmanta_type.String() == to_dsl_type(plugin_typing.string, location, namespace)

assert inmanta_type.NullableType(inmanta_type.Integer()) == to_dsl_type(
Annotated[int | None, "something"], location, namespace
)

assert inmanta_type.TypedDict(inmanta_type.Type()) == to_dsl_type(
Annotated[dict[str, int], plugin_typing.InmantaType("dict")], location, namespace
)

# FIXME: Not working
# entity: Entity = Entity("my-entity", namespace)
# namespace.define_type("my-entity", entity)
# assert inmanta_type.TypedDict(inmanta_type.Type()) == to_dsl_type(
# Annotated[Union[int, str] | None, plugin_typing.InmantaType("my-entity")], location, namespace
# )
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a bit awkward setting up the namespace in order to do this. Do you have any tips @sanderr ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make a method / fixture to_dsl_type_simple(python_type: str)?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would reserve custom types for a test with a compile.

assert inmanta_type.Integer() == to_dsl_type(int, location, namespace)
assert inmanta_type.Float() == to_dsl_type(float, location, namespace)
assert inmanta_type.NullableType(inmanta_type.Float()) == to_dsl_type(float | None, location, namespace)
assert inmanta_type.List() == to_dsl_type(list, location, namespace)
assert inmanta_type.TypedList(inmanta_type.String()) == to_dsl_type(list[str], location, namespace)
assert inmanta_type.TypedList(inmanta_type.String()) == to_dsl_type(Sequence[str], location, namespace)
assert inmanta_type.List() == to_dsl_type(Sequence, location, namespace)
assert inmanta_type.List() == to_dsl_type(collections.abc.Sequence, location, namespace)
assert inmanta_type.TypedList(inmanta_type.String()) == to_dsl_type(collections.abc.Sequence[str], location, namespace)
assert inmanta_type.TypedDict(inmanta_type.Type()) == to_dsl_type(dict, location, namespace)
assert inmanta_type.TypedDict(inmanta_type.Type()) == to_dsl_type(Mapping, location, namespace)
assert inmanta_type.TypedDict(inmanta_type.String()) == to_dsl_type(dict[str, str], location, namespace)
assert inmanta_type.TypedDict(inmanta_type.String()) == to_dsl_type(Mapping[str, str], location, namespace)

assert inmanta_type.TypedDict(inmanta_type.String()) == to_dsl_type(collections.abc.Mapping[str, str], location, namespace)

assert Null() == to_dsl_type(Union[None], location, namespace)

assert isinstance(to_dsl_type(Any, location, namespace), inmanta_type.Type)

with pytest.raises(RuntimeException):
to_dsl_type(dict[int, int])
to_dsl_type(dict[int, int], location, namespace)

with pytest.raises(RuntimeException):
to_dsl_type(set[str])
to_dsl_type(set[str], location, namespace)

class CustomList[T](list[T]):
pass
Expand All @@ -63,14 +90,14 @@ class CustomDict[K, V](Mapping[K, V]):
pass

with pytest.raises(RuntimeException):
to_dsl_type(CustomList[str])
to_dsl_type(CustomList[str], location, namespace)

with pytest.raises(RuntimeException):
to_dsl_type(CustomDict[str, str])
to_dsl_type(CustomDict[str, str], location, namespace)

# Check that a warning is produced when implicit cast to 'Any'
caplog.clear()
to_dsl_type(complex)
to_dsl_type(complex, location, namespace)
warning_message = (
"InmantaWarning: Python type <class 'complex'> was implicitly cast to 'Any' because no matching type "
"was found in the Inmanta DSL. Please refer to the documentation for an overview of supported types at the "
Expand Down
7 changes: 6 additions & 1 deletion tests/compiler/test_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,7 @@ def test_native_types(snippetcompiler: "SnippetCompilationTest") -> None:
"""
import plugin_native_types

test_entity = plugin_native_types::TestEntity()
a = "b"
a = plugin_native_types::get_from_dict({"a":"b"}, "a")

Expand All @@ -435,6 +436,10 @@ def test_native_types(snippetcompiler: "SnippetCompilationTest") -> None:
a = plugin_native_types::many_arguments(["a","c","b"], 1)

none = plugin_native_types::as_none("a")
"""
# Annotated types
plugin_native_types::annotated_arg_entity(test_entity) # type value: Annotated[object, InmantaType("TestEntity")]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to have an rejection scenario for this one as well. May require a dedicated test case.

plugin_native_types::annotated_return_entity(test_entity) # type return value: Annotated[object, InmantaType("TestEntity")]
""",
autostd=True,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
autostd=True,
ministd=True,

I think this should suffice here. Less overhead.

)
compiler.do_compile()
5 changes: 5 additions & 0 deletions tests/data/modules/plugin_native_types/model/_init.cf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import std
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not required. Though it doesn't hurt either.

entity TestEntity:
end

implement TestEntity using std::none
15 changes: 15 additions & 0 deletions tests/data/modules/plugin_native_types/plugins/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
Contact: [email protected]
"""

from typing import Any, Annotated
from inmanta.plugins import plugin
from inmanta import plugin_typing


@plugin
Expand All @@ -32,3 +34,16 @@ def many_arguments(il: list[str], idx: int) -> str:
@plugin
def as_none(value: str) -> None:
pass


# Annotated values


@plugin
def annotated_arg_entity(value: Annotated[object, plugin_typing.InmantaType("TestEntity")]) -> None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest trying with a python type that's not natively supported and/or one that is incompatible with the dsl type. i.e. object translates to any, meaning this would be valid even if we ignored InmantaType.

e.g. you could define a custom

class MyEntity(typing.Protocol):
  x: int

and then use Annotated[MyEntity, plugin_typing.InmantaType("TestEntity")]. That would tell us more, because without the annotated, it wouldn't be a valid type signature for a plugin.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you also add one that uses a typedef as its dsl type? e.g. a toy example of a valid use case might be

DSL: typedef response as string matching self in ["yes", "no"]
Python:

@plugin
def process_response(response: Annotated[Literal["yes", "no"], InmantaType("response")]) -> None ...

pass


@plugin
def annotated_return_entity(value: Any) -> Annotated[object, plugin_typing.InmantaType("TestEntity")]:
return value