From 20c899e0406157d0fcc5f046027b07e59cbfe53e Mon Sep 17 00:00:00 2001 From: Nejc Habjan Date: Sat, 7 May 2022 22:50:11 +0200 Subject: [PATCH] feat: display human-readable attribute in `repr()` if present --- gitlab/base.py | 23 +++++++++++---- gitlab/v4/cli.py | 6 ++-- gitlab/v4/objects/applications.py | 2 +- gitlab/v4/objects/commits.py | 4 +-- gitlab/v4/objects/events.py | 2 +- gitlab/v4/objects/files.py | 2 +- gitlab/v4/objects/groups.py | 2 +- gitlab/v4/objects/hooks.py | 6 ++-- gitlab/v4/objects/issues.py | 4 +-- gitlab/v4/objects/members.py | 10 +++---- gitlab/v4/objects/merge_request_approvals.py | 2 +- gitlab/v4/objects/milestones.py | 4 +-- gitlab/v4/objects/projects.py | 12 +------- gitlab/v4/objects/snippets.py | 4 +-- gitlab/v4/objects/tags.py | 4 +-- gitlab/v4/objects/users.py | 14 ++++----- gitlab/v4/objects/wikis.py | 4 +-- tests/unit/test_base.py | 31 ++++++++++++++++++++ 18 files changed, 85 insertions(+), 51 deletions(-) diff --git a/gitlab/base.py b/gitlab/base.py index 7f685425a..a1cd30fda 100644 --- a/gitlab/base.py +++ b/gitlab/base.py @@ -49,8 +49,12 @@ class RESTObject: another. This allows smart updates, if the object allows it. You can redefine ``_id_attr`` in child classes to specify which attribute - must be used as uniq ID. ``None`` means that the object can be updated + must be used as the unique ID. ``None`` means that the object can be updated without ID in the url. + + Likewise, you can define a ``_repr_attr`` in subclasses to specify which + attribute should be added as a human-readable identifier when called in the + object's ``__repr__()`` method. """ _id_attr: Optional[str] = "id" @@ -58,7 +62,7 @@ class RESTObject: _created_from_list: bool # Indicates if object was created from a list() action _module: ModuleType _parent_attrs: Dict[str, Any] - _short_print_attr: Optional[str] = None + _repr_attr: Optional[str] = None _updated_attrs: Dict[str, Any] manager: "RESTManager" @@ -158,10 +162,19 @@ def pprint(self) -> None: print(self.pformat()) def __repr__(self) -> str: + name = self.__class__.__name__ + + if (self._id_attr and self._repr_attr) and (self._id_attr != self._repr_attr): + return ( + f"<{name} {self._id_attr}:{self.get_id()} " + f"{self._repr_attr}:{getattr(self, self._repr_attr)}>" + ) if self._id_attr: - return f"<{self.__class__.__name__} {self._id_attr}:{self.get_id()}>" - else: - return f"<{self.__class__.__name__}>" + return f"<{name} {self._id_attr}:{self.get_id()}>" + if self._repr_attr: + return f"<{name} {self._repr_attr}:{getattr(self, self._repr_attr)}>" + + return f"<{name}>" def __eq__(self, other: object) -> bool: if not isinstance(other, RESTObject): diff --git a/gitlab/v4/cli.py b/gitlab/v4/cli.py index 6830b0874..245897e71 100644 --- a/gitlab/v4/cli.py +++ b/gitlab/v4/cli.py @@ -449,12 +449,12 @@ def display_dict(d: Dict[str, Any], padding: int) -> None: if obj._id_attr: id = getattr(obj, obj._id_attr) print(f"{obj._id_attr.replace('_', '-')}: {id}") - if obj._short_print_attr: - value = getattr(obj, obj._short_print_attr) or "None" + if obj._repr_attr: + value = getattr(obj, obj._repr_attr, "None") value = value.replace("\r", "").replace("\n", " ") # If the attribute is a note (ProjectCommitComment) then we do # some modifications to fit everything on one line - line = f"{obj._short_print_attr}: {value}" + line = f"{obj._repr_attr}: {value}" # ellipsize long lines (comments) if len(line) > 79: line = f"{line[:76]}..." diff --git a/gitlab/v4/objects/applications.py b/gitlab/v4/objects/applications.py index c91dee188..926d18915 100644 --- a/gitlab/v4/objects/applications.py +++ b/gitlab/v4/objects/applications.py @@ -9,7 +9,7 @@ class Application(ObjectDeleteMixin, RESTObject): _url = "/applications" - _short_print_attr = "name" + _repr_attr = "name" class ApplicationManager(ListMixin, CreateMixin, DeleteMixin, RESTManager): diff --git a/gitlab/v4/objects/commits.py b/gitlab/v4/objects/commits.py index 5f13f5c73..19098af0b 100644 --- a/gitlab/v4/objects/commits.py +++ b/gitlab/v4/objects/commits.py @@ -21,7 +21,7 @@ class ProjectCommit(RESTObject): - _short_print_attr = "title" + _repr_attr = "title" comments: "ProjectCommitCommentManager" discussions: ProjectCommitDiscussionManager @@ -172,7 +172,7 @@ def get( class ProjectCommitComment(RESTObject): _id_attr = None - _short_print_attr = "note" + _repr_attr = "note" class ProjectCommitCommentManager(ListMixin, CreateMixin, RESTManager): diff --git a/gitlab/v4/objects/events.py b/gitlab/v4/objects/events.py index b7d8fd14d..048f280b1 100644 --- a/gitlab/v4/objects/events.py +++ b/gitlab/v4/objects/events.py @@ -29,7 +29,7 @@ class Event(RESTObject): _id_attr = None - _short_print_attr = "target_title" + _repr_attr = "target_title" class EventManager(ListMixin, RESTManager): diff --git a/gitlab/v4/objects/files.py b/gitlab/v4/objects/files.py index 435e71b55..e5345ce15 100644 --- a/gitlab/v4/objects/files.py +++ b/gitlab/v4/objects/files.py @@ -24,7 +24,7 @@ class ProjectFile(SaveMixin, ObjectDeleteMixin, RESTObject): _id_attr = "file_path" - _short_print_attr = "file_path" + _repr_attr = "file_path" file_path: str manager: "ProjectFileManager" diff --git a/gitlab/v4/objects/groups.py b/gitlab/v4/objects/groups.py index a3a1051b0..28f3623ed 100644 --- a/gitlab/v4/objects/groups.py +++ b/gitlab/v4/objects/groups.py @@ -48,7 +48,7 @@ class Group(SaveMixin, ObjectDeleteMixin, RESTObject): - _short_print_attr = "name" + _repr_attr = "name" access_tokens: GroupAccessTokenManager accessrequests: GroupAccessRequestManager diff --git a/gitlab/v4/objects/hooks.py b/gitlab/v4/objects/hooks.py index 0b0092e3c..f37d514bc 100644 --- a/gitlab/v4/objects/hooks.py +++ b/gitlab/v4/objects/hooks.py @@ -15,7 +15,7 @@ class Hook(ObjectDeleteMixin, RESTObject): _url = "/hooks" - _short_print_attr = "url" + _repr_attr = "url" class HookManager(NoUpdateMixin, RESTManager): @@ -28,7 +28,7 @@ def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> Hook: class ProjectHook(SaveMixin, ObjectDeleteMixin, RESTObject): - _short_print_attr = "url" + _repr_attr = "url" class ProjectHookManager(CRUDMixin, RESTManager): @@ -75,7 +75,7 @@ def get( class GroupHook(SaveMixin, ObjectDeleteMixin, RESTObject): - _short_print_attr = "url" + _repr_attr = "url" class GroupHookManager(CRUDMixin, RESTManager): diff --git a/gitlab/v4/objects/issues.py b/gitlab/v4/objects/issues.py index f20252bd1..693c18f3b 100644 --- a/gitlab/v4/objects/issues.py +++ b/gitlab/v4/objects/issues.py @@ -42,7 +42,7 @@ class Issue(RESTObject): _url = "/issues" - _short_print_attr = "title" + _repr_attr = "title" class IssueManager(RetrieveMixin, RESTManager): @@ -108,7 +108,7 @@ class ProjectIssue( ObjectDeleteMixin, RESTObject, ): - _short_print_attr = "title" + _repr_attr = "title" _id_attr = "iid" awardemojis: ProjectIssueAwardEmojiManager diff --git a/gitlab/v4/objects/members.py b/gitlab/v4/objects/members.py index 5ee0b0e4e..d5d8766d9 100644 --- a/gitlab/v4/objects/members.py +++ b/gitlab/v4/objects/members.py @@ -28,7 +28,7 @@ class GroupMember(SaveMixin, ObjectDeleteMixin, RESTObject): - _short_print_attr = "username" + _repr_attr = "username" class GroupMemberManager(CRUDMixin, RESTManager): @@ -50,7 +50,7 @@ def get( class GroupBillableMember(ObjectDeleteMixin, RESTObject): - _short_print_attr = "username" + _repr_attr = "username" memberships: "GroupBillableMemberMembershipManager" @@ -73,7 +73,7 @@ class GroupBillableMemberMembershipManager(ListMixin, RESTManager): class GroupMemberAll(RESTObject): - _short_print_attr = "username" + _repr_attr = "username" class GroupMemberAllManager(RetrieveMixin, RESTManager): @@ -88,7 +88,7 @@ def get( class ProjectMember(SaveMixin, ObjectDeleteMixin, RESTObject): - _short_print_attr = "username" + _repr_attr = "username" class ProjectMemberManager(CRUDMixin, RESTManager): @@ -110,7 +110,7 @@ def get( class ProjectMemberAll(RESTObject): - _short_print_attr = "username" + _repr_attr = "username" class ProjectMemberAllManager(RetrieveMixin, RESTManager): diff --git a/gitlab/v4/objects/merge_request_approvals.py b/gitlab/v4/objects/merge_request_approvals.py index d34484b2e..3617131e4 100644 --- a/gitlab/v4/objects/merge_request_approvals.py +++ b/gitlab/v4/objects/merge_request_approvals.py @@ -165,7 +165,7 @@ def set_approvers( class ProjectMergeRequestApprovalRule(SaveMixin, ObjectDeleteMixin, RESTObject): _id_attr = "approval_rule_id" - _short_print_attr = "approval_rule" + _repr_attr = "approval_rule" id: int @exc.on_http_error(exc.GitlabUpdateError) diff --git a/gitlab/v4/objects/milestones.py b/gitlab/v4/objects/milestones.py index da75826db..e415330e4 100644 --- a/gitlab/v4/objects/milestones.py +++ b/gitlab/v4/objects/milestones.py @@ -22,7 +22,7 @@ class GroupMilestone(SaveMixin, ObjectDeleteMixin, RESTObject): - _short_print_attr = "title" + _repr_attr = "title" @cli.register_custom_action("GroupMilestone") @exc.on_http_error(exc.GitlabListError) @@ -102,7 +102,7 @@ def get( class ProjectMilestone(PromoteMixin, SaveMixin, ObjectDeleteMixin, RESTObject): - _short_print_attr = "title" + _repr_attr = "title" _update_uses_post = True @cli.register_custom_action("ProjectMilestone") diff --git a/gitlab/v4/objects/projects.py b/gitlab/v4/objects/projects.py index 7d9c834bd..b7df9ab0e 100644 --- a/gitlab/v4/objects/projects.py +++ b/gitlab/v4/objects/projects.py @@ -129,7 +129,7 @@ class ProjectGroupManager(ListMixin, RESTManager): class Project(RefreshMixin, SaveMixin, ObjectDeleteMixin, RepositoryMixin, RESTObject): - _short_print_attr = "path" + _repr_attr = "path" access_tokens: ProjectAccessTokenManager accessrequests: ProjectAccessRequestManager @@ -186,16 +186,6 @@ class Project(RefreshMixin, SaveMixin, ObjectDeleteMixin, RepositoryMixin, RESTO variables: ProjectVariableManager wikis: ProjectWikiManager - def __repr__(self) -> str: - project_repr = super().__repr__() - - if hasattr(self, "name_with_namespace"): - return ( - f'{project_repr[:-1]} name_with_namespace:"{self.name_with_namespace}">' - ) - else: - return project_repr - @cli.register_custom_action("Project", ("forked_from_id",)) @exc.on_http_error(exc.GitlabCreateError) def create_fork_relation(self, forked_from_id: int, **kwargs: Any) -> None: diff --git a/gitlab/v4/objects/snippets.py b/gitlab/v4/objects/snippets.py index 9d9dcc4e6..83b1378e2 100644 --- a/gitlab/v4/objects/snippets.py +++ b/gitlab/v4/objects/snippets.py @@ -21,7 +21,7 @@ class Snippet(UserAgentDetailMixin, SaveMixin, ObjectDeleteMixin, RESTObject): - _short_print_attr = "title" + _repr_attr = "title" @cli.register_custom_action("Snippet") @exc.on_http_error(exc.GitlabGetError) @@ -91,7 +91,7 @@ def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> Snippet class ProjectSnippet(UserAgentDetailMixin, SaveMixin, ObjectDeleteMixin, RESTObject): _url = "/projects/{project_id}/snippets" - _short_print_attr = "title" + _repr_attr = "title" awardemojis: ProjectSnippetAwardEmojiManager discussions: ProjectSnippetDiscussionManager diff --git a/gitlab/v4/objects/tags.py b/gitlab/v4/objects/tags.py index c76799d20..748cbad97 100644 --- a/gitlab/v4/objects/tags.py +++ b/gitlab/v4/objects/tags.py @@ -13,7 +13,7 @@ class ProjectTag(ObjectDeleteMixin, RESTObject): _id_attr = "name" - _short_print_attr = "name" + _repr_attr = "name" class ProjectTagManager(NoUpdateMixin, RESTManager): @@ -30,7 +30,7 @@ def get(self, id: Union[str, int], lazy: bool = False, **kwargs: Any) -> Project class ProjectProtectedTag(ObjectDeleteMixin, RESTObject): _id_attr = "name" - _short_print_attr = "name" + _repr_attr = "name" class ProjectProtectedTagManager(NoUpdateMixin, RESTManager): diff --git a/gitlab/v4/objects/users.py b/gitlab/v4/objects/users.py index ddcee707a..09964b1a4 100644 --- a/gitlab/v4/objects/users.py +++ b/gitlab/v4/objects/users.py @@ -66,7 +66,7 @@ class CurrentUserEmail(ObjectDeleteMixin, RESTObject): - _short_print_attr = "email" + _repr_attr = "email" class CurrentUserEmailManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManager): @@ -96,7 +96,7 @@ def get( class CurrentUserKey(ObjectDeleteMixin, RESTObject): - _short_print_attr = "title" + _repr_attr = "title" class CurrentUserKeyManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManager): @@ -112,7 +112,7 @@ def get( class CurrentUserStatus(SaveMixin, RESTObject): _id_attr = None - _short_print_attr = "message" + _repr_attr = "message" class CurrentUserStatusManager(GetWithoutIdMixin, UpdateMixin, RESTManager): @@ -128,7 +128,7 @@ def get( class CurrentUser(RESTObject): _id_attr = None - _short_print_attr = "username" + _repr_attr = "username" emails: CurrentUserEmailManager gpgkeys: CurrentUserGPGKeyManager @@ -147,7 +147,7 @@ def get( class User(SaveMixin, ObjectDeleteMixin, RESTObject): - _short_print_attr = "username" + _repr_attr = "username" customattributes: UserCustomAttributeManager emails: "UserEmailManager" @@ -373,7 +373,7 @@ class ProjectUserManager(ListMixin, RESTManager): class UserEmail(ObjectDeleteMixin, RESTObject): - _short_print_attr = "email" + _repr_attr = "email" class UserEmailManager(RetrieveMixin, CreateMixin, DeleteMixin, RESTManager): @@ -392,7 +392,7 @@ class UserActivities(RESTObject): class UserStatus(RESTObject): _id_attr = None - _short_print_attr = "message" + _repr_attr = "message" class UserStatusManager(GetWithoutIdMixin, RESTManager): diff --git a/gitlab/v4/objects/wikis.py b/gitlab/v4/objects/wikis.py index c4055da05..a7028cfe6 100644 --- a/gitlab/v4/objects/wikis.py +++ b/gitlab/v4/objects/wikis.py @@ -13,7 +13,7 @@ class ProjectWiki(SaveMixin, ObjectDeleteMixin, RESTObject): _id_attr = "slug" - _short_print_attr = "slug" + _repr_attr = "slug" class ProjectWikiManager(CRUDMixin, RESTManager): @@ -34,7 +34,7 @@ def get( class GroupWiki(SaveMixin, ObjectDeleteMixin, RESTObject): _id_attr = "slug" - _short_print_attr = "slug" + _repr_attr = "slug" class GroupWikiManager(CRUDMixin, RESTManager): diff --git a/tests/unit/test_base.py b/tests/unit/test_base.py index 17722a24f..0a7f353b6 100644 --- a/tests/unit/test_base.py +++ b/tests/unit/test_base.py @@ -226,6 +226,37 @@ def test_dunder_str(self, fake_manager): " => {'attr1': 'foo'}" ) + @pytest.mark.parametrize( + "id_attr,repr_attr, attrs, expected_repr", + [ + ("id", None, {"id": 1}, ""), + ( + "id", + "name", + {"id": 1, "name": "fake"}, + "", + ), + ("name", "name", {"name": "fake"}, ""), + (None, None, {}, ""), + (None, "name", {"name": "fake"}, ""), + ], + ids=[ + "GetMixin with id", + "GetMixin with id and _repr_attr", + "GetMixin with _repr_attr matching _id_attr", + "GetWithoutIDMixin", + "GetWithoutIDMixin with _repr_attr", + ], + ) + def test_dunder_repr(self, fake_manager, id_attr, repr_attr, attrs, expected_repr): + class ReprObject(FakeObject): + _id_attr = id_attr + _repr_attr = repr_attr + + fake_object = ReprObject(fake_manager, attrs) + + assert repr(fake_object) == expected_repr + def test_pformat(self, fake_manager): fake_object = FakeObject( fake_manager, {"attr1": "foo" * 10, "ham": "eggs" * 15} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy