Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat(api): Use protocols and overload to type hint mixins #3080

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

Conversation

igorp-collabora
Copy link
Contributor

Changes

Currently mixins like ListMixin are type hinted to return base RESTObject instead of a specific class like MergeRequest.

The GetMixin and GetWithoutIdMixin solve this problem by defining a new get method for every defined class. However, this creates a lot of duplicated code.

typing.Protocol can be used to type hint that the mixed in method will return a class matching attribute _obj_cls. The type checker will lookup the mixed in class attribute and adjust the return type accordinly.

Delete tests/unit/meta/test_ensure_type_hints.py file as the get method is no required to be defined for every class.

Closes #2228
Closes #2062

Currently mixins like ListMixin are type hinted to return base
RESTObject instead of a specific class like `MergeRequest`.

The GetMixin and GetWithoutIdMixin solve this problem by defining
a new `get` method for every defined class. However, this creates
a lot of duplicated code.

`typing.Protocol` can be used to type hint that the mixed in method
will return a class matching attribute `_obj_cls`. The type checker
will lookup the mixed in class attribute and adjust the return type
accordinly.

Delete `tests/unit/meta/test_ensure_type_hints.py` file as the `get`
method is no required to be defined for every class.

Signed-off-by: Igor Ponomarev <[email protected]>
@igorp-collabora
Copy link
Contributor Author

igorp-collabora commented Jan 7, 2025

This turned out to be a larger commit than I expected. I can probably split it by a mixin. For example, first commit for Get and GetWithoutId mixins, second one for ListMixin...

Also this is a pretty ad-hoc solution but requires the least amount of changes. Ideally making a RESTManager generic would be better as it would remove all overload. However, this MR can be a starting point to reworking the RESTManager hierarchy.

I made a table of the attributes each mixin uses: #2228 (comment)

Looking at it the RESTManager hierarchy would probably be a base object without _obj_cls and then a subclass with generic _obj_cls: Type[T]

@@ -347,7 +347,6 @@ class RESTManager:
_create_attrs: g_types.RequiredOptional = g_types.RequiredOptional()
_update_attrs: g_types.RequiredOptional = g_types.RequiredOptional()
_path: Optional[str] = None
_obj_cls: Optional[Type[RESTObject]] = None
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _obj_cls needs to be specific for each class. Having a _obj_cls as None in base class will prevent Protocol from working.

@@ -397,10 +397,11 @@ def auth(self) -> None:
The `user` attribute will hold a `gitlab.objects.CurrentUser` object on
success.
"""
self.user = self._objects.CurrentUserManager(self).get()
user = self._objects.CurrentUserManager(self).get()
self.user = user
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason self.user will now be type evaluated as Optional[User]. I had to separate user to a new variable that can't be None.

*,
iterator: Literal[True] = True,
**kwargs: Any,
) -> base.RESTObjectList: ...
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added an overload to the list method so that passing iterator will change if a list is returned or an iterator. This can be separated to another commit and MR.

Also RESTObjectList is not generic. I want to create a follow-up MR there it will be made generic to iterate over a specific type.

@@ -104,12 +104,12 @@ def reset_gitlab(gl: gitlab.Gitlab) -> None:
)
continue

for deploy_token in group.deploytokens.list():
for group_deploy_token in group.deploytokens.list():
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because list() now returns a list containing a specific type it would case a type error here because the same variable is used between two different types.

@@ -4,7 +4,9 @@
https://docs.gitlab.com/ee/api/projects.html#list-projects-starred-by-a-user
"""

from typing import Any, cast, Dict, List, Optional, Union
from __future__ import annotations
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason without delayed annotations type checks failed to resolve types for UserProject.list. This means the quotes around the type annotation had to be removed or managers would not be initialized.

@JohnVillalovos
Copy link
Member

@igorp-collabora Thanks for this. Not ignoring your PRs. Just I haven't had time yet to fully review them. Hopefully this week or next week. Sorry for the delay.

Thanks for the PRs! They look interesting from the description 👍

Copy link

codecov bot commented Jan 7, 2025

Codecov Report

Attention: Patch coverage is 97.63780% with 3 lines in your changes missing coverage. Please review.

Project coverage is 97.19%. Comparing base (f4f7d7a) to head (2335cf8).
Report is 39 commits behind head on main.

Files with missing lines Patch % Lines
gitlab/v4/objects/clusters.py 33.33% 2 Missing ⚠️
gitlab/v4/objects/pipelines.py 50.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3080      +/-   ##
==========================================
+ Coverage   96.68%   97.19%   +0.50%     
==========================================
  Files          96       96              
  Lines        6213     5913     -300     
==========================================
- Hits         6007     5747     -260     
+ Misses        206      166      -40     
Flag Coverage Δ
api_func_v4 83.39% <96.85%> (+0.82%) ⬆️
cli_func_v4 84.32% <92.91%> (+1.62%) ⬆️
unit 89.86% <94.48%> (+0.84%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
gitlab/base.py 100.00% <ø> (ø)
gitlab/client.py 98.72% <100.00%> (+<0.01%) ⬆️
gitlab/mixins.py 93.23% <100.00%> (+0.47%) ⬆️
gitlab/v4/cli.py 91.80% <100.00%> (ø)
gitlab/v4/objects/appearance.py 100.00% <100.00%> (ø)
gitlab/v4/objects/audit_events.py 100.00% <ø> (ø)
gitlab/v4/objects/award_emojis.py 100.00% <ø> (+10.38%) ⬆️
gitlab/v4/objects/badges.py 100.00% <ø> (ø)
gitlab/v4/objects/boards.py 100.00% <ø> (+4.65%) ⬆️
gitlab/v4/objects/branches.py 100.00% <ø> (ø)
... and 56 more

... and 2 files with indirect coverage changes

@igorp-collabora
Copy link
Contributor Author

Sorry for the delay.

No worries. I want to experiment with reworking RESTManager hierarchy meanwhile.

@igorp-collabora
Copy link
Contributor Author

The #3083 is a cleaner solution but more verbose. This one builds more work-around code on top of existing one.

@nejch nejch closed this in #3083 Feb 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Some mixins are not properly type-hinted Type hint on ListMixin.list is too strict
2 participants