Skip to content

Fix InlineModelAdmin parent-object method signatures#3308

Open
MyGodIsHe wants to merge 2 commits into
typeddjango:masterfrom
MyGodIsHe:issues-3224
Open

Fix InlineModelAdmin parent-object method signatures#3308
MyGodIsHe wants to merge 2 commits into
typeddjango:masterfrom
MyGodIsHe:issues-3224

Conversation

@MyGodIsHe
Copy link
Copy Markdown

@MyGodIsHe MyGodIsHe commented Apr 14, 2026

PR Summary
I fixed the InlineModelAdmin.get_readonly_fields typing issue from #3224 and added regression tests for it. The new test covers StackedInline[Child, Parent] with obj: Parent | None, and I also added a negative case to make sure wrong obj types are still rejected.

I ended up using BaseModelAdmin[Any] in InlineModelAdmin because I couldn’t find a cleaner way to satisfy mypy override checks while keeping compatibility and preserving Django’s inline parent-object semantics. I’m not fully happy with this approach, and I’m still not 100% sure Any can’t leak into some edge paths and affect typing somewhere unexpected. I tried to reduce that risk by explicitly typing the important inline methods and adding tests, but I’d definitely welcome a better alternative.

Comment on lines +345 to +347
@property
@override
def view_on_site(self) -> Callable[[_ParentModelT], str | None] | bool: ...
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Are class level view_on_site = False still accepted ?

This can be a boolena or a method https://docs.djangoproject.com/en/6.0/ref/contrib/admin/#django.contrib.admin.ModelAdmin.view_on_site

_ParentModelT = TypeVar("_ParentModelT", bound=Model)

class InlineModelAdmin(BaseModelAdmin[_ChildModelT], Generic[_ChildModelT, _ParentModelT]):
class InlineModelAdmin(BaseModelAdmin[Any], Generic[_ChildModelT, _ParentModelT]):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Why the change from _ChildModelT to Any ?

Can it cause issue with get_sortable_by ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Because with _ChildModelT, this case is incompatible with the base class signature.

If we keep class InlineModelAdmin(BaseModelAdmin[_ChildModelT], ...) then in BaseModelAdmin, the method get_readonly_fields is typed as obj: _ChildModelT | None.

However, for a real Django inline, it needs to be obj: _ParentModelT | None.


There is another option: introduce an additional type parameter.

_ObjT = TypeVar("_ObjT", bound=Model, default=_ModelT)

class BaseModelAdmin(Generic[_ModelT, _ObjT]): ...

class InlineModelAdmin(
    BaseModelAdmin[_ChildModelT, _ParentModelT],
    Generic[_ChildModelT, _ParentModelT],
): ...

This approach is cleaner, but it changes the public API. What do you think?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This approach is cleaner

I'm not sure it is, the notion of Child/Parent leak to BaseModelAdmin which does not have to know about it and I think it might be confusing for users to have 2 TypeVar on ModelAdmin while it make sense on InlineModelAdmin, maybe your approach with Any is correct but it does mean we have to redefine every method that would have used _ChildModelT in BaseModelAdmin right ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Yes, that's right.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

With the second approach, _ObjT has a default (_ObjT = _ModelT), so existing one-parameter usages should continue to work.
The model becomes more complex, but it matches Django’s real semantics: InlineModelAdmin is child-model based, while obj in many hooks is parent-model based.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

still feels off to me to declare a typevar in BaseModelAdmin that we don't use in the class directly.
I think the version with Any is better, just need to make sure we redefine every method with the new typevars

Comment on lines +343 to +347
@override
def get_view_on_site_url(self, obj: _ParentModelT | None = ...) -> str | None: ...
@property
@override
def view_on_site(self) -> Callable[[_ParentModelT], str | None] | bool: ...
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@override
def view_on_site(self, obj: "A") -> str:
return "http://example.org"
- case: test_inline_get_readonly_fields_parent_obj
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I've just migrated the rest of this file test to the tests/assert_type folder so that it also checks ty/pyrefly/pyright.

Could you rebase on master and put these two test case in the file tests/assert_type/contrib/admin/test_options.py ?

Thanks !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants