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

Native python types #8559

Closed
wants to merge 15 commits into from
11 changes: 10 additions & 1 deletion src/inmanta/ast/type.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ def with_base_type(self, base_type: "Type") -> "Type":
"""
return base_type

def __eq__(self, other):
if type(self) != Type: # noqa: E721
# Not for children
return NotImplemented
return type(self) == type(other) # noqa: E721


class NamedType(Type, Named):
def get_double_defined_exception(self, other: "NamedType") -> "DuplicateException":
Expand Down Expand Up @@ -371,7 +377,7 @@ def get_location(self) -> None:
return None

def __eq__(self, other: object) -> bool:
return type(self) == type(other)
return type(self) == type(other) # noqa: E721


@stable_api
Expand Down Expand Up @@ -406,6 +412,9 @@ def type_string_internal(self) -> str:
def get_location(self) -> None:
return None

def __eq__(self, other):
return type(self) == type(other) # noqa: E721


@stable_api
class TypedList(List):
Expand Down
38 changes: 25 additions & 13 deletions src/inmanta/plugins.py
sanderr marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import typing
import warnings
from collections import abc
from typing import TYPE_CHECKING, Any, Callable, Dict, Literal, Mapping, Optional, Sequence, Type, TypeVar
from typing import TYPE_CHECKING, Any, Callable, Literal, Mapping, Optional, Sequence, Type, TypeVar

import typing_inspect

Expand Down Expand Up @@ -216,7 +216,7 @@ def type_string_internal(self) -> str:
return self.type_string()

def __eq__(self, other: object) -> bool:
return type(self) == type(other)
return type(self) == type(other) # noqa: E721


# Define some types which are used in the context of plugins.
Expand All @@ -233,27 +233,30 @@ def __eq__(self, other: object) -> bool:
numbers.Number: inmanta_type.Number(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we want to support this, given that the number type has been deprecated?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Deprecated, but not gone.

I have no strong opinion on this

Copy link
Contributor

Choose a reason for hiding this comment

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

After giving it some more thought, me neither. At some level I'd prefer not to integrate deprecated types into new features, but if it's as simple as this I guess it doesn't hurt too much either. The only thing is perhaps that we have the opportunity to make a clean cut here, to be more visible than the current deprecation warning.

int: inmanta_type.Integer(),
bool: inmanta_type.Bool(),
dict: inmanta_type.LiteralDict(),
list: inmanta_type.LiteralList(),
dict: inmanta_type.TypedDict(inmanta_type.Type()),
list: inmanta_type.List(),
object: inmanta_type.Type(),
}


def to_dsl_type(python_type: type[object]) -> inmanta_type.Type:
sanderr marked this conversation as resolved.
Show resolved Hide resolved
sanderr marked this conversation as resolved.
Show resolved Hide resolved
"""
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.
wouterdb marked this conversation as resolved.
Show resolved Hide resolved
"""
# Any to any
if python_type is typing.Any:
return inmanta_type.Type()

# None to None
if python_type is type(None):
if python_type is type(None) or python_type is None:
return Null()

# Unions and optionals
if typing_inspect.is_union_type(python_type):
# Optional type
if any(typing_inspect.is_optional_type(tt) for tt in typing.get_args(python_type)):
if typing_inspect.is_optional_type(python_type):
other_types = [tt for tt in typing.get_args(python_type) if not typing_inspect.is_optional_type(tt)]
Copy link
Contributor

Choose a reason for hiding this comment

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

>>> typing_inspect.is_optional_type(int | None)
True

So I think we have to be more careful 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.

Why? That seems entirely as expected?

Copy link
Contributor

Choose a reason for hiding this comment

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

But you call it on tt (not python_type), and filter them out if they are optional. So suppose you'd have python_type=Union[int | Optional[str]] (admittedly a convoluted expression but still), wouldn't this evolve to Union[int, None], dropping the str because it is "optional"?

Copy link
Contributor

Choose a reason for hiding this comment

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

When you write it all without quotes, Python probably collapses them together so you can't get in this scenario. But then we have to be very careful about this case when we add support for quoted types.

Copy link
Contributor

Choose a reason for hiding this comment

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

Or even python_type=typing.Union[int, typing.Annotated[typing.Union[str, None], "myannotation"]]. Python can not collapse it there.

Copy link
Contributor

Choose a reason for hiding this comment

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

Then again, typing_inspect.is_optional_type doesn't seem to work at all on typing.Annotated.

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 think Union[int | Optional[str]] == Union[int | str | None]

Optionals always flatten

Copy link
Contributor

Choose a reason for hiding this comment

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

Plain ones do, but not if something non-trivial pops in there. If we plan to never support strings, that's one thing covered. That only leaves Annotated then, but that might not be a concern right now.

if len(other_types) == 0:
# Probably not possible
Expand All @@ -270,27 +273,36 @@ def to_dsl_type(python_type: type[object]) -> inmanta_type.Type:

# Lists and dicts
if typing_inspect.is_generic_type(python_type):
# List
origin = typing.get_origin(python_type)
if origin is Sequence:
# Can this fail?
base: inmanta_type.Type = typing.get_args(python_type)[0]
return inmanta_type.TypedList(to_dsl_type(base))

# dict
if issubclass(origin, collections.abc.Mapping):
args = typing_inspect.get_args(python_type)
assert len(args) == 2
if not args:
return inmanta_type.TypedDict(inmanta_type.Type())

if not issubclass(args[0], str):
raise TypingException(
None, f"invalid type {python_type}, the keys of any dict should be 'str', got {args[0]} instead"
)

if len(args) == 1:
return inmanta_type.TypedDict(inmanta_type.Type())

return inmanta_type.TypedDict(to_dsl_type(args[1]))

# List, set, ...
if issubclass(origin, collections.abc.Collection):
sanderr marked this conversation as resolved.
Show resolved Hide resolved
args = typing.get_args(python_type)
if not args:
return inmanta_type.List()
return inmanta_type.TypedList(to_dsl_type(args[0]))

# TODO annotated types
# if typing.get_origin(t) 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 think this one should also be easy enough to support for 7.4

# 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]
# 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
Expand Down
29 changes: 26 additions & 3 deletions tests/compiler/test_plugin_types.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
"""
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 collections.abc
from typing import Any, Mapping, Union
from typing import Any, Mapping, Sequence, Union

import pytest

Expand All @@ -12,14 +30,19 @@ def test_conversion():
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.LiteralList() == to_dsl_type(list)
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(set[str])
assert inmanta_type.TypedList(inmanta_type.String()) == to_dsl_type(Sequence[str])
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.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.AnyType)
assert isinstance(to_dsl_type(Any), inmanta_type.Type)

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