1616# along with this program. If not, see <http://www.gnu.org/licenses/>.
1717
1818import importlib
19+ import textwrap
1920from types import ModuleType
2021from typing import Any ,Dict ,Iterable ,NamedTuple ,Optional ,Tuple ,Type
2122
23+ import gitlab
2224from gitlab import types as g_types
2325from gitlab .exceptions import GitlabParsingError
2426
3234]
3335
3436
37+ _URL_ATTRIBUTE_ERROR = (
38+ f"https://python-gitlab.readthedocs.io/en/{ gitlab .__version__ } /"
39+ f"faq.html#attribute-error-list"
40+ )
41+
42+
3543class RESTObject (object ):
3644"""Represents an object built from server data.
3745
@@ -45,13 +53,20 @@ class RESTObject(object):
4553
4654_id_attr :Optional [str ]= "id"
4755_attrs :Dict [str ,Any ]
56+ _created_from_list :bool # Indicates if object was created from a list() action
4857_module :ModuleType
4958_parent_attrs :Dict [str ,Any ]
5059_short_print_attr :Optional [str ]= None
5160_updated_attrs :Dict [str ,Any ]
5261manager :"RESTManager"
5362
54- def __init__ (self ,manager :"RESTManager" ,attrs :Dict [str ,Any ])-> None :
63+ def __init__ (
64+ self ,
65+ manager :"RESTManager" ,
66+ attrs :Dict [str ,Any ],
67+ * ,
68+ created_from_list :bool = False ,
69+ )-> None :
5570if not isinstance (attrs ,dict ):
5671raise GitlabParsingError (
5772"Attempted to initialize RESTObject with a non-dictionary value: "
@@ -64,6 +79,7 @@ def __init__(self, manager: "RESTManager", attrs: Dict[str, Any]) -> None:
6479"_attrs" :attrs ,
6580"_updated_attrs" : {},
6681"_module" :importlib .import_module (self .__module__ ),
82+ "_created_from_list" :created_from_list ,
6783 }
6884 )
6985self .__dict__ ["_parent_attrs" ]= self .manager .parent_attrs
@@ -106,8 +122,22 @@ def __getattr__(self, name: str) -> Any:
106122except KeyError :
107123try :
108124return self .__dict__ ["_parent_attrs" ][name ]
109- except KeyError :
110- raise AttributeError (name )
125+ except KeyError as exc :
126+ message = (
127+ f"{ type (self ).__name__ !r} object has no attribute{ name !r} "
128+ )
129+ if self ._created_from_list :
130+ message = (
131+ f"{ message } \n \n "
132+ + textwrap .fill (
133+ f"{ self .__class__ !r} was created via a list() call and "
134+ f"only a subset of the data may be present. To ensure "
135+ f"all data is present get the object using a "
136+ f"get(object.id) call. For more details, see:"
137+ )
138+ + f"\n \n { _URL_ATTRIBUTE_ERROR } "
139+ )
140+ raise AttributeError (message )from exc
111141
112142def __setattr__ (self ,name :str ,value :Any )-> None :
113143self .__dict__ ["_updated_attrs" ][name ]= value
@@ -229,7 +259,7 @@ def __next__(self) -> RESTObject:
229259
230260def next (self )-> RESTObject :
231261data = self ._list .next ()
232- return self ._obj_cls (self .manager ,data )
262+ return self ._obj_cls (self .manager ,data , created_from_list = True )
233263
234264@property
235265def current_page (self )-> int :