-
Notifications
You must be signed in to change notification settings - Fork 7
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
base: master
Are you sure you want to change the base?
Add support for Annotated types in plugins #8665
Conversation
@sanderr What happens if we can't find the InmantaType? Do we default to |
tests/compiler/test_plugin_types.py
Outdated
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 | ||
# ) |
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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)
?
src/inmanta/plugin_typing.py
Outdated
There was a problem hiding this comment.
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
src/inmanta/plugin_typing.py
Outdated
import typing | ||
from dataclasses import dataclass | ||
|
||
# TODO: move this module to inmanta.plugins.typing? Probably not because that would import the whole `plugins` namespace? |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
src/inmanta/plugin_typing.py
Outdated
|
||
|
||
# TODO: how to do Entity? "object" is appropriate but raises too many errors for practical use. Any is Any | ||
Entity: typing.TypeAlias = typing.Annotated[object, InmantaType("std::Entity")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs a docstring
src/inmanta/plugin_typing.py
Outdated
|
||
# TODO: how to do Entity? "object" is appropriate but raises too many errors for practical use. Any is Any | ||
Entity: typing.TypeAlias = typing.Annotated[object, InmantaType("std::Entity")] | ||
string: typing.TypeAlias = typing.Annotated[str, InmantaType("string")] |
There was a problem hiding this comment.
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.
src/inmanta/plugin_typing.py
Outdated
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[]")]`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
`Annotated[Sequence[Entity], InmantaType("std::Entity[]")]`. | |
`Annotated[Sequence[MyEntity], InmantaType("std::Entity[]")]`. |
src/inmanta/plugin_typing.py
Outdated
dsl_type: str | ||
|
||
|
||
# TODO: how to do Entity? "object" is appropriate but raises too many errors for practical use. Any is Any |
There was a problem hiding this comment.
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.
src/inmanta/plugins.py
Outdated
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 |
There was a problem hiding this comment.
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.
src/inmanta/plugins.py
Outdated
# 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: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
src/inmanta/plugins.py
Outdated
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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: ...
?
description: Add support for annotated types to plugins. | ||
issue-nr: 8573 | ||
change-type: minor | ||
destination-branches: [master, iso8] |
There was a problem hiding this comment.
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.
tests/compiler/test_plugin_types.py
Outdated
# from inmanta.ast.entity import Entity | ||
# from inmanta.ast.type import TYPES | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Testing artifact?
tests/compiler/test_plugin_types.py
Outdated
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 | ||
# ) |
There was a problem hiding this comment.
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)
?
tests/compiler/test_plugin_types.py
Outdated
# 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 | ||
# ) |
There was a problem hiding this comment.
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.
@@ -0,0 +1,5 @@ | |||
import std |
There was a problem hiding this comment.
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.
tests/compiler/test_plugins.py
Outdated
plugin_native_types::annotated_arg_entity(test_entity) # type value: Annotated[object, InmantaType("TestEntity")] | ||
plugin_native_types::annotated_return_entity(test_entity) # type return value: Annotated[object, InmantaType("TestEntity")] | ||
""", | ||
autostd=True, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
autostd=True, | |
ministd=True, |
I think this should suffice here. Less overhead.
|
||
|
||
@plugin | ||
def annotated_arg_entity(value: Annotated[object, plugin_typing.InmantaType("TestEntity")]) -> None: |
There was a problem hiding this comment.
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.
tests/compiler/test_plugins.py
Outdated
@@ -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")] |
There was a problem hiding this comment.
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 | ||
def annotated_arg_entity(value: Annotated[object, plugin_typing.InmantaType("TestEntity")]) -> None: |
There was a problem hiding this comment.
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 ...
…ue/8573-add-support-for-annotated-types-on-plugin # Conflicts: # src/inmanta/plugins/__init__.py # tests/compiler/test_plugin_types.py # tests/compiler/test_plugins.py # tests/data/modules/plugin_native_types/plugins/__init__.py
…8573-add-support-for-annotated-types-on-plugin
|
||
# Check that a warning is produced when implicit cast to 'Any' | ||
caplog.clear() | ||
to_dsl_type(complex, location, namespace) | ||
to_dsl_type_simple(complex) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This one sparked a laugh
src/inmanta/plugins/typing.py
Outdated
dsl_type: str | ||
|
||
|
||
Entity: typing.TypeAlias = typing.Annotated[object, InmantaType("std::Entity")] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I still want to ask Bart's opinion on object
vs Any
here, but he's not available today. If we do go for object
, I think it might be useful to also introduce Resource
, for which we can introduce a typing.Protocol
with the (read-only) fields that all resources have. i.e. requires
, provides
, receive_events
(...?).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And PurgeableResource
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's not implement those yet, until we know about the object
vs Any
, and get some input from the solutions team on whether these would actually be useful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Something weird seems to have happened here. Could you merge in master? That might make it all a lot easier to review.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, nevermind, I see that I haven't merged the union PR yet. I went over all my PRs in the morning, but this is not officially mine so I missed it. I'll review like this, but it'll be for tomorrow, I didn't get where I'd hoped today.
This reverts commit b07e091.
…tated-types-on-plugin # Conflicts: # changelogs/unreleased/8574-add-support-native-union-types.yml # src/inmanta/plugins/__init__.py # tests/compiler/test_plugin_types.py # tests/compiler/test_plugins.py # tests/data/modules/plugin_native_types/plugins/__init__.py
Description
Introduce Annotated types in plugins
closes #8573
Self Check:
Strike through any lines that are not applicable (
~~line~~
) then check the box