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}