-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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 typings to the art resizer module #4649
base: master
Are you sure you want to change the base?
Conversation
The purpose of the delayed imports is to only require these libraries if they actually end up being used. See the
Can be both, since on Linux, the bytes are directly passed on (via |
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.
Did some local tests/type checks on this branch, highlighted some problems.
Other than the comments there are still a few minor problems that need to be addressed too:
- Some
Optional
values whereNone
is not taken into account - Some
str
vs.bytes
mismatches
@@ -66,7 +70,7 @@ class LocalBackendNotAvailableError(Exception): | |||
|
|||
class LocalBackend: | |||
@classmethod | |||
def available(cls): | |||
def available(cls) -> bool: | |||
try: | |||
cls.version() |
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.
We should have an abstract method LocalBackend.version()
for this line to pass the type check.
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.
Is that in scope for this typing PR or a comment on something that needs to be done at another 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.
Up to you, but since this just means adding a
def version(cls) -> ...:
raise NotImplementedError()
I think it's not such a big change that would make this PR intractable. (Actually making LocalBackend
an abc.ABC
and using the @abstractmethod
decorator would be some more work, but I guess with the above stopgap solution we already get quite far, and the @abstractmethod
way would require it anyway.)
@@ -84,7 +88,7 @@ class IMBackend(LocalBackend): | |||
_legacy = None | |||
|
|||
@classmethod | |||
def version(cls): | |||
def version(cls) -> Optional[Union[object, Tuple[int, int, int]]]: |
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 may be out of scope for a typing PR, but I think this annotation is a bit much; if anything it highlights that the version logic is a bit hard to work with.
I'd suggest
- Some other way to handle "no version available" instead of the raw object instance
None
would be way more clear- Could allow for that by changing the check below to
not hasattr(...)
instead of... is None
- Replace the "raw tuple" with a
NamedTuple
All in all this would simplify those annotations by a lot:
class Version(NamedTuple):
major: int
minor: int
patch: int
class IMBacked(LocalBackend):
@classmethod
def version(cls) -> Optional[Version]:
...
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 it is out of scope. I don't think I should be changing the logic of the methods I'm typing in this PR. If you feel differently then we can put changes in motion but I feel like there should be a different PR for those. There's a great deal of places where things could be changed or made simpler, but there's only so many changes before things become unmanageable.
@@ -240,10 +259,15 @@ def convert_format(self, source, target, deinterlaced): | |||
return source | |||
|
|||
@property | |||
def can_compare(self): | |||
def can_compare(self) -> bool: | |||
return self.version() > (6, 8, 7) |
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.
Type checker flags this because it doesn't take into account that the version may be non-existent.
The original signature is a bit too complicated, see my previous comment.
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.
Mypy is not usable at the current stage as an indicator of typing correctness. See next comment.
return True | ||
|
||
def write_metadata(self, file, metadata): | ||
def write_metadata(self, file: AnyStr, metadata: Mapping): | ||
assignments = list(chain.from_iterable( |
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.
Looks like something about this makes it "untypeable", I think we basically have no choice but to explicitly type it as Any
...
beets/util/artresizer.py:344: error: Cannot determine type of "assignments" [has-type]
Doesn't seem like too big of a shortcut to me given that util.command_output
is untyped anyway.
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 you're using mypy to get these hints, then you should know that I have been working under the understanding that mypy is not really relevant at this stage. It is much more strict and even without this PR, throws a huge number of errors. See here for details.
That MyPy gives this error doesn't mean it's true. It doesn't mean that it is actually Any
. Those are the typings that are presented by the codebase and those ones I'm sure of.
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.
Well, I wouldn't say mypy is completely irrelevant, it can definitely provide useful input.
What is certainly the case that having zero mypy issues left is not a requirement for a PR like this to be merged, since that might simply not be achievable without substantial refactoring to resolve issues that mypy reveals.
An example would be the version()
problem mentioned in another comment: The refactoring proposed by @ybnd looks like a great idea, but I also agree that we might want to hold it off for another PR in order to avoid blowing up this one too much (although, in this case, I'd be fine either way: the PR is not that huge yet).
As to the particular issue, I'm somewhat surprised that mypy cannot resolve this to at least a List[Any]
, which would be enough to verify that the list unpacking in the argument list is valid. Maybe explicitly annotating it as List[Any]
would be useful? Or, maybe specifying the Mapping
in the argument more concretely. I suspect this should be Mapping[str, Any]
, maybe even Mapping[str, str]
(but verifying the latter would require some research into the backend methods).
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 had a look through this (notably, I didn't actually run any type checker on it!), and added a few comments.
As @ybnd already pointed out, there are some inconsistencies with the design of the backend classes. Adding an abstract Backend
base class seems like a good way to get a hold on them. However, I'd say let's keep that for a follow-up.
@@ -172,7 +182,7 @@ def resize(self, maxwidth, path_in, path_out=None, quality=0, | |||
|
|||
return path_out | |||
|
|||
def get_size(self, path_in): | |||
def get_size(self, path_in: str) -> Optional[Tuple[int, ...]]: |
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.
def get_size(self, path_in: str) -> Optional[Tuple[int, ...]]: | |
def get_size(self, path_in: bytes) -> Optional[Tuple[int, int]]: |
Note that path_in
is passed through syspath
, i.e. it is a path in beets-internal bytestring-representation.
The output type should match the PIL backend. Probably, we should eventually make the code here validate the IM result completely, including the being exactly length 2 (just catching the IndexError
is definitely not enough (if such an error can even happen?)).
@@ -209,7 +223,7 @@ def deinterlace(self, path_in, path_out=None): | |||
# FIXME: Should probably issue a warning? | |||
return path_in | |||
|
|||
def get_format(self, filepath): | |||
def get_format(self, filepath: AnyStr) -> Optional[bytes]: |
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.
Probably out-of scope here; but this code seems to be broken: All backends should really have the same return type, so we should probably be decoding the stdout to a str
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.
I actually intend to start my grand project to convert to pathlib today on the core modules, so this should be fixed soon regardless.
source: AnyStr, | ||
target: AnyStr, | ||
deinterlaced: bool, | ||
) -> 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.
) -> str: | |
) -> AnyStr: |
given that this returns either of source
or target
.
On second thought: Probably all of these should be bytes
(again, these are paths in beets internal representation, as being evidenced by the syspath
call).
"""A boolean indicating whether the resizing method is performed | ||
locally (i.e., PIL or ImageMagick). | ||
""" | ||
return self.local_method is not None | ||
|
||
def get_size(self, path_in): | ||
def get_size(self, path_in: AnyStr) -> Union[Tuple[int, int], AnyStr]: |
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.
Can you correct this method to return None
in the else
branch? What's currently going on is just wrong (probably due to my own fault... the git blame is somewhat confusing, not sure what happened. Maybe a botched merge? copy&paste bug?)
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 don't think there's a way to conditionally return a single type. I can add it to be Optional
though so it recognises that a null value is possible.
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, what I meant is that this should return Optional[Tuple[int, int]]
, and in the else case return None
instead of return path_in
.
Is this still relevant? If so, what is blocking it? Is there anything you can do to help move it forward? This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. |
Continuing my effort to type the modules of beets, here's another module typed. Few comments:
AnyStr
which includes both bytes and strings for maximum breadth, but if they are truly strings, then this can be tightened.