-
Notifications
You must be signed in to change notification settings - Fork 26
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 #181
base: main
Are you sure you want to change the base?
Improve typing #181
Conversation
locales: List[str], | ||
resource_ids: List[str], | ||
locales: Sequence[str], | ||
resource_ids: Iterable[str], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just read the other day that a str
is also an Iterable{str]
, we should avoid that type if we really want multiple strings.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In a loosely typed language like Python, I'm not sure this can be avoided. There are two options, in my mind: use a union of a bunch of common invariant collections (Union[List[str], Tuple[str], AbstractSet[str]]
) or add a runtime check to ensure that a string isn't being passed. I really don't like the first option because you're limiting what the user can pass in (a List[MyStringSubtype]
can't be passed in, for instance), which is why I changed this to Sequence[str]
and Iterable[str]
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A third option is to convert from str
to a list()
(which is done in a couple of other places):
def __init__(
self,
locales: Union[str, Sequence[str]],
resource_ids: Union[str, Iterable[str]],
resource_loader: 'AbstractResourceLoader',
use_isolating: bool = False,
bundle_class: Type[FluentBundle] = FluentBundle,
functions: Union['SupportsKeysAndGetItem[str, Callable[[Any], FluentType]]', None] = None,
):
self.locales = [locales] if isinstance(locales, str) else list(locales)
self.resource_ids = [resource_ids] if isinstance(resource_ids, str) else list(resource_ids)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you, this looks pretty great! A few nitpicky things inline.
I share @Pike's concern about str
also qualifying as an Iterable[str]
as well as a Sequence[str]
, but this seems like a pretty common issue that doesn't really have a satisfactory solution?
Regarding your suggestions of additional next steps:
- Improving the
__init__
typing ofSyntaxNode
'sspan
argument probably won't benefit actual users, so we can probably not bother with that. - I considered the same re:
typing_extensions
, but came to the conclusion that the type quoting would get really rather unwieldy, and I think keeping it as a nominal runtime dependency until we can drop 3.6 support shouldn't cause too many problems. - Hearing that the public API satisfies
pyright
is good, and probably a good minimum on that front. - Sorry if asking something obvious, but could you clarify what you mean by "A
typing
extra could be added tofluent.runtime
"?
if '.' in cast(str, self.value): | ||
self.value = FluentFloat(self.value) | ||
self.value = FluentFloat(cast(str, self.value)) | ||
else: | ||
self.value = FluentInt(self.value) | ||
self.value = FluentInt(cast(str, self.value)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Triplicating the cast is a bit much, especially as we already have the source value
right there.
if '.' in cast(str, self.value): | |
self.value = FluentFloat(self.value) | |
self.value = FluentFloat(cast(str, self.value)) | |
else: | |
self.value = FluentInt(self.value) | |
self.value = FluentInt(cast(str, self.value)) | |
if '.' in value: | |
self.value = FluentFloat(value) | |
else: | |
self.value = FluentInt(value) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. However, without the cast to str
and with the change I made to FluentInt
, it's trying to pass what it thinks is a Union[FluentFloat, FluentInt]
to FluentInt()
and marks it as an error. This should probably be cleaned up to something like this:
original_value = cast(str, self.value)
if '.' in original_value:
self.value = FluentFloat(original_value)
else:
self.value = FluentInt(original_value)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The diff is a perhaps a bit misleading. The argument value
is already properly recognised as a str
, and it's assigned to self.value
in the super().__init__()
. So we can just use that directly when re-setting the self.value
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had thought of that but thought that manipulation of value
could happen in the future in FTL.NumberLiteral
or BaseResolver
, so I didn't want to assume too much. Do you foresee that happening? If not, using value
directly is the better solution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That sounds like it'd be excessive magic. Better to use value
directly to add an extra hurdle for such shenanigans.
CURRENCY_DISPLAY_SYMBOL, | ||
CURRENCY_DISPLAY_CODE, | ||
CURRENCY_DISPLAY_NAME, | ||
} | ||
|
||
DATE_STYLE_OPTIONS = { | ||
DATE_STYLE_OPTIONS: Final[Set[Union[Literal['full', 'long', 'medium', 'short'], None]]] = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So explicitly typing this and TIME_STYLE_OPTIONS
below seems a bit excessive. Is there some actual benefit to doing so?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, and I made this change when I was making sure the API worked with Pyright. Mypy is a bit looser validating attr.ib()
calls with validators, so the error doesn't show up there. Without the explicit typing, DATE_STYLE_OPTIONS
is inferred as Set[Union[str, None]]
and the dateStyle
attribute definition is marked as an error by pyright because the validator checks for Union[str, None]
which is wider than the attribute type of Union[Literal['full', 'long', 'medium', short'], None]
. Since this error doesn't affect how pyright interprets the public API, I can revert this and just make it Final
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That sounds good.
https://patrick.cloke.us/posts/2023/02/24/python-str-collection-gotchas/ has some resolutions on their side. |
I'm not sure it's as big of an issue as it's made out to be. I replied to the concern with a couple of options.
👍
I agree. Quoted types are super unwieldy.
👍
Adding an |
I've done the following:
typing_extensions
in order to useSelf
mypy
1.1.1 to type check the projectMapping
,Sequence
,Iterable
, etc.) where possibleFinal
merge_options()
so it can be used by consumersfluent_number()
andfluent_date()
Iterator[]
for iterators rather than the more complexGenerator[]
isort
onfluent.syntax
andfluent.runtime
There are probably other things that could be done:
SyntaxNode
could have its__init__
typing improved by adding a properly typedspan
keyword-only parameter, but I'm not sure if splitting that out fromkwargs
is very beneficial for the codebase.typing_extensions
isn't being used for anything but typing annotations, so it's not really needed as a runtime dependency. However, that would require only importing fromtyping_extensions
inif TYPE_CHECKING:
blocks, but that would require everything that uses those imports to wrap them in strings (since Python 3.6 is still being supported,from __future__ import annotations
can't be used).pyright
, but wasn't sure if that was desired. The public API works fine withpyright
, so I wasn't too worried.typing
extra could be added tofluent.runtime
withtypes-babel
as a requirement to make it easier for consumers to make sure they have all of the types for the public API.Let me know if you'd like me to do any of the above.