labrad / pylabrad

python interface for labrad
51 stars 31 forks source link

Support type annotations for @settings #377

Open gschaffner opened 4 years ago

gschaffner commented 4 years ago

@fanmingyu212 brought this up and it seems like a really nice idea. It also looks like it was briefly mentioned in https://github.com/labrad/pylabrad/issues/162#issue-107612954.

One thing to consider is whether users are using static type checkers. If they are, hints should be of the format

from labrad.types import TInt, TStr
from typing import Generator
from twisted.internet.defer import returnValue

@setting(1)
async def foo(self, c, a: TInt) -> TStr:
    await something(a)
    b = "foo"
    return b

@setting(1)
def foo(self, c, a: TInt) -> Generator[Any, Any, None]:
    yield something(a)
    b = yield "foo"  # not a Deferred, but twisted is OK with this
    returnValue(b)  # technically, this generator returns None

@setting(1)
def foo(self, c, a: TInt) -> Generator[Any, Any, TStr]:
    yield something(a)
    b = yield "foo"  # not a Deferred, but twisted is OK with this
    return b

Technically speaking, the return type hint for twisted generators should be Generator[Any, Any, ???] in order for static type checkers to not become angry. I doubt many users are using static type checkers though, so we could probably just let users use coroutine-style hints and tell them to have their type checker ignore @setting functions. It's unlikely that users are type checking @setting functions anyway since twisted doesn't provide type annotations for inlineCallbacks or most of their library.

maffoo commented 4 years ago

This is one of many reasons why we want async/await support, because the type annotations actually behave nicely :-) Also, I think if we did this we'd want to support standard python types like str, List[str], etc in the annotations, because then the code is compatible with static type checking.

@setting(1)
async def foo(self, c, a: int) -> str:
    b = await something(a)
    return f'b: {b}'

Putting your own object into the annotations is going against the grain, though 3.9 adds typing.Annotated which would allow including both; for labrad this could help disambiguate between, for example, signed and unsigned ints:

@setting(1)
async def foo(self, c, unsigned: Annotated[int, 'w'], signed: Annotated[int, 'i']) -> str:
    return f'{unsigned=}, {signed=}'
gschaffner commented 4 years ago

That makes sense. typing.Annotated looks perfect for this too, but I think it would be extreme to require 3.9+ since nobody is using it with pylabrad yet. A quick search doesn't find a package similar to https://python-future.org but for bringing 3.x stdlib changes to lower 3.x versions.

For int (and similar cases), should we choose a default interpretation of the hint or just require users to clarify in @setting(...)? I think it would be handy to assume that arg: int means signed int, but there may be a downside to this that I'm not seeing.