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 37 commits into
base: master
Choose a base branch
from

Conversation

jptrindade
Copy link
Contributor

@jptrindade jptrindade commented Jan 20, 2025

Description

Introduce Annotated types in plugins

closes #8573

Self Check:

Strike through any lines that are not applicable (~~line~~) then check the box

  • Attached issue to pull request
  • Changelog entry
  • Type annotations are present
  • Code is clear and sufficiently documented
  • No (preventable) type errors (check using make mypy or make mypy-diff)
  • Sufficient test cases (reproduces the bug/tests the requested feature)
  • Correct, in line with design
  • End user documentation is included or an issue is created for end-user documentation (add ref to issue here: )
  • If this PR fixes a race condition in the test suite, also push the fix to the relevant stable branche(s) (see test-fixes for more info)

@jptrindade jptrindade requested a review from sanderr January 21, 2025 08:54
@jptrindade
Copy link
Contributor Author

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

Comment on lines 37 to 59
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 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

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.



# 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")]
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


# 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")]
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.

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.

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.

# 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

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: ...?

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.

Comment on lines 29 to 31
# 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?

Comment on lines 37 to 59
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

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)?

Comment on lines 54 to 59
# 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

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
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.

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,
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.



@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.

@@ -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
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.

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 ...

@jptrindade jptrindade self-assigned this Jan 22, 2025
@jptrindade jptrindade requested a review from sanderr January 22, 2025 15:13
@jptrindade jptrindade changed the title Draft: Add support for Annotated types in plugins Add support for Annotated types in plugins Jan 22, 2025
sanderr and others added 4 commits January 22, 2025 16:15
…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
@jptrindade jptrindade changed the base branch from master to issue/8574-add-support-union-types January 23, 2025 16:13
@jptrindade jptrindade changed the base branch from issue/8574-add-support-union-types to master January 23, 2025 16:13

# Check that a warning is produced when implicit cast to 'Any'
caplog.clear()
to_dsl_type(complex, location, namespace)
to_dsl_type_simple(complex)
Copy link
Contributor

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

dsl_type: str


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.

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 (...?).

Copy link
Contributor

Choose a reason for hiding this comment

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

And PurgeableResource

Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

native types: annotated types for allow explicit inmanta types
3 participants