-
-
Notifications
You must be signed in to change notification settings - Fork 369
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 ToolCache
typing
#964
Add ToolCache
typing
#964
Conversation
647227e
to
83e8410
Compare
Explanation of the Hoop JumpingThe primary problem with typing for The The Feedback is welcome. |
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.
FWIW: Visual Studio will make use of type annotations if they're available, but doesn't get bent out of shape if they're missing. Anecdotally, my observation is that PyCharm is a lot more enthusiastic (or noisy, your pick 😄 ) about them.
I'm weakly in favour of adding type annotations - they can be good documentation, and as the codebase grows, they're another piece reinforcing correctness. My concern is the extent to which they become onerous to work with - adding a simple type declaration is mostly harmless; but requiring Optional[List[Dict[Tuple(...
makes code harder to write, and I'm not sure it really improves readability, either.
The other thing to consider is whether we're treating this as "light documentation assistance", or actual type correctness. If we're going to pursue type annotation much further, I'd like to see some CI validation of the type declarations - there's not much point having them if they're wrong :-) My concern on this front is whether adding mypy on a partially typed codebase is going to cause more headaches than benefits; and whether the type of dynamic attribute management that we're doing with AppConfig
is going to lead to contributors playing mypy whack-a-mole with every contribution.
@freakboy3742, This is purely efficiency for me. As for actually enforcing typing.....I'm less interested in that goal....or in achieving some sort of typing coverage. To that end, I have been experimenting with typing for |
83e8410
to
dc6cbd0
Compare
dc6cbd0
to
f3ec03f
Compare
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 makes sense; and it's certainly a lot less onerous to add typing in a few places where it's helpful than to do a wholesale type strictness change.
However, the question about long term maintenance still holds. What (if anything) can we do to ensure these type declarations are accurate, and stay that way? In particular, it makes me a little twitchy that we need to add chunks of code that are marked #pragma nocover
just to keep an IDE happy; looking forward, if a new tool is added, there's nothing other than goodwill to ensure that any additional type declarations are added.
That's not necessarily a blocker from my perspective; I just want to make sure we've exhausted any possible safety mechanisms first.
@@ -22,7 +22,8 @@ precision = 1 | |||
exclude_lines = [ | |||
"pragma: no cover", | |||
"@(abc\\.)?abstractmethod", | |||
"NotImplementedError\\(\\)" | |||
"NotImplementedError\\(\\)", | |||
"if TYPE_CHECKING:", |
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 added this after discovering Ned said it should be part of the default exclude
.
I'll revert back to # pragma: no cover
if you're not a fan.
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'd actually prefer it here; manual #pragma
calls are sometimes inevitable, but are a code smell IMHO. My comment was more about having a fairly big block of code that isn't ever executed that only exists to keep an IDE happy, and the resulting problems ensuring that code is correct.
At first I was thinking testing these annotations is probably overkill....but then I discovered I spelled This sent me down quite a few rabbit holes before getting here.
|
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.
These mostly look like good improvements; a couple of questions/clarifications inline.
I'm not sure the tests are really testing that much, though. They're all tests of strings - so all we're really validating is that we're making the same typo in multiple locations :-) The only way I can think of improving this would be do walk over briefcase.integrations.__dict__
- that would be a list of actual module names, so __name__
would be reliably accurate.
@@ -22,7 +22,8 @@ precision = 1 | |||
exclude_lines = [ | |||
"pragma: no cover", | |||
"@(abc\\.)?abstractmethod", | |||
"NotImplementedError\\(\\)" | |||
"NotImplementedError\\(\\)", | |||
"if TYPE_CHECKING:", |
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'd actually prefer it here; manual #pragma
calls are sometimes inevitable, but are a code smell IMHO. My comment was more about having a fairly big block of code that isn't ever executed that only exists to keep an IDE happy, and the resulting problems ensuring that code is correct.
src/briefcase/integrations/base.py
Outdated
|
||
|
||
# TODO: Implement Tool base class | ||
class Tool(ABC): | ||
"""Tool Base.""" # pragma: no cover | ||
|
||
|
||
TOOL_T = TypeVar("TOOL_T", bound=Union[Tool, ModuleType]) |
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 presume this is an interim measure until we can convert tools like Xcode into full Tool classes?
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.
Not necessarily. This idiom allows IDEs (and type checkers) to track a specific type being used in a higher level function that may operate on many types. In this case, it explicitly hints that the return value of ToolCache.add_tool(name=..., tool=...)
will be the same as the type passed as tool
.
Furthermore, though, ModuleType
is necessary because tools.git
is not a class; it is an actual module. Right now, TOOL_T
isn't even accommodating xcode
and xcode_cli
.
src/briefcase/integrations/base.py
Outdated
:return: The added Tool. | ||
""" | ||
setattr(self, name, tool) | ||
return tool |
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.
This strikes me as an odd construction - is there a reason for using this instead of the previous tools.mytool = mytool; return mytool
? Unless we're planning to do additional verification on tools when they're added, I'm not sure I see what we gain over simple attribute assignment.
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.
This is just for evaluating things added as tools during testing so every tool could be confirmed as typed in ToolCache
. It doesn't serve a functional purpose otherwise.
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'm not sure I follow how that validation works (or how it's a guarantee) - could you elaborate?
On top of that - my concern would be that add_tool
is sufficiently un-idiomatic that the next tool added will inadvertently use tool.mytool = ...
, and as a result, won't get picked up in testing.
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 created ProxyToolCache
in tests.integrations.conftest
to inspect if tools added through ToolCache.add_tool()
are annotated in ToolCache
.
I agree it is straightforward to skip using this wrapper altogether; it would ultimately depend on devs and the reviewers enforcing it.
For the record, I'm not a big fan either of this either given the architecture of ToolCache
....I just couldn't think of anything else to try to meet your reasonable comment below:
looking forward, if a new tool is added, there's nothing other than goodwill to ensure that any additional type declarations are added.
We're a bit on the verge of rolling our own type checker... It may be possible to iterate though the namespace of I think given the low volume and low churn of tools, we can confirm this typing is accurate; if not, I know PyCharm will definitely let people know. While I'm not quite sure how far unaligned we may be...but I'm definitely willing to get on a call if it'd be at all useful to hash out these specifics as well as larger perspectives about typing. Like I mentioned, I would also like to institute a bit more typing for |
To be clear - I definitely don't want to go down the path of rolling our own. The
My concern is that something is committed to main incorrect, but it won't get picked up until someone with a specific editor tries to use the code. Given how often that list will change, maybe eyeballing it is enough.
I don't think we're that far misaligned - I'm mostly pushing around the edges of this because (a) I don't use PyCharm, so I'm not experiencing the underlying problem that is motivating this change, and (b) generally speaking, I'm not a fan of changes that can't be (or aren't) validated by automated testing. That said - practicality also beats purity. At this point, the only things that really stand out to me are the big "ignored" import block in However, if you want to catch up to talk through your plans about typing, I'm happy to jump on a call - grab me on Discord whenever you see me; or, if you want to document them on a ticket, I'm happy to respond there too. |
Out of curiosity, does VS provide typing for tools in At the end of the day, that's the problem I'd really like to solve here.
Agree; I went in to a lot of rabbit holes today trying to find a way to do these imports with coverage. (Although, while the imports don't have coverage, the code also never actually executes either.) Unfortunately, it proved impossible to get coverage without circular import errors....unless I guess we put all the tools and the
Also agree; I can remove Finally, do you see any value in any of the other "testing" I added? I think that's just limited to |
a1f621c
to
850c454
Compare
850c454
to
37528b9
Compare
I removed |
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 think we're getting close; one minor cleanup and one ponderous question inline.
for tool_module_name in briefcase.integrations.__all__: | ||
if tool_module_name not in tools_unannotated: | ||
assert tool_module_name in ToolCache.__annotations__.keys() | ||
for tool_name in tools_for_module(tool_module_name): |
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.
It's not a huge deal, but tools_for_module
is a moderately complex method with an answer that shouldn't change; both of the invocations are occurring inside for loops. Is there any reason we shouldn't pre-compute once and store, rather than re-computing every time?
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 avoided caching mostly for simplicity. Although, tools_for_module()
should only be called twice for any particular module.
I can add the functools.lru_cache
decorator to tools_for_module()
to get caching for "free" though.
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's a bit more complexity than is warranted here. This isn't a big deal; I won't go to the mat over 2 calls to a test function that isn't that complex.
if TYPE_CHECKING: | ||
# Tools are imported only for type checking | ||
# to avoid circular import errors. | ||
import git as git_ |
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 was almost ready to approve this PR... and then I remembered something.
That opens up a bunch of questions:
- On what condition will
TYPE_CHECKING
be true? - Is it possible for the user to have
TYPE_CHECKING
be true and not have a working git install? - Is there any meaningful fallback/error recovery to use in this case?
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 was almost ready to approve this PR... and then I remembered something.
That opens up a bunch of questions:
- On what condition will
TYPE_CHECKING
be true?
TYPE_CHECKING
probably shouldn't ever be True
; even for type checkers like mypy, they should only assume TYPE_CHECKING
is the same as True
.
- Is it possible for the user to have
TYPE_CHECKING
be true and not have a working git install?
It's probably possible if something monkeypatched typing.TYPE_CHECKING
at interpreter startup. But that definitely isn't the intended way to "assume" TYPE_CHECKING
is True
when relevant. I really hope the stronger promise of the design of TYPE_CHECKING
is that it is always False
at runtime.
- Is there any meaningful fallback/error recovery to use in this case?
Not currently. But if we wanted to ensure this was avoided, we could change if TYPE_CHECKING:
to simply if False:
. Although, for instance, pytest
explicitly moved from if False:
once Python 3.5 could use typing.TYPE_CHECKING
.
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 someone is monkey patching an internal variable like TYPE_CHECKING
, I think we're firmly in "you get to keep all the shiny pieces" territory.
On that basis, I think I'm happy with this; if we ever enable mypy as part of the CI checks, we might need to revisit that, but I'm happy to call that a problem for Future Apiarists.
Changes
Added typing support for
ToolCache
for QoL.Specifically:
ToolCache
tools
asToolCache
each place it is introduced in a ToolRationale
Personally, I really like my IDE's hints and auto-completion. The current implementation of
ToolCache
prevents any of these luxuries because its attributes cannot be inferred.briefcase.integrations
becauseself.tools
cannot be inferred as aToolCache
.self.tools
is at least inferred to be aToolCache
(at least for the subclasses that aren't mix-ins) but since none of the Tools are actually defined withinToolCache
, none of them can be inferred.Tradeoff
Unfortunately, this typing requires jumping through a few hoops and a bit more maintenance due to new dependencies within
ToolCache
itself....but I think its ultimately a win.Also interested to know if this is just a PyCharm problem or if VS is in the same boat.
PR Checklist: