-
Notifications
You must be signed in to change notification settings - Fork 815
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
add missing generic types to Worker apis #3937
Conversation
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.
Thanks for this.
A couple of the typing changes can be improved and there are also a couple of situations where I think we can do better.
For reference, this is related to #3862.
@@ -375,7 +375,7 @@ def _populate_node(self, node: TreeNode[DirEntry], content: Iterable[Path]) -> N | |||
) | |||
node.expand() | |||
|
|||
def _directory_content(self, location: Path, worker: Worker) -> Iterator[Path]: | |||
def _directory_content(self, location: Path, worker: Worker[Any]) -> Iterator[Path]: |
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.
If I'm reading the code correctly, this worker
instance comes from _load_directory
, so it would be a Worker[list[Path]]
, which is a bit more specific than Worker[Any]
.
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.
_directory_content
is called with the result of get_current_worker()
which returns (and can only return) a Worker[Any]
, so this would cause an error in _load_directory
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.
Of course I might be misunderstanding something, but what I'm suggesting is that we narrow the type ourselves, which should work.
E.g., the code below typechecks:
from typing import Any
def any_factory() -> Any:
...
integer: int = any_factory()
get_current_worker
is being called inside a worker, so we know that the current worker is the function we're in, so we know its type, right?
That was my reasoning.
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.
Sorry for not getting back to you earlier, it's been a hectic week! But yes, you're right. I have a bad habit of thinking of Any
as the base type, in which case this would have not worked due to invariance, but it's actually an opt-out of the typing system. Personally I always avoid implicit casts from Any
, since it means the cast goes completely unchecked and it makes it easy to introduce errors while still being under a false sense of security.
Incidentally, this reminded me of something that I'd had in the back of my mind for a while as something I've been wanting to try in practice, namely this note on Any
vs object
from this page on typing best practices:
Generally, use Any when a type cannot be expressed appropriately with the current type system or using the correct type is unergonomic.
If a function accepts every possible object as an argument, for example because it’s only passed to str(), use object instead of Any as type annotation. Similarly, if the return value of a callback is ignored, annotate it with object
I actually did have a go this week to see if I could eliminate Any
in favour of object
in my own codebase for better typing safety, and while I wasn't able to remove all uses due to limitations in Python's typing system (notably the lack of wildcards), I did have decent success and I'm definitely more confident in the type checking now.
If that's something you would be interested in I'd be happy to update this PR to use Worker[object]
instead of Worker[Any]
where applicable, which would provide better type safety for users of the code and make clear where explicit casts need to be made. It would mean users having to add an explicit cast if they want to get the result of get_current_worker()
as a Worker[int]
or whatever, but I don't think that's an issue - what would you even use the result type for from the worker itself before the result has been returned? is_cancelled
can be checked even if you don't have the specific type.
Anyway! To get back to the point of your comment, there are three options I see here, in descending order of personal preference:
- Keep the argument as
Worker[Any]
(or better,Worker[object]
)- We're only using the worker to access
is_cancelled
, and I don't think there's any real reason to add more specialisation than we actually need for the function - less casting means more type safety
- We're only using the worker to access
- Have
get_current_worker
returnWorker[object]
but let_directory_content
take aWorker[Path]
, with an explicit cast to silence the type checker- Would at least make it clear that there is casting going on, and it makes it easy to do a search for
cast
later if one wanted to eliminate casting for the sake of stronger typing guarantees in the future
- Would at least make it clear that there is casting going on, and it makes it easy to do a search for
- Stick to
Worker[Any]
forget_current_worker
and rely on implicit casting toWorker[Path]
- Not a fan personally! Hard to spot and easy for issues to slip in. Only real way of tracking this down in the future is to run Mypy with
--disallow-any-expr
which is impossibly noisy
- Not a fan personally! Hard to spot and easy for issues to slip in. Only real way of tracking this down in the future is to run Mypy with
Let me know your thoughts!
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.
Thanks for your comments!
I wasn't aware of the best practice of using object
(instead of Any
) in the contexts listed but it does make sense.
I think we'd better leave it as Worker[Any]
to be in line with the remainder of the code base.
I'll muster the courage to review this PR and then I'll rope someone else in to also take a look!
@davep sorry to bother, but given that this is related to my PR 3862 and that the errors I see with mypy/Pylance differ from the ones you see with pyright, could you check that this PR does reduce the number of pyright errors, please? |
@rodrigogiraoserrao I had a look at this a couple of days back but couldn't see any of the errors mentioned, so made a mental note to return to this after I've had another once-over of my Emacs/eglot/pyright setup to see if there's some extra level of checking I'm missing out on (generally I see all the issues I'd expect with assorted Python code, and a failing mypy run generally tends to coincide with similar complaints from pyright). All of which is to say: I didn't see the errors in the first place. |
Sorry, to clarify, the errors are from my own project - Pyright is complaining about the fact that I have code that calls third-party functions that are missing type parameters. Also, I'm not sure |
I went ahead and fixed up the I also decided to improve the typing of
However, there is a caveat that I couldn't manage to find a solution for. The following is inherently ambiguous: class MyClass(Widget):
@work(thread=True)
async def my_worker() -> int:
return 0 Since both asynchronous and synchronous functions are allowed when
In order to handle this ambiguity I ended up adding a new Still, I think the added type safety is worth it, as demonstrated in the following gist, showing the issues that Mypy and Pyright failed to report previously, and how the inferred types of the worker factories were not very informative: https://gist.github.com/arvidfm/a57eb3f451ab76c26b881c3dd515f3bc. Besides, if the user can't be bothered about type checking they don't need to specify the |
Thanks for the extra work put into the Note: at this point, this PR supersedes #3862. |
I'm afraid we can't accept this. Too many changes makes it hard to understand what the intent was. Suggest breaking it up in to smaller updates, ideally with a discussion beforehand so we can better understand the problem(s). |
makes pyright stop yelling at me
before:
after: