Closed rsokl closed 3 years ago
I see what might be the trouble here; when an alias gets put into a union (and itself is a union), then we lose the name to union flattening. This is sort of expected at the moment, but I do think we should look into doing something like TS did in 4.2 to preserve alias names:
I can't recall if we have another issue about this specifically, but we do have a couple issues that refer to the tooltips getting too large (and likely are related; we have aliases in stubs for big libs like pandas to shorten names, but they get put into unions in function annotations too).
Thanks for the references and for the explanation!
when an alias gets put into a union (and itself is a union)
Does that apply for these cases?
# (function) nice: (x: Alias) -> None
def nice(x: Alias):
...
# (function) also_nice: (x: Alias = 1) -> None
def also_nice(x: Alias = 1):
...
# (function) messy: (x: int | str | bool | (*args: Unknown, **kwargs: Unknown) -> Unknown | Sequence[int | str | bool | (*args: Unknown, **kwargs: Unknown) -> Unknown | None] | None = None) -> None
def messy(x: Alias = None):
...
Here, the alias itself is not put in a union. The only difference that I can see is that the default value is None
.
That all being said, in my actual use case, simply changing the default value from None
does not fix things. It appears that, no matter what I do, the following alias will always be displayed in full, gory detail.
ZenWrappers = Union[
Union[
None,
Builds[Callable[[_T2], _T2]],
PartialBuilds[Callable[[_T2], _T2]],
Just[Callable[[_T2], _T2]],
Callable[[_T2], _T2],
str,
],
Sequence[
Union[
None,
Builds[Callable[[_T2], _T2]],
PartialBuilds[Callable[[_T2], _T2]],
Just[Callable[[_T2], _T2]],
Callable[[_T2], _T2],
str,
]
],
]
we have aliases in stubs for big libs like pandas to shorten names
Would that be a way to circumvent this? Move my annotations for this module to a stub file?
(Thanks again for the help. I ❤️ Pylance & pyright)
The first two cases (nice
and also_nice
) are working as expected.
This case might be a bug:
def messy(x: Alias = None):
...
There's an old, deprecated typing feature where things like this are legal:
def foo(x: int = None):
reveal_type(x) # int | None
It might be the case that we tack an extra union around Alias
in that case and then drop it once we see that Alias
contains a None
already.
That'd also cover the ZenWrappers
case where you're also using = None
.
I missed that your huge one doesn't go away no matter what; that's confusing as I see this:
Which confirms my suspicion.
One line change to https://github.com/microsoft/pyright/blob/ff8fcd2abdb1cfb10a57557b33500bc3ce40fc81/packages/pyright-internal/src/analyzer/typeEvaluator.ts#L12680 to be:
if (annotatedType && !isOptionalType(annotatedType)) { ... }
And it behaves how it should:
So, that is a simple fix for the cases where the alias already has None
(but, not for when an alias appears in a Union
).
The first two cases (nice and also_nice) are working as expected.
Yep! Agreed 😄
I missed that your huge one doesn't go away no matter what; that's confusing as I see this:
Ah! You are right. I completely overlooked this when moving from my minimum-reproducible example, back to my real-world code.... I have a decorator too 😅
# OK
# (function) zen: (x: ZenWrappers[Unknown]) -> None
def zen(x: ZenWrappers):
pass
_T2 = TypeVar("_T2", bound=Callable)
def wrapped(func: _T2) -> Callable[..., _T2]:
return func
# (function) zen2: (*args: Any, **kwargs: Any) -> (x: (_p0: _T2@ZenWrappers) -> _T2@ZenWrappers | str | Sequence[(_p0: _T2@ZenWrappers) -> _T2@ZenWrappers | str | None] | None) -> None
@wrapped
def zen2(x: ZenWrappers):
pass
So, that is a simple fix for the cases where the alias already has None (but, not for when an alias appears in a Union).
I agree that this fix does work... when there is no decorator present.
So to summarize... there does seem to be a bug for:
# sig does not use alias
def zen(x: ZenWrappers = None): ...
however there is also the case where a decorator is present for which the alias also fails to be reported:
_T2 = TypeVar("_T2", bound=Callable)
def wrapped(func: _T2) -> Callable[..., _T2]:
return func
# sig does not use alias
@wrapped
def zen2(x: ZenWrappers): ...
The decorator thing is probably different; my comment is limited to the single case where we have x: Something = None
and we are applying the "if you do = None
, we'll ensure that the parameter's type includes None
", and won't fix any of the other cases.
Can you provide the full type info for Builds
, PartialBuilds
, and Just
?
Just to be clear, the current behavior is not a bug. It's working as it was designed. We try to retain type alias names when reporting types in hover text, error messages, etc., but if that type is mutated in any way, the type alias name is lost and the full type (the one represented by the type alias) is shown instead.
There is arguably value in trying to retain type aliases that represent unions when those that type is mutated in certain ways (e.g. combined with other types to create a wider union). That's something we've considered doing, but it's not a trivial piece of functionality. Mypy doesn't do this, for example. As Jake said, TypeScript introduced this feature recently, but only after many years of not doing it.
The decorator thing is probably different; my comment is limited to the single case where we have x: Something = None
Totally fair! And apologies for sneaking in the descriptor at the last moment - I had totally overlooked it when switching between examples.
Can you provide the full type info for Builds, PartialBuilds, and Just?
Thanks for the insights, @erictraut !
but if that type is mutated in any way [...]
Perhaps I don't understand what "mutated" means here. How is the following mutated?
# shows full type
def zen(x: ZenWrappers = None): ...
# shows alias name
def zen2(x: ZenWrappers): ...
or are you referring to the decorator case?
When a default argument of None
is used for a parameter, the annotated parameter type is implicitly unioned with None
.
Consider the example:
StrOrBytes = str | bytes
def func(x: StrOrBytes = None): ...
The type of parameter x would be a new union: str | bytes | None
. Internally to pyright, that's tracked as a new type, and the StrOrBytes
type alias no longer applies to it. Pyright creates new types all over the place within its type evaluator — sometimes combining (unioning) multiple types, sometimes narrowing types, sometimes converting type variables into their concrete forms, etc. When I say "mutate", I'm referring to all of those transforms.
Jake is proposing a surgical change that applies to your specific case where we can avoid a mutation (and therefore retain the type alias) by conditionally checking whether the parameter type already contains a None
in the union. That's a worthwhile change, but it's very specific to this case and won't apply to any other forms of type mutations in the type evaluator. The more general case will involve a much bigger change.
Ah, thanks so much for the great explanation. That all makes sense to me.
Apologies again for ending up with a somewhat scattered series of posts here. It had been my intention to keep this focused and slim. I really appreciate both of you spending your time helping me.
Just to try to tidy things up a little bit:
I was able to change the default value associated with my ZenWrappers
field (from None
to tuple
) and have overloads on my function, which fixes the decorator issue for me.
So my signature went from
(function)
zen_wrappers: Builds[(_p0: _T2@ZenWrappers) -> _T2@ZenWrappers] | PartialBuilds[(_p0: _T2@ZenWrappers) -> _T2@ZenWrappers] | Just[(_p0: _T2@ZenWrappers) -> _T2@ZenWrappers] | (_p0: _T2@ZenWrappers) -> _T2@ZenWrappers | str | Sequence[Builds[(_p0: _T2@ZenWrappers) -> _T2@ZenWrappers] | PartialBuilds[(_p0: _T2@ZenWrappers) -> _T2@ZenWrappers] | Just[(_p0: _T2@ZenWrappers) -> _T2@ZenWrappers] | (_p0: _T2@ZenWrappers) -> _T2@ZenWrappers | str | None] | None = None
to
zen_wrappers: ZenWrappers[Unknown] = tuple()
which is great. Thanks again!
I've merged in the targeted change here for the next release: https://github.com/microsoft/pyright/pull/2395
Further stuff definitely gets more complicated.
Hello! I am here from the typing-sig mailing list.
There are a couple of conditions under which Pylance appears stop showing the alias for a type, and instead shows the (very-long) whole type.
None
(whereas a default value of1
works)I happen to be have a case on my hands where I am hitting both in a single field.
Environment data
Boiler Plate
I will include function definitions that leverage the following alias. I will include a comment above each function to indicate the signature that is revealed by Pylance.
Expected behaviour
Actual behaviour
Logs
Code Snippet / Additional information