Skip to content

Allow VGroup type subscripting - #3606

Open
JasonGrace2282 wants to merge 25 commits into
ManimCommunity:mainfrom
JasonGrace2282:VGroup_type_subscripting
Open

JasonGrace2282 wants to merge 25 commits into
ManimCommunity:mainfrom
JasonGrace2282:VGroup_type_subscripting

Conversation

@JasonGrace2282

@JasonGrace2282 JasonGrace2282 commented Feb 1, 2024

Copy link
Copy Markdown
Member

Allows things like
VGroup[Rectangle]

[v1]

Adds a __class_getitem__ method to Mobject

This was not useful, as discussed later

[v2]

Make VGroup a generic

Guideline

Since VGroup's are invariant, type VGroup's similar to lists. For example:

Correct:

def get_lines() -> VGroup[Line]:
    return VGroup(Line(), Line()).arrange()

Incorrect:

def get_lines() -> VGroup[Line, Line]:
    return VGroup(Line(), Line()).arrange()

Also renders correctly in the docs, ex LinearTransformationScene.get_ghost_vectors

@JasonGrace2282 JasonGrace2282 added the enhancement Additions and improvements in general label Feb 1, 2024
@JasonGrace2282
JasonGrace2282 marked this pull request as draft February 1, 2024 21:42
@JasonGrace2282

This comment has been minimized.

@JasonGrace2282
JasonGrace2282 marked this pull request as ready for review February 4, 2024 22:55
@JasonGrace2282 JasonGrace2282 added this to the v0.18.1 milestone Feb 13, 2024
@Viicos

Viicos commented Feb 13, 2024

Copy link
Copy Markdown
Member

What are you trying to achieve with this feature? VGroup[Rectangle] seems to be a good use case for generics, but in that case lets inherit from Generic and define the correct type variable(s), as currently the __class_getitem__ has no value for type checkers.

@JasonGrace2282

JasonGrace2282 commented Feb 13, 2024

Copy link
Copy Markdown
Member Author

lets inherit from Generic and define the correct type variable(s)

I agree that would be the best solution, but I wanted to avoid (if possible) having to manually make every class a ParentClass[VMobject] or whatever. If there's no other way, I can do that.
Please correct me if I have any misunderstandings, as I've never used Generic classes before.

currently the __class_getitem__ has no value for type checkers.

Oh I didn't know that, thanks for letting me know :)

@Viicos

Viicos commented Feb 13, 2024

Copy link
Copy Markdown
Member

Having VGroup a generic class can make sense. It would look like:

class VGroup(VMobject, Generic[T], metaclass=ConvertToOpenGL):
    def __init__(self, *vmobjects: T, **kwargs):
        super().__init__(**kwargs)
        self.add(*vmobjects)

But it would imply VGroups can only hold one vmobject type, is it the case?


Mobject[Mobject] doesn't really make sense, the general (but not limited to) use case for generics is to type check values of a container (such as VGroup).


I agree that would be the best solution, but I wanted to avoid (if possible) having to manually make every class a ParentClass[VMobject] or whatever.

Not sure I understood this part, could you please give an example?

@JasonGrace2282

JasonGrace2282 commented Feb 13, 2024

Copy link
Copy Markdown
Member Author

But it would imply VGroups can only hold one vmobject type, is it the case?

It is not in fact the case, but maybe something like T | VMobject could fix it

Not sure I understood this part, could you please give an example?

Sure, ideally we would only change what VGroup inherits from, and every subclass "just works". However, if we inherit from Generic[T] every subclass of VGroup must then inherit from VGroup[T] instead of just VGroup.

class VGroup(VMobject, Generic[T], metaclass=ConvertToOpenGL):
    def __init__(self, *vmobjects: T, **kwargs):
        super().__init__(**kwargs)
        self.add(*vmobjects)
class Subclass1(VGroup):
    pass
class Subclass2(VGroup[T]):
    pass

Subclass1[VMobject] # error
Subclass2[VMobject] # no error

It was just me being lazy and not wanting to have to create a TypeVar("T") everywhere. If you think it's the better thing to do, I'll trust your judgement.

@Viicos

Viicos commented Feb 14, 2024

Copy link
Copy Markdown
Member

I see. While this might look cumbersome to add the type variable to every subclass, this is required for type checkers to understand the generic class. Adding a bare __class_getitem__ will provide no valuable info.

In my opinion, the type variable should only be added to VGroup. Mobject also has submobjects, but it isn't really common to deal with it for plain mobjects.

It is not in fact the case, but maybe something like T | VMobject could fix it

You can do something like:

VMobjectT = TypeVar("VMobjectT", bound=VMobject)

class VGroup(VMobject, Generic[VMobjectT], metaclass=ConvertToOpenGL):

    def __init__(self, *vmobjects: VMobjectT, **kwargs): ...

That way, users that use different vmobjects in a vgroup won't be impacted:

g = VGroup(DashedVMobject(), DashedVMobject())
reveal_type(g)  # VGroup[DashedVMobject()]

g = VGroup(DashedVMobject(), Triangle())
reveal_type(g)  # "VGroup[DashedVMobject | Triangle]" for pyright | "VGroup[VMobject]" for mypy

We could also make use of PEP 696 in this case. It is still in draft but will most probably make it:

from typing_extensions import TypeVar

VMobjectT = TypeVar("VMobjectT", bound=VMobject, default=VMobject)

That way a plain VGroup annotation will still have submobjects annotated as list[VMobject].


As you can see, it is not perfect as mypy and pyright handle unions differently. But that could still work

@JasonGrace2282

Copy link
Copy Markdown
Member Author

And I assume all subclasses would be subclassing VGroup[VMobject] (unless of course it's possible to be more specific)?
e.g.

class ManimBanner(VGroup[VMobject]):
    ...

@Viicos

Viicos commented Feb 16, 2024

Copy link
Copy Markdown
Member

And I assume all subclasses would be subclassing VGroup[VMobject] (unless of course it's possible to be more specific)?

If the subclass behaves the same way VGroup does, it can still be parametrized by the user, so ManimBanner(VGroup[VMobjectT]) would be correct

@JasonGrace2282
JasonGrace2282 marked this pull request as draft February 16, 2024 21:28
@JasonGrace2282 JasonGrace2282 changed the title Add :meth:.Mobject.__class_getitem__ to allow type subscripting Allow VGroup type subscripting Feb 16, 2024
Comment thread manim/mobject/geometry/shape_matchers.py Fixed
@JasonGrace2282 JasonGrace2282 added breaking changes This PR introduces breaking changes and removed breaking changes This PR introduces breaking changes labels Feb 20, 2024
@JasonGrace2282
JasonGrace2282 marked this pull request as ready for review February 27, 2024 03:45
@JasonGrace2282
JasonGrace2282 requested a review from Viicos March 11, 2024 23:33
Comment thread manim/mobject/text/text_mobject.py Fixed
Comment thread manim/mobject/vector_field.py Fixed
@JasonGrace2282

Copy link
Copy Markdown
Member Author

Seeing as PEP 696 was accepted I've gone ahead and made the changes, let me know what you think!

@JasonGrace2282 JasonGrace2282 added the typehints For adding/discussing typehints label Apr 18, 2024
@behackl behackl modified the milestones: v0.18.1, v0.19.0 Apr 24, 2024
@JasonGrace2282
JasonGrace2282 force-pushed the VGroup_type_subscripting branch 2 times, most recently from 9caa588 to 99c3241 Compare April 30, 2024 11:42
Comment thread manim/mobject/types/vectorized_mobject.py Fixed
@Viicos

Viicos commented May 7, 2024

Copy link
Copy Markdown
Member

@JasonGrace2282,

this week I'll have time to review your typing related PRs.

Comment thread manim/mobject/vector_field.py Fixed
@JasonGrace2282 JasonGrace2282 modified the milestones: v0.19.0, v0.20.0 Jul 23, 2024
Comment thread manim/mobject/geometry/shape_matchers.py Fixed
Comment thread manim/mobject/text/text_mobject.py Fixed
Comment thread manim/mobject/text/text_mobject.py Fixed
Comment thread manim/mobject/text/text_mobject.py Fixed

@chopan050 chopan050 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sorry for the long wait. LGTM!

There are still a lot of places where we could subscript VGroup such as matrix.py, table.py or coordinate_systems.py, but the changes are too big and could be done in one or more subsequent PRs.

EDIT: I pushed a small change such that VGroup.__getitem__() now returns a VMobjectT | VGroup[VMobjectT]. This allows for things such as:

lines: VGroup[Line] = VGroup(Line() for _ in range(10))
lines[0].get_unit_vector()  # lines[0] is detected as a Line and this method can be autocompleted
some_lines = lines[:3]  # some_lines is detected as a VGroup[Line]
some_lines[0].get_unit_vector()  # therefore, some_lines[0] is also detected as a Line


def __getitem__(self, key: int | slice) -> VMobject:
@overload
def __getitem__(self, key: int) -> VMobjectT: ...
def __getitem__(self, key: int) -> VMobjectT: ...

@overload
def __getitem__(self, key: slice) -> VGroup[VMobjectT]: ...
@nikolajmunk

Copy link
Copy Markdown
Contributor

Can I ask what the benefit of doing this might be? Unless I'm missing something, I don't see any discussion anywhere in this thread of what problem it solves or how people are intended to use this new typing feature.

VMobjectT = TypeVar("VMobjectT", bound=VMobject, default=VMobject)


class VGroup(VMobject, Generic[VMobjectT], metaclass=ConvertToOpenGL):

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.

Is there a reason for Mobject or VMobject not to be the generic class instead?

@chopan050 chopan050 Sep 14, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The first iterations of this PR had Mobject as the generic class, but it was later changed to only VGroup. It seems the reason is that having Mobject as a generic implied modifying every single subclass of Mobject and it would've been too cumbersome to implement. Viicos suggested making only VGroup generic, saying that it's not really common to deal with the type variable for Mobject submobjects. Jason could explain it better, though.

@chopan050

Copy link
Copy Markdown
Member

Can I ask what the benefit of doing this might be? Unless I'm missing something, I don't see any discussion anywhere in this thread of what problem it solves or how people are intended to use this new typing feature.

Some Mobjects have attributes which are VGroups and/or methods which return VGroups. Examples are:

  • Axes.axes, a VGroup[NumberLine], and Axes.get_axes() which returns the previous .axes;
  • Matrix.get_columns() and Matrix.get_rows() which could be typed to return VGroup[VGroup[VMobjectT]], and Matrix.elements which would be a VGroup[VMobjectT], where VMobjectT could be defined in the element_to_mobject parameter;
  • Table.horizontal_lines and Table.vertical_lines which are VGroup[Line]s;
  • Angle.get_lines() which returns a VGroup[Line];
  • VectorScene.get_basis_vectors() which returns a VGroup[Vector], and VectorScene.ghost_vectors which is also a VGroup[Vector];
  • etc.

Allowing VGroup to be generic lets the user index these groups and get autocompletion on the attributes and methods they can use. It is also useful for devs: inside some methods, we need to index or unpack VGroups obtained from somewhere else, we know that the submobjects are of a certain subclass, and we intend to use attributes or methods specific to that subclass, so having a generic VGroup is helpful for typing.

@nikolajmunk

Copy link
Copy Markdown
Contributor

@chopan050: That makes a lot of sense, at least on the face of it! The Matrix.elements case is particularly compelling to me; this might also be useful for the many mobjects which let you decide whether to use Text or Tex or some other class for building text objects.

I do have one concern which I hope we can go over: IMO, one of the big benefits of Manim is that everything(-ish) is a mobject and can be modified using the same mobject semantics. Could this feature potentially risk limiting this freedom for the end-user?

For example, I could easily see a user do one or more of the following:

ax = Axes(*some_args)
# Let's add a background to the axes for some reason
ax.axes.add(BackgroundRectangle(ax.axes, color=BLACK))
matrix = Matrix(*some_args, element_to_mobject=Text) # matrix.elements is a VGroup[Text]
# Let's replace the 5th element with an image of a funny meme
current_element = matrix.elements[4]
matrix.elements[4] = ImageMobject("my_image.png").set(height=current_element)
# Let's import and use a hypothetical mobject from a plugin. We'll say it has type VGroup[Circle]
from some_plugin import DiskIntersection
disks = DiskIntersection(*some_args)
# For some cool math reason, we can treat some of these disks as squares, so let's add those
disks.add(*[Square().move_to(pos) for pos in square_positions])

Assuming the user is using mypy, what kind of errors would they be getting by doing the above things? Warnings would probably be okay, but I'm personally wary of flagging any "valid" userland code with an error just because it doesn't match an internal type assumption. It's possible that there's an easy workaround that users would be expected to use, but I think my knowledge of mypy and generics isn't strong enough here.

To put it differently: it seems like this feature runs the risk of creating a class of Manim-native mobjects which deliberately restrict their range of "permitted" interactions in a way that appears somewhat arbitrary to the user ("why shouldn't I be able to insert a funny meme in my matrix?").
Is this something that has already been considered and viewed as an acceptable tradeoff for stronger autocomplete and easier typing for devs? If not, I would suggest a discussion of this risk and perhaps some plan and/or documentation for how users are expected to interact with these more-rigorously-typed mobjects.

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

Labels

enhancement Additions and improvements in general typehints For adding/discussing typehints

Projects

Status: 🏗 In progress

Development

Successfully merging this pull request may close these issues.

7 participants