-
Notifications
You must be signed in to change notification settings - Fork 38
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
Improve typing support for _TupleParser #80
Comments
I've made some changes to the code to support the above cases by utilizing |
@beekill95 Thanks for your idea! I managed to cover cases for 2-tuples with types, giving up on tuples of 3+ elements. The idea was to start small with types here. This issue for 4-tuples (and apparently on 6-tuples, 8-tuples, etc.) wasn't something I had unit tests for. I'm open to improvements in typing for more complex cases, but I'd like to preserve compatibility with all supported versions of Python (3.8+ as for now). Also, I'd like for funcparserlib to keep having no package dependencies (dev-dependencies are OK though). Are there any ways to improve typing for parsers of 3+ tuples without dropping compatibility with 3.8? Maybe via a dev-dependency on |
I don't know how the typing would behave if we just install Another option is to add only the relevant parts of For unit testing, we could utilize |
@beekill95 I've experimented with
I haven't found a combination that was good for most cases for Python < 3.11. My best shot was this Patch A:
But Patch A didn't work with code inspections in PyCharm. It produced false positive errors for functions that use Another version was to run Mypy and take into account IDE inspections only for Python 3.11 and 3.12, this is Patch B:
I wonder how many people may still want to use Mypy or edit their code in IDEs with Python 3.10 or older. Patch B won't work for them and they won't be able to fix things by installing At this moment I haven't come up with better options that I would be confident to introduce. I'm open to new options. |
@beekill95 I'm wondering how your PR might look like... If it solves typing for tuple parsers of any length, it may convince me to drop support for 3.10 and earlier from the code editor perspective. It would still be possible to use funcparserlib with 3.8-3.10 (e.g. for anyone still using it as a dependency in production). Only actual code editing experience for 3.8-3.10 will become worse. |
@vlasovskikh, I just created a draft PR #81. I've tested on my editors (VScode/Neovim + pyright lsp) with python 3.10, the type hinting seems correct for me:
You're right about Pycharm, they're still implementing the support for Let me know how you feel about this approach. |
@beekill95 I've quickly looked through the PR and noticed a few possible problems with typing. Pre-commit checks have caught these problems too. Based on your idea about Source code of the example: from __future__ import annotations
from dataclasses import dataclass, field
from typing import Generic, TypeVar, TypeVarTuple, assert_type, overload
from unittest import TestCase
T = TypeVar("T")
V = TypeVar("V")
Ts = TypeVarTuple("Ts")
Ks = TypeVarTuple("Ks")
@dataclass
class Ign:
value: None = field(default=None, init=False)
@overload
def __add__(self, other: Ign) -> Ign:
pass
@overload
def __add__(self, other: Val[T]) -> Val[T]:
pass
@overload
def __add__(self, other: Tpl[*Ts]) -> Tpl[*Ts]:
pass
def __add__(self, other: Val[T] | Tpl[*Ts] | Ign) -> Val[T] | Tpl[*Ts] | Ign:
if isinstance(other, Ign):
return self
else:
return other
@dataclass
class Val(Generic[T]):
value: T
def __neg__(self) -> Ign:
return Ign()
@overload
def __add__(self, other: Ign) -> Val[T]:
pass
@overload
def __add__(self, other: Val[V]) -> Tpl[T, V]:
pass
@overload
def __add__(self, other: Tpl[*Ts]) -> Tpl[T, tuple[*Ts]]:
pass
def __add__(
self, other: Val[V] | Tpl[*Ts] | Ign
) -> Tpl[T, V] | Tpl[T, tuple[*Ts]] | Val[T]:
if isinstance(other, Ign):
return self
else:
return Tpl((self.value, other.value)) # type: ignore[return-value]
@dataclass
class Tpl(Generic[*Ts]):
value: tuple[*Ts]
@overload
def __add__(self, other: Ign) -> Tpl[*Ts]:
pass
@overload
def __add__(self, other: Val[T]) -> Tpl[*Ts, T]:
pass
@overload
def __add__(self, other: Tpl[*Ks]) -> Tpl[*Ts, tuple[*Ks]]:
pass
def __add__(
self, other: Val[T] | Tpl[*Ks] | Ign
) -> Tpl[*Ts, T] | Tpl[*Ts] | Tpl[*Ts, tuple[*Ks]]:
if isinstance(other, Ign):
return self
else:
return Tpl(self.value + (other.value,)) # type: ignore[return-value]
class TypeVarTupleTest(TestCase):
def test_every_1(self) -> None:
assert_type(Val("a"), Val[str])
assert_type(-Val("a"), Ign)
self.assertEqual(Val("a").value, "a")
self.assertEqual((-Val("a")).value, None)
def test_every_2(self) -> None:
assert_type(-Val("a") + -Val("b"), Ign)
assert_type(Val("a") + -Val("b"), Val[str])
assert_type(-Val("a") + Val("b"), Val[str])
assert_type(Val("a") + Val("b"), Tpl[str, str])
self.assertEqual((-Val("a") + -Val("b")).value, None)
self.assertEqual((Val("a") + -Val("b")).value, "a")
self.assertEqual((-Val("a") + Val("b")).value, "b")
self.assertEqual((Val("a") + Val("b")).value, ("a", "b"))
def test_all_3_ignored_of_3(self) -> None:
assert_type(-Val("a") + -Val("b") + -Val("c"), Ign)
assert_type(-Val("a") + (-Val("b") + -Val("c")), Ign)
self.assertEqual((-Val("a") + -Val("b") + -Val("c")).value, None)
self.assertEqual((-Val("a") + (-Val("b") + -Val("c"))).value, None)
def test_every_2_ignored_of_3(self) -> None:
# Left-associative
assert_type(Val("a") + -Val("b") + -Val("c"), Val[str])
assert_type(-Val("a") + Val("b") + -Val("c"), Val[str])
assert_type(-Val("a") + -Val("b") + Val("c"), Val[str])
self.assertEqual((Val("a") + -Val("b") + -Val("c")).value, "a")
self.assertEqual((-Val("a") + Val("b") + -Val("c")).value, "b")
self.assertEqual((-Val("a") + -Val("b") + Val("c")).value, "c")
# Right-associative
assert_type(Val("a") + (-Val("b") + -Val("c")), Val[str])
assert_type(-Val("a") + (Val("b") + -Val("c")), Val[str])
assert_type(-Val("a") + (-Val("b") + Val("c")), Val[str])
self.assertEqual((Val("a") + (-Val("b") + -Val("c"))).value, "a")
self.assertEqual((-Val("a") + (Val("b") + -Val("c"))).value, "b")
self.assertEqual((-Val("a") + (-Val("b") + Val("c"))).value, "c")
def every_1_ignored_of_3(self) -> None:
# Left-associative
assert_type(Val("a") + Val("b") + -Val("c"), Tpl[str, str])
assert_type(Val("a") + -Val("b") + Val("c"), Tpl[str, str])
assert_type(-Val("a") + Val("b") + Val("c"), Tpl[str, str])
self.assertEqual((Val("a") + Val("b") + -Val("c")).value, ("a", "b"))
self.assertEqual((Val("a") + -Val("b") + Val("c")).value, ("a", "c"))
self.assertEqual((-Val("a") + Val("b") + Val("c")).value, ("b", "c"))
# Right-associative
assert_type(Val("a") + (Val("b") + -Val("c")), Tpl[str, str])
assert_type(Val("a") + (-Val("b") + Val("c")), Tpl[str, str])
assert_type(-Val("a") + (Val("b") + Val("c")), Tpl[str, str])
self.assertEqual((Val("a") + (Val("b") + -Val("c"))).value, ("a", "b"))
self.assertEqual((Val("a") + (-Val("b") + Val("c"))).value, ("a", "c"))
self.assertEqual((-Val("a") + (Val("b") + Val("c"))).value, ("b", "c"))
def test_all_3(self) -> None:
assert_type(Val("a") + Val("b") + Val("c"), Tpl[str, str, str])
assert_type(Val("a") + (Val("b") + Val("c")), Tpl[str, tuple[str, str]])
self.assertEqual((Val("a") + Val("b") + Val("c")).value, ("a", "b", "c"))
self.assertEqual((Val("a") + (Val("b") + Val("c"))).value, ("a", ("b", "c")))
def test_all_4(self) -> None:
assert_type(
Val("a") + Val("b") + Val("c") + Val("d"),
Tpl[str, str, str, str],
)
assert_type(
Val("a") + Val("b") + (Val("c") + Val("d")),
Tpl[str, str, tuple[str, str]],
)
assert_type(
Val("a") + (Val("b") + Val("c")) + Val("d"),
Tpl[str, tuple[str, str], str],
)
assert_type(
Val("a") + (Val("b") + Val("c") + Val("d")),
Tpl[str, tuple[str, str, str]],
)
self.assertEqual(
(Val("a") + Val("b") + Val("c") + Val("d")).value,
("a", "b", "c", "d"),
)
self.assertEqual(
(Val("a") + Val("b") + (Val("c") + Val("d"))).value,
("a", "b", ("c", "d")),
)
self.assertEqual(
(Val("a") + (Val("b") + Val("c")) + Val("d")).value,
("a", ("b", "c"), "d"),
)
self.assertEqual(
(Val("a") + (Val("b") + Val("c") + Val("d"))).value,
("a", ("b", "c", "d")),
) |
@east825 ^ FYI this example is correct from the perspective of PEP 646. Both Mypy and Pylance are fine with it. However, in PyCharm, it results in |
@vlasovskikh, I've fixed some of the pre-commit errors, however, it seems that this is not as easy as I thought. Here are the problems I'm facing:
|
@beekill95 Sorry for the delay in replying to your comment and your updated PR. I've looked at this problem shortly and haven't found a working solution yet. It looks like it won't be enough to make changes to type signatures. We may have to make |
Right now, when working with 3 parsers combined together, the resulting type of the combined parser is
Any
.For instance,
, and with 4 parsers, the resulting type is
Tuple
:This is troublesome when working with mypy for type checking. For example, the second code snippet will lead to mypy error:
Ideally, for both examples, the resulting parsers should be
_TupleParser
, and the resulting types should be recognized correctly,tuple[str, str, str]
andtuple[str, str, str, str]
respectively.The text was updated successfully, but these errors were encountered: