cogent3 icon indicating copy to clipboard operation
cogent3 copied to clipboard

some app.typing union types not working when importing annotations from future

Open GavinHuttley opened this issue 1 year ago • 6 comments

The SeqsCollectionType, UnalignedSeqsType don't get resolved by typing.get_constraint_names() when from __future__ import annotations is invoked.

We should detect this setting and either change algorithm for resolving types or raise an exception.

Here's a demo.

# from __future__ import annotations

from cogent3.app.composable import define_app
from cogent3.app.typing import SeqsCollectionType
from cogent3.core.alignment import ArrayAlignment

@define_app
def make_coll(data: dict[str, str]) -> SeqsCollectionType:
    return ArrayAlignment(data)

@define_app
def array_align_to_fasta(aln: ArrayAlignment) -> str:
    return aln.to_fasta()

# the following works with the from future line commented out, but fails with TypeError if it's active
app = make_coll() + array_align_to_fasta()

GavinHuttley avatar Jun 14 '24 16:06 GavinHuttley

So, the problem with this of course is that the from __future__ import annotations stringifys the type hints. This can be resolved through using typing.get_type_hints rather than inspect in composable.py's _get_raw_hints function (though might take some time for me to properly verify).

However, should this example actually proceed without error? For instance, it is currently allowable for make_coll to return something of type SequenceCollection or Alignment in addition to ArrayAlignment but array_align_to_fasta only accepts something of type ArrayAlignment. i.e. shouldn't the first app's output type/s be a subset of the second app's allowable input types?

rmcar17 avatar Jun 26 '24 07:06 rmcar17

Yes because

AlignedSeqsType = TypeVar("AlignedSeqsType", "Alignment", "ArrayAlignment") # this
UnalignedSeqsType = TypeVar("UnalignedSeqsType", bound="SequenceCollection")
SeqsCollectionType = Union[AlignedSeqsType, UnalignedSeqsType] # is part of this

GavinHuttley avatar Jun 26 '24 07:06 GavinHuttley

Perhaps the thing to do for now is to raise detect this case and raise an exception about not currently future proof? (pun intended)

GavinHuttley avatar Jun 26 '24 07:06 GavinHuttley

Yes because

AlignedSeqsType = TypeVar("AlignedSeqsType", "Alignment", "ArrayAlignment") # this
UnalignedSeqsType = TypeVar("UnalignedSeqsType", bound="SequenceCollection")
SeqsCollectionType = Union[AlignedSeqsType, UnalignedSeqsType] # is part of this

Just to follow up on this, so this simpler case shouldn't fail? (it currently doesn't though other type checking tools would complain)

from cogent3.app.composable import define_app

@define_app
def make_num(num: int) -> int | str:
    if num >= 0:
        return num
    return "negative " + str(num)

@define_app
def add_one(num: int) -> int:
    return num + 1

app = make_num() + add_one()

While the app would work for non-negative numbers, int | str is not part of int and tools like mypy would complain about the non-app variant add_one(make_num(3)).

rmcar17 avatar Jun 27 '24 00:06 rmcar17

Perhaps the thing to do for now is to raise detect this case and raise an exception about not currently future proof? (pun intended)

Probably a good idea for now. Though I wonder if another library may suit our needs, typing in python can be quite a complex (and frequently updated between versions) beast

rmcar17 avatar Jun 27 '24 00:06 rmcar17

In terms of your example above, our type introspection code only looks at the types as a set and asks whether there is an intersection. We have to rely on developers making coherent choices about their type hints.

Looking for a third-party library to do this is a great idea.

GavinHuttley avatar Jun 27 '24 03:06 GavinHuttley

fixed in #1931

GavinHuttley avatar Jul 17 '24 07:07 GavinHuttley