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

a way to refer to result type of an overloaded function #641

Open
kshpytsya opened this issue Jun 6, 2019 · 5 comments
Open

a way to refer to result type of an overloaded function #641

kshpytsya opened this issue Jun 6, 2019 · 5 comments
Labels
topic: feature Discussions about new features for Python's type annotations

Comments

@kshpytsya
Copy link

kshpytsya commented Jun 6, 2019

I am looking for a way to refer to result type of an overloaded function.

To illustrate:

import typing as tp


T = tp.TypeVar("T")


@tp.overload
def gf1(i: int, j: tp.Any) -> str:
    ...


@tp.overload
def gf1(i: tp.Iterable[T], j: bool) -> tp.Dict[T, T]:
    ...


def gf1(i: tp.Union[int, tp.Iterable[T]], j: tp.Any) -> tp.Union[str, tp.Dict[T, T]]:
    pass


gf1.resultType = lambda *a: None



class C(tp.Generic[T]):
    def f1(self) -> gf1.resultType(T, bool):
        pass

    def f2(self) -> gf1.resultType(tp.List[T], bool):
        pass


if not tp.TYPE_CHECKING:
    def reveal_type(x):
        pass


# outputs: builtins.str
reveal_type(gf1(1, True))
# wanted: builtins.str
reveal_type(C[int]().f1())

# outputs: builtins.dict[builtins.int*, builtins.int*]
reveal_type(gf1([1], True))
# wanted: builtins.dict[builtins.int*, builtins.int*]
reveal_type(C[int]().f2())

The f.resultOf syntax is, of course, tentative.

Issue #623 seems to suggest that this may be achievable with a clever use of of Protocol but I just cannot twist my head around that.

Upon request I can elaborate on my specific use case for this but I believe it would suffice to say that in real code gf1.resultType would be used multiple times. Specific argument types for gf1 were chosen arbitrarily for the sake of example.

Not sure whether #548 is anyhow related.

@ilevkivskyi
Copy link
Member

I can see how this can be useful, but this is pretty niche use case so I would say it is rather low priority. It would help to see other examples when this would be useful (e.g. some scenarios where precise type is impossible to express without this feature).

@kshpytsya
Copy link
Author

kshpytsya commented Jun 6, 2019

Let me try to briefly describe my specific use case. I am implementing a module that allows building of Python wrappers around complex external data structures which are the typical combination of "objects" (in json sense), lists and dictionaries. Classes from this module are used in code generated from some external schema. The intended users of the generated code write Python-based configurations. Since those users are not necessary highly skilled Python developers, these wrappers attempt to provide exhaustive run-time type checking and also make best use of static type checking (e.g. with mypy), which, by extension, also provides for working code-completions with recent version of jedi (not yet released, AFAIK).

For brevity, I will omit dictionaries and will mention only objects and lists.

import typing as tp

T = tp.TypeVar("T")

# note, this is not "x: tp.List[T]" but our own list defined below
@tp.overload
def typeSetter(x: "List[T]") -> tp.Union["List[T]", tp.Iterable["typeSetter.resultType(T)"]]:
	...

@tp.overload
def typeSetter(x: T) -> T:
	...

# just to silence the "An overloaded function outside a stub file must have an implementation" error
def typeSetter(x: tp.Any) -> tp.Any:
	pass

class Object:
	...

class ObjectField(tp.Generic[T]):
	def __set_name__(self, owner: tp.Type[Object], name: str) -> None:
		...

	def __set__(self, instance: Object, value: typeSetter.resultType(T)) -> None:
		...

	def __get__(self, instance: Object, owner: tp.Type[Object]) -> T:
		...

class List(tp.MutableSequence[T]):
	...

	# overloads for slice ommitted for brevity 
	def __getitem__(self, i: int) -> T:
		...

	def __setitem__(self, i: int, value: typeSetter.resultType(T)) -> None:
		...

	# of course, all the other methods required by MutableSequence

	# some extra methods
	def some_extra_method(self, x: int) -> None:
		...

The generated code could look like this:

import typing as tp
import container_wrappers as cnt

T = tp.TypeVar("T")


class ExtList1(cnt.List[T]):
	def some_extra_method2(self) -> None:
		...


class ObjectType1(cnt.Object):
	int_field = cnt.ObjectField[int]()
	list_of_str_field = cnt.ObjectField[cnt.List[str]]()
	list_of_list_of_str_field = cnt.ObjectField[cnt.List[ExtList1[str]]]()


class ObjectType2(cnt.Object):
	obj1_list = cnt.ObjectField[ObjectType1]()

The configuration code written by an inexperienced user whom I want to pamper with best diagnostics and code-completion experience possible:

import generated_code


o1 = ObjectType1(...)
reveal_type(o1.int)  # int
reveal_type(o1.list_of_str_field)  # cnt.List[str]
o1.list_of_str.some_extra_method(1)
o1.list_of_str.some_extra_method("x")  # error: first parameter must be "int"
o1.list_of_str = ["a", "b"]
o1.list_of_str = [1, 2]  # error
o1.list_of_list_of_str_field = [["a", "b"], ["c"]]
t1 = o1.list_of_list_of_str_field[0]
reveal_type(t1)  # ExtList1[str]
t1.some_extra_method2()
o1.list_of_list_of_str_field[1] = t1
o1.list_of_list_of_str_field = [[1], 1]  # error

o2 = ObjectType2(...)
o2.obj1_list.append(o1)
o2.obj1_list.some_extra_method(1)

As it is hopefully seen from the code, the key use for the functionality requested in this issue is ability to have different types for getters and setters.
Currently, I have worked around for the lack of this feature by explicitly supplying two types (Tvalue and Tsetter) to Object and List generics but it makes for a very ugly generated code.

@kshpytsya
Copy link
Author

kshpytsya commented Jun 6, 2019

Please note that I have made the following correction:

 @tp.overload
-def typeSetter(x: "List[T]") -> tp.Union["List[T]", tp.Iterable[T]]:
+def typeSetter(x: "List[T]") -> tp.Union["List[T]", tp.Iterable["typeSetter.resultType(T)"]]:
 	...

Actually, I have initially envisioned this feature as a generic pattern based type transformation (i.e. F[T1,...]->Tr) mechanism but it seems that piggybacking on existing function overload semantics and implementation makes it more likely to be accepted and easier to implement.
The advantage of the initially envisioned mechanism would be ability to add specializations in arbitrary order whereas with overload the "fallback" must be the last, precluding from adding specializations in other modules. However, this would require definition of ordering predicate for type specializations which can be quite twisted.

@kshpytsya
Copy link
Author

Bump. I don't want to sound impatient but I would greatly appreciate any conclusions on the destiny of this feature, given my extra input, so as to be able to steer my future development.

@JukkaL
Copy link
Contributor

JukkaL commented Jun 25, 2019

Sorry, this is not going to be implemented any time soon. If you want to be able to do effective type checking, I'm afraid you'll have to rethink your design a bit.

@srittau srittau added topic: feature Discussions about new features for Python's type annotations and removed enhancement labels Nov 4, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
topic: feature Discussions about new features for Python's type annotations
Projects
None yet
Development

No branches or pull requests

4 participants