-
Notifications
You must be signed in to change notification settings - Fork 30
Improve typing #181
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
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'sspanargument 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
pyrightis good, and probably a good minimum on that front. - Sorry if asking something obvious, but could you clarify what you mean by "A
typingextra 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.
| } | ||
|
|
||
| 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_extensionsin order to useSelfmypy1.1.1 to type check the projectMapping,Sequence,Iterable, etc.) where possibleFinalmerge_options()so it can be used by consumersfluent_number()andfluent_date()Iterator[]for iterators rather than the more complexGenerator[]isortonfluent.syntaxandfluent.runtimeThere are probably other things that could be done:
SyntaxNodecould have its__init__typing improved by adding a properly typedspankeyword-only parameter, but I'm not sure if splitting that out fromkwargsis very beneficial for the codebase.typing_extensionsisn'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_extensionsinif 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 annotationscan't be used).pyright, but wasn't sure if that was desired. The public API works fine withpyright, so I wasn't too worried.typingextra could be added tofluent.runtimewithtypes-babelas 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.