-
Notifications
You must be signed in to change notification settings - Fork 326
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 type annotations #1761
base: main
Are you sure you want to change the base?
Add type annotations #1761
Conversation
|
@@ -327,7 +327,7 @@ class Rule(object): | |||
"""Base package filter rule""" | |||
|
|||
#: Rule name | |||
name = None | |||
name: 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.
This pattern appears on abstract base classes in a number of places, and we have 3 choices:
Option 1:
name: str | None = None
typically produces many spurious mypy errors, because the code assumes the value is a string everywhere, and the None
-case is not handled.
Option 2:
name: str
only safe if we're sure that the name
attribute is always accessed on concrete sub-classes, otherwise will result in an AttributeError
since this does not actually create an attribute.
Option 3:
@property
@abstractmethod
def name(self) -> str:
raise NotImplementedError
Provides us some guarantee that sub-classes actually implement name
, but it cannot be accessed from the class, only from an instance.
def __eq__(self, other): | ||
def __eq__(self, other: object) -> bool: | ||
if not isinstance(other, FallbackComparable): | ||
return NotImplemented |
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
strongly prefers that comparison functions define the type of other
as object
. by returning NotImplemented
we allow other other object to handle equality if it implements it. This should not change the behavior as long as other
is always FallbackComparable
within Rez.
pass | ||
|
||
def iter_packages(self) -> Iterator[Package]: | ||
raise NotImplementedError |
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.
All subclasses of PackageFamilyResource
implement this method, so it appears to be considered an abstractmethod
of PackageFamilyResource
. Defining it simplifies some type annotation problems.
@property | ||
@abstractmethod | ||
def parent(self) -> PackageRepositoryResource: | ||
raise NotImplementedError |
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.
same situation here as with PackageFamilyResource.iter_packages
.
@property | ||
@abstractmethod | ||
def parent(self) -> PackageRepositoryResource: | ||
raise NotImplementedError |
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.
here as well.
@@ -319,6 +326,7 @@ def get_variant(self, index=None): | |||
for variant in self.iter_variants(): | |||
if variant.index == index: | |||
return variant | |||
return 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.
mypy prefer explicit return None
statements
raise ResolvedContextError( | ||
"Cannot perform operation in a failed context") | ||
return _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.
mypy does not like these decorators defined at the class-level.
We have a few options:
|
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1761 +/- ##
==========================================
- Coverage 58.41% 58.27% -0.14%
==========================================
Files 126 126
Lines 17163 17278 +115
Branches 3506 3550 +44
==========================================
+ Hits 10025 10069 +44
- Misses 6473 6519 +46
- Partials 665 690 +25 ☔ View full report in Codecov by Sentry. |
I got bored and added lots more, particularly focused on the solver module. Once the solver module is complete, we can experiment with compiling it to a c-extension using mypyc, which could provide a big speed boost! |
I now have |
self.dirty = True | ||
return super().append(*args, **kwargs) | ||
if not TYPE_CHECKING: | ||
def append(self, *args, **kwargs): |
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.
Since this class inherits from list
it's easier to rely on the type hints coming from that base class than to redefine them here, so we hide them by placing them behind not TYPE_CHECKING
. In reality, the runtime value of TYPE_CHECKING
is always False
.
def get_plugin_class(self, plugin_type: str, plugin_name: str, expected_type: type[T]) -> type[T]: | ||
pass | ||
|
||
def get_plugin_class(self, plugin_type, plugin_name, expected_type=None): | ||
"""Return the class registered under the given plugin name.""" | ||
plugin = self._get_plugin_type(plugin_type) |
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.
Added a new argument here to validate the returned result. This provides both runtime and static validation.
"""Perform a package resolve, and store the result. | ||
|
||
Args: | ||
package_requests (list[typing.Union[str, PackageRequest]]): request | ||
package_requests (list[typing.Union[str, Requirement]]): request |
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 noticed that everywhere that we've documented types as PackageRequest
, they appear to actually be Requirement
. I'm not sure if there any real-world exceptions to this.
@@ -884,7 +912,7 @@ def _rt(t): | |||
return | |||
|
|||
_pr("resolved packages:", heading) | |||
rows = [] | |||
rows3: list[tuple[str, str, 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.
you can't redefine types with mypy, so you need to use new variable names.
@@ -88,7 +88,7 @@ def shell(self): | |||
args = ['ps', '-o', 'args=', '-p', str(parent_pid)] | |||
proc = sp.Popen(args, stdout=sp.PIPE) | |||
output = proc.communicate()[0] | |||
shell = os.path.basename(output.strip().split()[0]).replace('-', '') | |||
shell = os.path.basename(output.decode().strip().split()[0]).replace('-', '') |
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.
output
is bytes
in python3, so need to call decode()
if TYPE_CHECKING: | ||
cached_property = property | ||
else: | ||
class cached_property(object): |
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 much easier to pretend that cached_property
is property
than to type hint all the subtleties of a descriptor.
self.depth_counts: dict | ||
self.solve_begun: bool | ||
self.solve_time: float | ||
self.load_time: float |
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 values are never actually None
, so we define the types here, and their values are assigned in _init()
@@ -74,9 +97,6 @@ class SolverStatus(Enum): | |||
cyclic = ("The solve contains a cycle.", ) | |||
unsolved = ("The solve has started, but is not yet solved.", ) | |||
|
|||
def __init__(self, description): | |||
self.description = description | |||
|
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.
mypyc did not like adding a new __init__
for Enum
, so it was simple enough to replace this attribute with a call to the enum value
.
"""Reset the solver, removing any current solve.""" | ||
if not self.request_list.conflict: | ||
phase = _ResolvePhase(self.request_list.requirements, solver=self) | ||
phase = _ResolvePhase(solver=self) |
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 appears to be a bug: _ResolvePhase
only takes one argument. mypy to the rescue.
I found I probably won't have time to dig into this much more, but once this PR is merged I'll make a new PR with the changes necessary for people to test the compiled version of rez. |
Note: this PR likely invalidates #1745 |
@instinct-vfx Can you or someone from the Rez group have a look at this, please? |
This is a first pass at adding type annotations throughout the code-base. Mypy is not fully passing yet, but it's getting close.
Addresses #1631