""" Ensure objects defined in gitlab.v4.objects have REST* as last item in class definition Original notes by John L. Villalovos An example of an incorrect definition: class ProjectPipeline(RESTObject, RefreshMixin, ObjectDeleteMixin): ^^^^^^^^^^ This should be at the end. Correct way would be: class ProjectPipeline(RefreshMixin, ObjectDeleteMixin, RESTObject): Correctly at the end ^^^^^^^^^^ Why this is an issue: When we do type-checking for gitlab/mixins.py we make RESTObject or RESTManager the base class for the mixins Here is how our classes look when type-checking: class RESTObject(object): def __init__(self, manager: "RESTManager", attrs: Dict[str, Any]) -> None: ... class Mixin(RESTObject): ... # Wrong ordering here class Wrongv4Object(RESTObject, RefreshMixin): ... If we actually ran this in Python we would get the following error: class Wrongv4Object(RESTObject, Mixin): TypeError: Cannot create a consistent method resolution order (MRO) for bases RESTObject, Mixin When we are type-checking it fails to understand the class Wrongv4Object and thus we can't type check it correctly. Almost all classes in gitlab/v4/objects/*py were already correct before this check was added. """ import inspect import pytest import gitlab.v4.objects def test_show_issue(): """Test case to demonstrate the TypeError that occurs""" class RESTObject(object): def __init__(self, manager: str, attrs: int) -> None: ... class Mixin(RESTObject): ... with pytest.raises(TypeError) as exc_info: # Wrong ordering here class Wrongv4Object(RESTObject, Mixin): ... # The error message in the exception should be: # TypeError: Cannot create a consistent method resolution # order (MRO) for bases RESTObject, Mixin # Make sure the exception string contains "MRO" assert "MRO" in exc_info.exconly() # Correctly ordered class, no exception class Correctv4Object(Mixin, RESTObject): ... def test_mros(): """Ensure objects defined in gitlab.v4.objects have REST* as last item in class definition. We do this as we need to ensure the MRO (Method Resolution Order) is correct. """ failed_messages = [] for module_name, module_value in inspect.getmembers(gitlab.v4.objects): if not inspect.ismodule(module_value): # We only care about the modules continue # Iterate through all the classes in our module for class_name, class_value in inspect.getmembers(module_value): if not inspect.isclass(class_value): continue # Ignore imported classes from gitlab.base if class_value.__module__ == "gitlab.base": continue mro = class_value.mro() # We only check classes which have a 'gitlab.base' class in their MRO has_base = False for count, obj in enumerate(mro, start=1): if obj.__module__ == "gitlab.base": has_base = True base_classname = obj.__name__ if has_base: filename = inspect.getfile(class_value) # NOTE(jlvillal): The very last item 'mro[-1]' is always going # to be 'object'. That is why we are checking 'mro[-2]'. if mro[-2].__module__ != "gitlab.base": failed_messages.append( ( f"class definition for {class_name!r} in file {filename!r} " f"must have {base_classname!r} as the last class in the " f"class definition" ) ) failed_msg = "\n".join(failed_messages) assert not failed_messages, failed_msg